Merge pull request #1014: perf: let one tessellation serve every background

This commit is contained in:
Hakan Seven committed 2026-09-05 19:28:34 +03:00
commit 1a4b389b0e
40 files changed
+308 -9

No files matched your search

+1
View File
@@ -2407,6 +2407,7 @@ impl OpenCADStudio {
let far_pos = base + dir * far;
let far_neg = base - dir * far;
let guide = crate::scene::WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+7
View File
@@ -3882,6 +3882,7 @@ fn tessellate_dimension_inner(
let (ext1, ext2) = split_ext_lines(&geom.ext_lines);
if !ext1.is_empty() {
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -3917,6 +3918,7 @@ fn tessellate_dimension_inner(
}
if !ext2.is_empty() {
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -3952,6 +3954,7 @@ fn tessellate_dimension_inner(
}
} else {
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -3988,6 +3991,7 @@ fn tessellate_dimension_inner(
}
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -4032,6 +4036,7 @@ fn tessellate_dimension_inner(
let mut points = Vec::new();
add_polyline(&mut points, &symbol);
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -4080,6 +4085,7 @@ fn tessellate_dimension_inner(
aci_to_rgba(&c)
};
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -4130,6 +4136,7 @@ fn tessellate_dimension_inner(
let p3 = rect[2];
let p4 = rect[5];
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+2
View File
@@ -672,6 +672,7 @@ impl LeaderTess for Leader {
if verts.len() < 2 {
return WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -852,6 +853,7 @@ impl LeaderTess for Leader {
crate::scene::convert::tessellate::points_to_ds(fill_tris);
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+5
View File
@@ -2068,6 +2068,7 @@ impl MultiLeaderTess for MultiLeader {
// WireModels so the renderer respects per-piece coloring.
let mut wires: Vec<WireModel> = Vec::new();
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -2367,6 +2368,7 @@ impl MultiLeaderTess for MultiLeader {
xy = xy.max(p[1] as f64);
}
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -2441,6 +2443,7 @@ impl MultiLeaderTess for MultiLeader {
pts.push([bx, by, z]);
}
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -2530,6 +2533,7 @@ impl MultiLeaderTess for MultiLeader {
wcs_corners[3],
];
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -2574,6 +2578,7 @@ impl MultiLeaderTess for MultiLeader {
wcs_corners[0],
];
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -1990,6 +1990,7 @@ pub fn tessellate_table(
let mk =
|color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+2
View File
@@ -286,6 +286,7 @@ impl CadCommand for AlignedDimensionCommand {
let p1 = p1.as_vec3();
let p2 = p2.as_vec3();
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -348,6 +349,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel {
let half_height = 0.16;
let to_point = |point: DVec3| point.as_vec3().to_array();
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -1132,6 +1132,7 @@ fn nan() -> DVec3 {
fn preview_wire(points: Vec<DVec3>) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -63,6 +63,7 @@ impl DataLinkPlaceCommand {
);
}
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -272,6 +272,7 @@ fn dvec(point: Vector3) -> DVec3 {
fn preview_line(far_chord: Vec3, chord: Vec3, text: Vec3) -> WireModel {
let separator = [f32::NAN; 3];
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -709,6 +709,7 @@ fn preview_for_dimension(dimension: &Dimension) -> WireModel {
_ => {}
}
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -86,6 +86,7 @@ impl CadCommand for DimJogLineCommand {
}
let d = 0.3_f32;
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -103,6 +103,7 @@ impl CadCommand for DimTeditCommand {
}
let d = 0.2_f32;
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -442,6 +442,7 @@ fn dvec(point: Vector3) -> DVec3 {
fn preview_wire(points: Vec<Vec3>) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -300,6 +300,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel {
points.push([w2.x, w2.y, w2.z]);
}
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -467,6 +467,7 @@ fn ocs_point(point: [f64; 2], elevation: f64, normal: Vector3) -> DVec3 {
fn preview_wire(points: Vec<DVec3>) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -764,6 +764,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel {
points.push([w2.x, w2.y, w2.z]);
}
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -490,6 +490,7 @@ impl CadCommand for MLeaderCollectCommand {
fn preview_wire(pts: &[Vec3]) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -270,6 +270,7 @@ fn is_x_type(feature: DVec3, leader: DVec3) -> bool {
fn preview_wire(points: Vec<Vec3>) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -277,6 +277,7 @@ fn dvec(point: Vector3) -> DVec3 {
fn preview_wire(points: Vec<Vec3>) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -212,6 +212,7 @@ impl TableCommand {
);
}
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -81,6 +81,7 @@ impl CadCommand for ToleranceCommand {
}));
}
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -199,6 +199,7 @@ impl CadCommand for AttdefCommand {
pt + self.plane.y * d,
];
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -180,6 +180,7 @@ impl MlineCommand {
let (fill_tris, fill_tris_low) =
crate::scene::convert::tessellate::points_to_ds(fill_tris);
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -123,6 +123,7 @@ impl CadCommand for ImageCommand {
let [p0, p1, p2, p3] = corners;
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+2
View File
@@ -85,6 +85,7 @@ impl CadCommand for RayCommand {
let dir = (pt - base).normalize_or_zero();
let far = base + dir * DISPLAY_EXTENT;
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -189,6 +190,7 @@ impl CadCommand for XLineCommand {
let far_pos = base + dir * DISPLAY_EXTENT;
let far_neg = base - dir * DISPLAY_EXTENT;
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -365,6 +365,7 @@ impl CadCommand for AreaCommand {
points.push(to_render(point));
points.push(to_render(self.points[0]));
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -86,6 +86,7 @@ impl CadCommand for DistCommand {
let p1 = self.first?;
// The preview wire is purely visual, so f32 vertices are fine here.
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -70,6 +70,7 @@ impl MeasureGeomCommand {
/// Build a cyan preview wire connecting the picked points and the cursor.
fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -3746,6 +3746,7 @@ fn collect_runs(pts: &[[f64; 2]], take: &dyn Fn([f64; 2]) -> bool, out: &mut Vec
/// Build a preview wire from a NaN-break point list.
fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -3621,6 +3621,7 @@ inventory::submit!(crate::command::CommandRegistration {
fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -54,6 +54,7 @@ impl CadCommand for PlotWindowCommand {
let p1 = self.p1?.as_vec3();
// Draw the selection rectangle.
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+1
View File
@@ -136,6 +136,7 @@ impl CadCommand for ZoomWindowCommand {
let max = p1.max(pt);
// Draw a rectangle preview
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+136
View File
@@ -1591,6 +1591,25 @@ impl Batches {
[b.min_x, b.min_y, b.max_x, b.max_y]
};
let contrast_bg = b.contrast_bg.unwrap_or(bg_color);
// Record what this resolution consumed, so switching layout can
// redo it for another background instead of re-running the
// tessellation that produced identical geometry. Skipped when
// the resolution is a no-op — a preserved colour with no canvas
// fill looks the same under every background.
let bg_adapt: crate::scene::model::wire_model::BgAdapt =
(b.canvas_color || !b.preserve_color).then(|| {
Box::new(crate::scene::model::wire_model::BgAdaptInputs {
raw_color: b.color,
text_raw_colors: if b.preserve_color {
Vec::new()
} else {
b.text_verts.iter().map(|v| v.color).collect()
},
contrast_bg: b.contrast_bg,
canvas_color: b.canvas_color,
preserve_color: b.preserve_color,
})
});
let color = if b.canvas_color {
bg_color
} else if b.preserve_color {
@@ -1615,6 +1634,7 @@ impl Batches {
);
}
WireModel {
bg_adapt,
point_marker: b.point_marker,
taper_widths: Vec::new(),
pattern_stations: b.pattern_stations,
@@ -2728,3 +2748,119 @@ fn is_unreasonable_extent(e: &EntityType) -> bool {
_ => false,
}
}
#[cfg(test)]
mod bg_resolution_tests {
use super::*;
use crate::scene::pipeline::text_gpu::TextVertex;
use crate::scene::view::render::{adapt_to_bg, resolve_colors_for_bg};
const DARK: [f32; 4] = [0.1, 0.1, 0.1, 1.0];
const LIGHT: [f32; 4] = [0.95, 0.95, 0.95, 1.0];
/// Near-white, not pure white: the case that makes `adapt_to_bg`
/// non-re-appliable, so it must survive a round trip through the recorded
/// raw colour.
const NEAR_WHITE: [f32; 4] = [0.97, 0.99, 0.96, 1.0];
fn entry(
color: [f32; 4],
contrast_bg: Option<[f32; 4]>,
preserve_color: bool,
canvas_color: bool,
) -> BatchEntry {
let mut e = BatchEntry::new(
color,
contrast_bg,
preserve_color,
canvas_color,
0.0,
[0.0; 8],
1.0,
0.0,
0.0,
None,
1,
false,
false,
false,
false,
true,
false,
);
e.points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
e.text_verts = vec![TextVertex {
pos: [0.0; 3],
pos_low: [0.0; 3],
uv: [0.0; 2],
color,
draw_depth: 0.0,
}];
e
}
/// Every combination `finalize` distinguishes, in one set.
fn batches() -> Batches {
Batches {
by_style: HashMap::default(),
closed: vec![
entry(NEAR_WHITE, None, false, false),
entry([0.0, 0.0, 0.0, 1.0], None, false, false),
entry(NEAR_WHITE, None, true, false),
entry(NEAR_WHITE, Some(LIGHT), false, false),
entry(NEAR_WHITE, None, false, true),
],
extra_wires: Vec::new(),
nested_prototypes: HashMap::default(),
}
}
fn colors(wires: &[WireModel]) -> Vec<([f32; 4], Vec<[f32; 4]>)> {
wires
.iter()
.map(|w| (w.color, w.text_verts.iter().map(|v| v.color).collect()))
.collect()
}
/// The contract the whole background-key removal rests on: resolving a set
/// built under one background for another must land exactly where building
/// it under that background would have.
#[test]
fn resolving_for_a_background_matches_building_under_it() {
for (from, to) in [(DARK, LIGHT), (LIGHT, DARK), (DARK, DARK), (LIGHT, LIGHT)] {
let mut resolved = batches().finalize("b", false, from);
resolve_colors_for_bg(&mut resolved, to);
let direct = batches().finalize("b", false, to);
assert_eq!(
colors(&resolved),
colors(&direct),
"built under {from:?}, resolved for {to:?}",
);
}
}
/// Safe to apply twice, and over a set mixing already-resolved wires with
/// freshly built ones — which is exactly what a memo hit plus a miss is.
#[test]
fn resolving_twice_changes_nothing() {
let mut once = batches().finalize("b", false, DARK);
resolve_colors_for_bg(&mut once, LIGHT);
let mut twice = once.clone();
resolve_colors_for_bg(&mut twice, LIGHT);
assert_eq!(colors(&once), colors(&twice));
}
/// Why the raw colour is stored instead of re-adapting the resolved one.
/// If this ever starts passing, `raw_color` could be dropped — and if it
/// is removed while this still fails, near-white geometry loses its tint
/// on the first layout switch.
#[test]
fn adapt_to_bg_cannot_be_reapplied() {
let once = adapt_to_bg(NEAR_WHITE, LIGHT);
assert_eq!(once, [0.0, 0.0, 0.0, 1.0], "near-white snaps to pure black");
assert_ne!(
adapt_to_bg(once, DARK),
adapt_to_bg(NEAR_WHITE, DARK),
"re-adapting the resolved colour must not equal adapting the raw one",
);
}
}
+4
View File
@@ -923,6 +923,7 @@ fn tessellate_entity_inner(
Vec::new()
};
return vec![WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1167,6 +1168,7 @@ fn tessellate_entity_inner(
(ins.insert_point.z) as f32,
);
let marker = WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1510,6 +1512,7 @@ fn lod_stub_wire(
// LOD boundary. #19.
let stored_color = if selected { WireModel::SELECTED } else { color };
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1605,6 +1608,7 @@ fn lod_stub_wire_3d(
}
let stored_color = if selected { WireModel::SELECTED } else { color };
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+19
View File
@@ -220,6 +220,7 @@ fn point_cloud_wires(
};
let (points, points_low) = points_to_ds(body_points);
let mut wires = vec![WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -474,6 +475,7 @@ pub fn tessellate(
}
};
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -637,6 +639,7 @@ pub fn tessellate(
None
};
elem_wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1026,6 +1029,7 @@ pub fn tessellate(
}
}
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1082,6 +1086,7 @@ pub fn tessellate(
}
}
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1135,6 +1140,7 @@ pub fn tessellate(
low.push(ll);
}
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1169,6 +1175,7 @@ pub fn tessellate(
});
}
wires.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1233,6 +1240,7 @@ pub fn tessellate(
(Vec::new(), Vec::new(), Vec::new())
};
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1279,6 +1287,7 @@ pub fn tessellate(
(Vec::new(), Vec::new(), Vec::new())
};
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1322,6 +1331,7 @@ pub fn tessellate(
// are empty → the early-return path above).
if !sdf_verts.is_empty() {
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1358,6 +1368,7 @@ pub fn tessellate(
if out.is_empty() {
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1428,6 +1439,7 @@ pub fn tessellate(
.map(|[kx, ky, kz]| [kx, ky, kz])
.collect();
return vec![WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1556,6 +1568,7 @@ pub fn tessellate(
let point_marker =
crate::entities::point::relative_marker_spec(entity, document);
out.push(WireModel {
bg_adapt: None,
point_marker,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1601,6 +1614,7 @@ pub fn tessellate(
(Vec::new(), Vec::new(), Vec::new())
};
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1637,6 +1651,7 @@ pub fn tessellate(
if out.is_empty() {
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1695,6 +1710,7 @@ pub fn tessellate(
&station_pieces,
);
return vec![WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: station_data,
@@ -1741,6 +1757,7 @@ pub fn tessellate(
// treatment as the Contour arm, restarting the dash per segment.
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
return vec![WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -1791,6 +1808,7 @@ pub fn tessellate(
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
let world_width = widths.iter().copied().fold(0.0f32, f32::max);
return vec![WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: widths,
pattern_stations: Vec::new(),
@@ -1899,6 +1917,7 @@ pub fn tessellate(
_ => (Vec::new(), Vec::new()),
};
vec![WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+25 -9
View File
@@ -1961,12 +1961,14 @@ pub struct Scene {
/// Per-entity memo for the **resident** model wire set (`model_tile_wires_arc`,
/// the one the main GPU render holds). Kept separate from `tess_memo` because
/// the resident set is camera-INDEPENDENT (no view cull, no zoom LOD), so its
/// guard depends only on anno-scale / background — it survives pan/zoom, and a
/// guard depends only on anno-scale — it survives pan/zoom and layout switches, and a
/// single-entity edit re-tessellates just the changed entity instead of the
/// whole model. Sharing `tess_memo` would let the camera-dependent culled path
/// thrash it on every zoom. (#perf)
resident_tess_memo: RefCell<HashMap<Handle, Arc<Vec<WireModel>>>>,
/// Guard hash for `resident_tess_memo` (anno-scale / bg only).
/// Guard hash for `resident_tess_memo` (anno-scale only). The background
/// is not part of it: it changes colours, not geometry, and
/// `resolve_colors_for_bg` reapplies it to the assembled set.
resident_tess_guard: std::cell::Cell<u64>,
/// Per-mutation delta journal: which handles changed on each `geometry_epoch`
/// bump. Bounded ring ([`GEOMETRY_JOURNAL_CAP`]); a derived cache replays the
@@ -9087,10 +9089,11 @@ impl Scene {
let mut guard_was_stale = false;
let t_build = perf.then(iced::time::Instant::now);
let mut wires: Vec<WireModel> = if memo_active {
// Guard hash of everything tessellate_entity output depends on
// Guard hash of everything tessellate_entity's GEOMETRY depends on
// besides the entity itself. A mismatch (zoom/tol, view, anno,
// offset, bg, entered viewport) means the memo is stale. For the
// resident path wpp/view_aabb are None, so this collapses to anno/bg.
// offset, entered viewport) means the memo is stale. For the
// resident path wpp/view_aabb are None, so this collapses to anno.
// The background is excluded on purpose — see the note below.
let guard = {
let mut g: u64 = 0xcbf2_9ce4_8422_2325;
let mut mix = |x: u64| g = g.rotate_left(13) ^ x;
@@ -9103,9 +9106,13 @@ impl Scene {
mix(anno.to_bits() as u64);
mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0));
mix(all_visible as u64);
for c in bg {
mix(c.to_bits() as u64);
}
// The background is deliberately NOT mixed in. It changes
// colours, never geometry — `local_wires_for` discards it with
// `let _ = bg_color;` and `Batches::finalize` applies it last —
// so a memo entry built under one background serves any other
// once `resolve_colors_for_bg` has run over the assembled set
// below. Keying on it made a layout switch re-tessellate the
// whole model to arrive at identical points.
mix(avp.map(|h| h.value()).unwrap_or(0));
// SDF glyph quads bake the atlas UV of each tile, so a growth or
// a re-bake (which rescale / rewind every UV) makes memoized text
@@ -9209,6 +9216,14 @@ impl Scene {
};
let build_ms = crate::perf::elapsed_ms(t_build);
// Resolve display colours for the live background. Memo hits were
// built under whatever background was current then, misses under this
// one; the pass recomputes both from their recorded raw colour, so a
// mixed set lands in the same place either way.
let t_colors = perf.then(iced::time::Instant::now);
crate::scene::view::render::resolve_colors_for_bg(&mut wires, bg);
let colors_ms = crate::perf::elapsed_ms(t_colors);
// Apply draw order via the cached index (O(1) block lookup).
let t_sort = perf.then(iced::time::Instant::now);
let mut sorted = false;
@@ -9233,7 +9248,7 @@ impl Scene {
}
if perf {
crate::perf_record!(
"[perf] wires-build total={:.1}ms sort_cache={:.1} visible={:.1} blk_cache={:.1} \
"[perf] wires-build total={:.1}ms sort_cache={:.1} visible={:.1} blk_cache={:.1} colors={:.1} \
build={:.1} [classify={:.1} hits={:.1} tess={:.1} materialize={:.1}] sort={:.1}({}) \
entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates={} \
guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
@@ -9241,6 +9256,7 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
sort_cache_ms,
visible_ms,
blk_ms,
colors_ms,
build_ms,
classify_ms,
hits_ms,
+39
View File
@@ -140,6 +140,40 @@ impl PointMarker {
///
/// Linetype is encoded as a GPU-side dash pattern so the CPU never needs to
/// split wires into per-dash segments. `pattern_length = 0.0` means solid.
/// Everything needed to resolve a wire's display colour against a background,
/// kept so the resolution can be redone for another background without
/// re-tessellating.
///
/// `None` — the default — means the wire did not record its inputs and a
/// resolution pass must leave it alone. Only `Batches::finalize` fills it in.
pub type BgAdapt = Option<Box<BgAdaptInputs>>;
/// The inputs `Batches::finalize` consumes when it resolves a colour.
///
/// The flags are independent — a wire can paint the background itself while
/// its glyphs still adapt — so this mirrors that code's branches rather than
/// collapsing them into one enum.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BgAdaptInputs {
/// Colour before adaptation. Resolution always recomputes from here rather
/// than from `WireModel::color`, so it is idempotent and safe over a set
/// mixing memo hits with freshly tessellated wires. `adapt_to_bg` is
/// **not** re-appliable: a near-white colour snaps to pure black on a
/// light background, and pure black comes back pure *white* on a dark one,
/// losing the tint.
pub raw_color: [f32; 4],
/// Glyph colours before adaptation, parallel to `WireModel::text_verts`.
/// Empty when the wire has no glyphs or their colour never adapts.
pub text_raw_colors: Vec<[f32; 4]>,
/// Adapt against this instead of the live background: an MTEXT carrying
/// its own background fill.
pub contrast_bg: Option<[f32; 4]>,
/// The wire paints the background; its colour *is* the live background.
pub canvas_color: bool,
/// Authored colour. The background must not touch it, glyphs included.
pub preserve_color: bool,
}
#[derive(Clone, Debug)]
pub struct WireModel {
/// Unique identifier — the handle value as a decimal string.
@@ -156,6 +190,9 @@ pub struct WireModel {
pub point_marker: Option<PointMarker>,
/// RGBA colour in [0, 1].
pub color: [f32; 4],
/// How `color` and `text_verts` follow the background, or `None` when this
/// wire's colour must not be re-resolved.
pub bg_adapt: BgAdapt,
/// Whether this wire is currently selected.
#[allow(dead_code)]
pub selected: bool,
@@ -315,6 +352,7 @@ impl WireModel {
/// Create a solid wire (no dash pattern, 1px weight).
pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self {
Self {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -688,6 +726,7 @@ impl Default for WireModel {
points: Vec::new(),
points_low: Vec::new(),
color: Self::WHITE,
bg_adapt: None,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
+1
View File
@@ -296,6 +296,7 @@ pub fn frame_wire(
lo.push([lx, ly, 0.0]);
}
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+2
View File
@@ -309,6 +309,7 @@ pub fn apply_along(
.into_iter()
.filter(|s| s.len() >= 2)
.map(|pts| WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
@@ -357,6 +358,7 @@ pub fn apply_along(
xy = xy.max(y);
}
out.push(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
+37
View File
@@ -3222,6 +3222,43 @@ pub(crate) fn adapt_to_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
}
}
/// Re-resolve display colours for `bg`, for wires that recorded what their
/// resolution consumed.
///
/// This is what lets a set tessellated under one background be shown under
/// another without rebuilding it: the geometry never depended on the
/// background, only the colours did — see the `let _ = bg_color;` in
/// `block_cache::local_wires_for`.
///
/// Every colour is recomputed from `raw_color`, never from the current
/// `color`, so the pass is idempotent and safe over a set that mixes memoized
/// wires with freshly tessellated ones. Recomputing from `color` would not be:
/// `adapt_to_bg` maps a near-white colour to pure black, and pure black back
/// to pure *white*, so a second application loses the original tint.
///
/// Wires with `bg_adapt: None` — everything not built by `Batches::finalize` —
/// are left alone.
pub(crate) fn resolve_colors_for_bg(wires: &mut [WireModel], bg: [f32; 4]) {
for wire in wires {
let Some(adapt) = wire.bg_adapt.as_deref() else {
continue;
};
let contrast = adapt.contrast_bg.unwrap_or(bg);
wire.color = if adapt.canvas_color {
bg
} else if adapt.preserve_color {
adapt.raw_color
} else {
adapt_to_bg(adapt.raw_color, contrast)
};
if !adapt.preserve_color {
for (vertex, raw) in wire.text_verts.iter_mut().zip(&adapt.text_raw_colors) {
vertex.color = adapt_to_bg(*raw, contrast);
}
}
}
}
// ── Primitive builder helpers (called by ViewportPane's shader::Program impl) ──
impl Scene {