forked from 1b-agent/opencadstudio-upstream
fix(block-cache): preserve per-wire colour and fill precision in cached blocks
Three related bugs in tessellate_sub_local and expand_defn: 1. Wire folding lost per-segment colour. Multi-wire tessellator output was concatenated (NaN-separated) into a single LocalWire using the first wire's colour. Inline \C/\c MTEXT colour overrides inside block definitions rendered in the entity colour instead of the override colour. Fix: tessellate_sub_local returns Vec<LocalWire>; each wire from the tessellator becomes its own LocalWire with its own colour. 2. fill_tris_low was never forwarded. The double-single precision path was silently degraded on cached wires. Fix: fill_tris_low forwarded from WireModel through to LocalWire and BatchEntry. debug_assert_eq! added at emit site to catch future invariant violations. 3. Fill-only wires hit the text LOD ladder. MTEXT outline/fill pairs produce separate LocalWires; the fill-only wire was routed through emit_text_baseline/emit_greeked_text which ignore fill_tris (invisible double-draw). Fix: is_fill_only field on LocalWire bypasses the LOD ladder. Also fixes StyleKey::fill_only being hardcoded false at both emit_wire call sites (latent batch misclassification).
This commit is contained in:
1 parent
06bb6b54ff
commit
7da8fbe316
1 file changed
+85
-77
Vendored
+85
-77
@@ -42,12 +42,20 @@ pub struct LocalWire {
|
|||||||
pub snap_pts: Vec<(glam::DVec3, SnapHint)>,
|
pub snap_pts: Vec<(glam::DVec3, SnapHint)>,
|
||||||
pub tangent_geoms: Vec<TangentGeom>,
|
pub tangent_geoms: Vec<TangentGeom>,
|
||||||
pub fill_tris: Vec<[f32; 3]>,
|
pub fill_tris: Vec<[f32; 3]>,
|
||||||
|
pub fill_tris_low: Vec<[f32; 3]>,
|
||||||
|
/// Per-wire colour from the tessellator output. For most entities this
|
||||||
|
/// equals the sub-entity's resolved colour. For colour-split MTEXT
|
||||||
|
/// (`\C`/`\c` inline overrides) each wire carries its own override colour.
|
||||||
pub color: [f32; 4],
|
pub color: [f32; 4],
|
||||||
pub aci: u8,
|
pub aci: u8,
|
||||||
pub pattern_length: f32,
|
pub pattern_length: f32,
|
||||||
pub pattern: [f32; 8],
|
pub pattern: [f32; 8],
|
||||||
pub line_weight_px: f32,
|
pub line_weight_px: f32,
|
||||||
pub plinegen: bool,
|
pub plinegen: bool,
|
||||||
|
/// Set at construction; used to discriminate fill-only GPU batches from
|
||||||
|
/// stroke batches in [`StyleKey`]. Derived from
|
||||||
|
/// `points.is_empty() && !fill_tris.is_empty()`.
|
||||||
|
pub is_fill_only: bool,
|
||||||
pub color_is_byblock: bool,
|
pub color_is_byblock: bool,
|
||||||
pub lt_is_byblock: bool,
|
pub lt_is_byblock: bool,
|
||||||
pub lw_is_byblock: bool,
|
pub lw_is_byblock: bool,
|
||||||
@@ -292,9 +300,7 @@ fn build_defn(
|
|||||||
subs.push(LocalSub::Nested(build_nested_ref(nested_ins, doc, bg_color)));
|
subs.push(LocalSub::Nested(build_nested_ref(nested_ins, doc, bg_color)));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(lw) =
|
for lw in tessellate_sub_local(doc, entity, anno_scale, bg_color) {
|
||||||
tessellate_sub_local(doc, entity, anno_scale, bg_color)
|
|
||||||
{
|
|
||||||
subs.push(LocalSub::Wire(lw));
|
subs.push(LocalSub::Wire(lw));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +350,7 @@ fn tessellate_sub_local(
|
|||||||
sub: &EntityType,
|
sub: &EntityType,
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
bg_color: [f32; 4],
|
bg_color: [f32; 4],
|
||||||
) -> Option<LocalWire> {
|
) -> Vec<LocalWire> {
|
||||||
let h = sub.common().handle;
|
let h = sub.common().handle;
|
||||||
|
|
||||||
// Sanity guard: skip sub-entities whose primary dimension is so large
|
// Sanity guard: skip sub-entities whose primary dimension is so large
|
||||||
@@ -352,7 +358,7 @@ fn tessellate_sub_local(
|
|||||||
// of points. These are typically corrupt-radius primitives that slipped
|
// of points. These are typically corrupt-radius primitives that slipped
|
||||||
// past purge_corrupt_entities (finite but absurd values).
|
// past purge_corrupt_entities (finite but absurd values).
|
||||||
if is_unreasonable_extent(sub) {
|
if is_unreasonable_extent(sub) {
|
||||||
return None;
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the RAW colour. `Batches::finalize` applies `adapt_to_bg`
|
// Store the RAW colour. `Batches::finalize` applies `adapt_to_bg`
|
||||||
@@ -369,42 +375,12 @@ fn tessellate_sub_local(
|
|||||||
// Pass `local_offset` as the f64 world-offset so tessellate subtracts it
|
// Pass `local_offset` as the f64 world-offset so tessellate subtracts it
|
||||||
// before casting to f32 — same precision-preservation trick used for
|
// before casting to f32 — same precision-preservation trick used for
|
||||||
// top-level entities, applied per-defn.
|
// top-level entities, applied per-defn.
|
||||||
//
|
let wires_out = tessellate::tessellate(
|
||||||
// tessellate() may emit multiple WireModels for a single sub-entity (e.g.
|
|
||||||
// MTEXT with inline `\C` / `\c` colour switches splits one entity into
|
|
||||||
// one wire per colour group). The block-defn cache stores a single
|
|
||||||
// LocalWire per sub-entity, so we fold the wires into one — points get
|
|
||||||
// concatenated with NaN separators, the primary colour comes from the
|
|
||||||
// first wire. Per-segment colour is lost inside cached block defns; for
|
|
||||||
// top-level MTEXT outside blocks the colour split is preserved via the
|
|
||||||
// hot path in `tessellate_entity`.
|
|
||||||
let mut wires_out = tessellate::tessellate(
|
|
||||||
doc, h, sub, false, sub_color, pat_len, pat, lw_px, anno_scale, None,
|
doc, h, sub, false, sub_color, pat_len, pat, lw_px, anno_scale, None,
|
||||||
);
|
);
|
||||||
if wires_out.is_empty() {
|
if wires_out.is_empty() {
|
||||||
return None;
|
return vec![];
|
||||||
}
|
}
|
||||||
let mut wire = wires_out.remove(0);
|
|
||||||
for extra in wires_out {
|
|
||||||
if !wire.points.is_empty() && !extra.points.is_empty() {
|
|
||||||
wire.points.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
||||||
}
|
|
||||||
wire.points.extend(extra.points);
|
|
||||||
wire.key_vertices.extend(extra.key_vertices);
|
|
||||||
wire.snap_pts.extend(extra.snap_pts);
|
|
||||||
wire.tangent_geoms.extend(extra.tangent_geoms);
|
|
||||||
wire.fill_tris.extend(extra.fill_tris);
|
|
||||||
}
|
|
||||||
|
|
||||||
if wire.points.len() > 100_000 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Geometry is stored absolute; the double-single (high/low) render path
|
|
||||||
// keeps it precise at UTM scale, so no per-defn offset is subtracted.
|
|
||||||
let aabb_local = aabb_from_points_iter(
|
|
||||||
wire.points.iter().copied().chain(wire.fill_tris.iter().copied()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let text_height_local: Option<f32> = match sub {
|
let text_height_local: Option<f32> = match sub {
|
||||||
EntityType::Text(t) => Some((t.height * anno_scale as f64) as f32),
|
EntityType::Text(t) => Some((t.height * anno_scale as f64) as f32),
|
||||||
@@ -427,26 +403,46 @@ fn tessellate_sub_local(
|
|||||||
let text_obb_local: Option<[[f64; 3]; 4]> =
|
let text_obb_local: Option<[[f64; 3]; 4]> =
|
||||||
crate::entities::text_support::text_obb_corners_native(sub, anno_scale, mtext_lines);
|
crate::entities::text_support::text_obb_corners_native(sub, anno_scale, mtext_lines);
|
||||||
|
|
||||||
Some(LocalWire {
|
let mut result = Vec::with_capacity(wires_out.len());
|
||||||
points: wire.points,
|
for wire in wires_out {
|
||||||
points_low: wire.points_low,
|
// Per-wire point-count cap: a single wire that exceeds this is skipped
|
||||||
key_vertices: wire.key_vertices,
|
// rather than aborting the whole sub-entity — with per-wire separation,
|
||||||
snap_pts: wire.snap_pts,
|
// other colour segments from the same entity still render.
|
||||||
tangent_geoms: wire.tangent_geoms,
|
if wire.points.len() > 100_000 {
|
||||||
fill_tris: wire.fill_tris,
|
continue;
|
||||||
color: sub_color,
|
}
|
||||||
aci,
|
|
||||||
pattern_length: pat_len,
|
// Geometry is stored absolute; the double-single (high/low) render path
|
||||||
pattern: pat,
|
// keeps it precise at UTM scale, so no per-defn offset is subtracted.
|
||||||
line_weight_px: lw_px,
|
let aabb_local = aabb_from_points_iter(
|
||||||
plinegen: wire.plinegen,
|
wire.points.iter().copied().chain(wire.fill_tris.iter().copied()),
|
||||||
color_is_byblock,
|
);
|
||||||
lt_is_byblock,
|
let is_fill_only = wire.points.is_empty() && !wire.fill_tris.is_empty();
|
||||||
lw_is_byblock,
|
|
||||||
aabb_local,
|
result.push(LocalWire {
|
||||||
text_height_local,
|
points: wire.points,
|
||||||
text_obb_local,
|
points_low: wire.points_low,
|
||||||
})
|
key_vertices: wire.key_vertices,
|
||||||
|
snap_pts: wire.snap_pts,
|
||||||
|
tangent_geoms: wire.tangent_geoms,
|
||||||
|
fill_tris: wire.fill_tris,
|
||||||
|
fill_tris_low: wire.fill_tris_low,
|
||||||
|
color: wire.color,
|
||||||
|
aci,
|
||||||
|
pattern_length: pat_len,
|
||||||
|
pattern: pat,
|
||||||
|
line_weight_px: lw_px,
|
||||||
|
plinegen: wire.plinegen,
|
||||||
|
is_fill_only,
|
||||||
|
color_is_byblock,
|
||||||
|
lt_is_byblock,
|
||||||
|
lw_is_byblock,
|
||||||
|
aabb_local,
|
||||||
|
text_height_local,
|
||||||
|
text_obb_local,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn aabb_from_points_iter<I: IntoIterator<Item = [f32; 3]>>(pts: I) -> [f32; 4] {
|
fn aabb_from_points_iter<I: IntoIterator<Item = [f32; 3]>>(pts: I) -> [f32; 4] {
|
||||||
@@ -1118,20 +1114,30 @@ fn expand_defn(
|
|||||||
// We apply the Insert's transform scale to the stored
|
// We apply the Insert's transform scale to the stored
|
||||||
// local glyph height to get the screen height.
|
// local glyph height to get the screen height.
|
||||||
if let Some(h_local) = lw.text_height_local {
|
if let Some(h_local) = lw.text_height_local {
|
||||||
let m = &accum_xform.matrix.m;
|
// Fill-only wires (outline stripped into a sibling
|
||||||
let sy = ((m[1][0] * m[1][0]
|
// LocalWire) must bypass the LOD ladder: both
|
||||||
+ m[1][1] * m[1][1]
|
// emit_text_baseline and emit_greeked_text generate
|
||||||
+ m[1][2] * m[1][2]) as f64)
|
// their own OBB geometry and completely ignore
|
||||||
.sqrt() as f32;
|
// lw.fill_tris, so routing a fill-only wire through
|
||||||
let h_world = h_local * sy;
|
// either path would produce an invisible double-draw.
|
||||||
let h_px = h_world / wpp;
|
// emit_wire handles fill_tris correctly at all zoom
|
||||||
if h_px < 1.0 {
|
// levels, so skip the LOD rungs for these wires.
|
||||||
emit_text_baseline(lw, accum_xform, ctx, out, wpp);
|
if !lw.is_fill_only {
|
||||||
continue;
|
let m = &accum_xform.matrix.m;
|
||||||
}
|
let sy = ((m[1][0] * m[1][0]
|
||||||
if h_px < 5.0 {
|
+ m[1][1] * m[1][1]
|
||||||
emit_greeked_text(lw, local, accum_xform, ctx, out);
|
+ m[1][2] * m[1][2]) as f64)
|
||||||
continue;
|
.sqrt() as f32;
|
||||||
|
let h_world = h_local * sy;
|
||||||
|
let h_px = h_world / wpp;
|
||||||
|
if h_px < 1.0 {
|
||||||
|
emit_text_baseline(lw, accum_xform, ctx, out, wpp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if h_px < 5.0 {
|
||||||
|
emit_greeked_text(lw, local, accum_xform, ctx, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1289,7 +1295,7 @@ fn emit_wire(
|
|||||||
final_lw_px,
|
final_lw_px,
|
||||||
lw.aci,
|
lw.aci,
|
||||||
lw.plinegen,
|
lw.plinegen,
|
||||||
false,
|
lw.is_fill_only,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the open batch for this style would exceed wgpu's per-buffer limit
|
// If the open batch for this style would exceed wgpu's per-buffer limit
|
||||||
@@ -1309,7 +1315,7 @@ fn emit_wire(
|
|||||||
final_lw_px,
|
final_lw_px,
|
||||||
lw.aci,
|
lw.aci,
|
||||||
lw.plinegen,
|
lw.plinegen,
|
||||||
false,
|
lw.is_fill_only,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1388,11 +1394,13 @@ fn emit_wire(
|
|||||||
.tangent_geoms
|
.tangent_geoms
|
||||||
.push(transform_tangent(tg, accum_xform));
|
.push(transform_tangent(tg, accum_xform));
|
||||||
}
|
}
|
||||||
for p in &lw.fill_tris {
|
debug_assert_eq!(lw.fill_tris.len(), lw.fill_tris_low.len());
|
||||||
|
for (idx, p) in lw.fill_tris.iter().enumerate() {
|
||||||
|
let pl = lw.fill_tris_low[idx];
|
||||||
let v = accum_xform.apply(Vector3::new(
|
let v = accum_xform.apply(Vector3::new(
|
||||||
p[0] as f64,
|
p[0] as f64 + pl[0] as f64,
|
||||||
p[1] as f64,
|
p[1] as f64 + pl[1] as f64,
|
||||||
p[2] as f64,
|
p[2] as f64 + pl[2] as f64,
|
||||||
));
|
));
|
||||||
let (hx, lx) = WireModel::split_ds(v.x);
|
let (hx, lx) = WireModel::split_ds(v.x);
|
||||||
let (hy, ly) = WireModel::split_ds(v.y);
|
let (hy, ly) = WireModel::split_ds(v.y);
|
||||||
|
|||||||
Reference in new issue
Block a user