forked from 1b-agent/opencadstudio-upstream
perf: let one tessellation serve every background
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`, 2034 ms — to arrive at geometry identical to what was already in the memo. The only thing that differed was colour. `local_wires_for` already knows this: it stores the raw colour and drops its background argument with `let _ = bg_color;`, noting that `Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer has to rebuild on BACKGROUND / layout-switch". The tessellation memo's guard had not been told, and still mixed the background in, so every entry was discarded on a switch. `finalize` now records what its colour resolution consumed — the raw colour, the raw glyph colours, and the three flags it branches on — as `WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another background. The guard drops the background, so a memo entry serves any layout. The resolution recomputes from the recorded raw colour rather than from the resolved one, which makes it idempotent and therefore safe over a set that mixes memo hits with freshly tessellated misses. Re-adapting the resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour to pure black on a light background, and pure black comes back pure *white* on a dark one, losing the tint. A test pins that asymmetry so the raw colour is not optimised away later. The contract is tested against the real `finalize` rather than a copy of its logic: for every background pair, building under one and resolving for the other lands exactly where building under the other would have. `bg_adapt` is `None` for wires built anywhere else, and the resolution skips them, so nothing outside the block cache changes colour. It is also `None` when the resolution would be a no-op — a preserved colour with no canvas fill looks the same under every background — which keeps the common case at no cost. Ordering is unchanged: the pass runs inside the builder, before `append_scene_markers` and `apply_refedit_fade`, both of which have their own separate background dependency. `wires-build` gains `colors=` so the pass has a number of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e4f755e6f
commit
391a74a615
40 files changed
+308
-9
No files matched your search
Vendored
+136
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -1939,12 +1939,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
|
||||
@@ -9013,10 +9015,11 @@ impl Scene {
|
||||
let memo_misses;
|
||||
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;
|
||||
@@ -9029,9 +9032,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
|
||||
@@ -9133,6 +9140,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;
|
||||
@@ -9157,13 +9172,14 @@ 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={}",
|
||||
crate::perf::elapsed_ms(t_fn),
|
||||
sort_cache_ms,
|
||||
visible_ms,
|
||||
blk_ms,
|
||||
colors_ms,
|
||||
build_ms,
|
||||
classify_ms,
|
||||
hits_ms,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -3145,6 +3145,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 {
|
||||
|
||||
Reference in new issue
Block a user