feat(annotation): show all scales on highlight

This commit is contained in:
Hakan Seven 2026-08-02 14:37:12 +03:00
commit baf9b240a4
3 changed files with 238 additions and 3 deletions

View file

@ -1485,6 +1485,10 @@ pub struct Scene {
/// id — one slot would thrash between them every frame.
sdf_text_cache:
RefCell<HashMap<u64, std::sync::Arc<Vec<crate::scene::pipeline::text_gpu::TextVertex>>>>,
/// Extra scale representations for highlighted annotative entities. These
/// stay outside the resident wire set so rollover and selection never
/// invalidate normal geometry, picking, or snapping caches.
annotation_highlight_cache: RefCell<HashMap<u64, Arc<Vec<WireModel>>>>,
/// Per wire-source `(geometry_epoch, gathered text)` of the most recent
/// SDF-text build. When a new content id misses the cache but the journal
/// shows no text-bearing entity changed since that epoch, the same glyphs
@ -1915,6 +1919,7 @@ impl Scene {
}]),
active_model_tile: std::cell::Cell::new(0),
sdf_text_cache: RefCell::new(HashMap::default()),
annotation_highlight_cache: RefCell::new(HashMap::default()),
last_sdf_text: RefCell::new(HashMap::default()),
// One pane mapped to tile 0 — matches the single default tile above.
model_panes: iced::widget::pane_grid::State::new(0).0,

View file

@ -272,6 +272,7 @@ pub struct Pipeline {
pub cached_wipeout_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_image_source: Option<std::sync::Arc<Vec<ImageModel>>>,
pub cached_text_source: Option<std::sync::Arc<Vec<text_gpu::TextVertex>>>,
pub cached_annotation_highlight_source: Option<std::sync::Arc<Vec<WireModel>>>,
pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>,
pub cached_face3d_source: Option<std::sync::Arc<Vec<WireModel>>>,
pub cached_face3d_depth_source:
@ -1694,6 +1695,7 @@ impl Pipeline {
cached_wipeout_source: None,
cached_image_source: None,
cached_text_source: None,
cached_annotation_highlight_source: None,
cached_mesh_source: None,
cached_face3d_source: None,
cached_face3d_depth_source: None,
@ -1785,11 +1787,12 @@ impl Pipeline {
wires: &[WireModel],
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
hover: Option<acadrust::Handle>,
annotation_context_wires: &[WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
let hover = hover.filter(|h| !selected.contains(h));
if selected.is_empty() && hover.is_none() {
if selected.is_empty() && hover.is_none() && annotation_context_wires.is_empty() {
self.gpu_selected_wires = vec![];
return;
}
@ -1819,6 +1822,13 @@ impl Pipeline {
}
}
}
for wire in annotation_context_wires {
if wire.selected {
selected_wires.push(wire);
} else {
hover_wires.push(wire);
}
}
let mut gpu = WireGpu::from_highlight_refs(
device,
&selected_wires,
@ -1857,10 +1867,11 @@ impl Pipeline {
wires: &[WireModel],
selected: &rustc_hash::FxHashSet<acadrust::Handle>,
hover: Option<acadrust::Handle>,
annotation_context_wires: &[WireModel],
) {
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
let hover = hover.filter(|h| !selected.contains(h));
if selected.is_empty() && hover.is_none() {
if selected.is_empty() && hover.is_none() && annotation_context_wires.is_empty() {
self.text_highlight_vbuf = None;
self.text_highlight_vcount = 0;
return;
@ -1887,6 +1898,19 @@ impl Pipeline {
if let Some(h) = hover {
push(h.value(), WireModel::HOVER, wires, &mut out);
}
for wire in annotation_context_wires {
let tint = if wire.selected {
WireModel::SELECTED
} else {
WireModel::HOVER
};
for vertex in &wire.text_verts {
out.push(text_gpu::TextVertex {
color: [tint[0], tint[1], tint[2], vertex.color[3]],
..*vertex
});
}
}
self.text_highlight_vcount = out.len() as u32;
self.text_highlight_vbuf = text_gpu::upload_vertices(device, &out);
if let Some(started) = perf_started {

View file

@ -52,6 +52,9 @@ pub struct ViewportData {
/// the main `wires` buffer so a drag re-uploads only this small set each
/// frame, never the resident base buffer. Drawn on top in the wire pass.
pub(in crate::scene) preview_wires: Arc<Vec<WireModel>>,
/// Non-current scale representations of selected or hovered annotative
/// entities. Uploaded with the xray highlight instead of the resident set.
pub(in crate::scene) annotation_context_wires: Arc<Vec<WireModel>>,
/// One/few live hatch models for grip editing. Uploaded through a separate
/// tiny GPU batch so the resident hatch buffer remains untouched.
pub(in crate::scene) preview_hatches: Arc<Vec<HatchModel>>,
@ -802,12 +805,20 @@ impl shader::Primitive for Primitive {
let highlighted_geometry_changed = inner.cached_selection.0
!= vp.wire_content_id
&& !highlighted_geometry_unchanged;
if selection_changed || highlighted_geometry_changed {
let annotation_context_changed = inner
.cached_annotation_highlight_source
.as_ref()
.map_or(!vp.annotation_context_wires.is_empty(), |previous| {
!(previous.is_empty() && vp.annotation_context_wires.is_empty())
&& !Arc::ptr_eq(previous, &vp.annotation_context_wires)
});
if selection_changed || highlighted_geometry_changed || annotation_context_changed {
inner.upload_selected_wires(
device,
&vp_wires[..],
&vp.selected_handles,
vp.hover_handle,
&vp.annotation_context_wires,
&draw_depths,
);
// Text highlight rides the same selection key: a pick / rollover
@ -818,7 +829,10 @@ impl shader::Primitive for Primitive {
&vp_wires[..],
&vp.selected_handles,
vp.hover_handle,
&vp.annotation_context_wires,
);
inner.cached_annotation_highlight_source =
Some(Arc::clone(&vp.annotation_context_wires));
}
// Advance the content id even when an unrelated entity patch kept
// the existing overlay valid, so future deltas compare to the
@ -1886,6 +1900,192 @@ impl Scene {
verts
}
fn annotation_context_highlight_wires(
&self,
inst: &ViewportInstance,
) -> Arc<Vec<WireModel>> {
if self.selected.is_empty() && self.hover_highlight.is_none() {
return Arc::new(Vec::new());
}
let content_viewport = !inst.paper_sheet
&& inst.tile_idx.is_none()
&& inst.handle != Handle::NULL;
let target_block = if inst.paper_sheet {
self.current_layout_block_handle()
} else {
self.content_render_block_handle()
};
let annotation_scale_handle = if inst.paper_sheet {
self.paper_annotation_scale_handle()
} else if content_viewport {
self.viewport_scale_handle(inst.handle)
} else {
crate::scene::annotative::scale_handle_by_name(
&self.document,
&self.document.header.current_annotation_scale,
)
};
let annotation_scale = if inst.paper_sheet {
1.0
} else if content_viewport {
self.viewport_annotation_multiplier(inst.handle)
} else {
self.annotation_scale
};
let frozen: rustc_hash::FxHashSet<Handle> = if content_viewport {
match self.document.get_entity(inst.handle) {
Some(EntityType::Viewport(viewport)) => {
viewport.frozen_layers.iter().copied().collect()
}
_ => rustc_hash::FxHashSet::default(),
}
} else {
rustc_hash::FxHashSet::default()
};
let bg = if self.current_layout == "Model" {
self.bg_color
} else {
self.paper_bg_color
};
let all_visible = self.annotation_all_visible();
let mut key = 0xcbf2_9ce4_8422_2325_u64;
let mut mix = |value: u64| {
key = key.rotate_left(17) ^ value.wrapping_mul(0x9E37_79B9_7F4A_7C15)
};
mix(self.geometry_epoch);
mix(self.selection_generation);
mix(target_block.value());
mix(annotation_scale_handle.map_or(0, |handle| handle.value()));
mix(annotation_scale.to_bits() as u64);
mix(u64::from(content_viewport));
mix(u64::from(all_visible));
mix(self.active_viewport.map_or(0, |handle| handle.value()));
mix(crate::scene::text::sdf_atlas::generation());
for component in bg {
mix(component.to_bits() as u64);
}
let mut frozen_sig = frozen.len() as u64;
for handle in &frozen {
frozen_sig ^= handle
.value()
.wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
mix(frozen_sig);
if let Some(wires) = self.annotation_highlight_cache.borrow().get(&key) {
return Arc::clone(wires);
}
let mut highlighted: Vec<(Handle, bool)> = self
.selected
.iter()
.copied()
.map(|handle| (handle, true))
.collect();
if let Some(handle) = self
.hover_highlight
.filter(|handle| !self.selected.contains(handle))
{
highlighted.push((handle, false));
}
highlighted.sort_unstable_by_key(|(handle, _)| handle.value());
let empty_selection = rustc_hash::FxHashSet::default();
let mut wires = Vec::new();
for (handle, selected) in highlighted {
let Some(entity) = self.document.get_entity(handle) else {
continue;
};
if !crate::scene::annotative::is_annotative(&self.document, entity)
|| !self.resident_entity_visible(
entity,
target_block,
Some(&frozen),
annotation_scale_handle,
true,
)
{
continue;
}
let mut scales: Vec<Handle> = crate::scene::annotative::object_scale_memberships(
&self.document,
handle,
)
.into_iter()
.map(|(_, scale)| scale)
.collect();
scales.sort_unstable_by_key(Handle::value);
scales.dedup();
let base_visible = !crate::scene::annotative::annotative_offscale_for(
&self.document,
entity.common(),
annotation_scale_handle,
all_visible,
);
let displayed_scale = base_visible
.then(|| {
crate::scene::annotative::active_object_context_for_scale(
&self.document,
handle,
annotation_scale_handle,
)
.map(|context| context.scale)
})
.flatten();
let tint = if selected {
WireModel::SELECTED
} else {
WireModel::HOVER
};
for scale in scales {
if displayed_scale == Some(scale) {
continue;
}
let context_scale = match self.document.objects.get(&scale) {
Some(acadrust::objects::ObjectType::Scale(value)) => {
value.inverse_factor() as f32
}
_ => annotation_scale,
};
let block_cache = self.block_cache_arc_for(Some(scale), true);
let mut context_wires = crate::scene::tessellate_entity(
&self.document,
&empty_selection,
self.active_viewport,
bg,
context_scale,
Some(scale),
entity,
Some(&block_cache),
None,
None,
content_viewport,
);
for wire in &mut context_wires {
wire.color = tint;
wire.selected = selected;
for vertex in &mut wire.text_verts {
vertex.color = [tint[0], tint[1], tint[2], vertex.color[3]];
}
}
wires.extend(context_wires);
}
}
let wires = Arc::new(wires);
let mut cache = self.annotation_highlight_cache.borrow_mut();
if cache.len() > 16 {
cache.clear();
}
cache.insert(key, Arc::clone(&wires));
wires
}
/// Build the unified multi-viewport `Primitive` for the current layout.
/// Model layout → one full-window viewport (more once tiled); paper
/// layout → one viewport per floating content viewport. Each entry is
@ -2143,6 +2343,11 @@ impl Scene {
} else {
inst.paper_sheet
};
let annotation_context_wires = if show_live_overlay {
self.annotation_context_highlight_wires(inst)
} else {
Arc::new(Vec::new())
};
let preview_wires = if !show_live_overlay
|| (self.interim_wire.is_none() && self.preview_wires.is_empty())
{
@ -2346,6 +2551,7 @@ impl Scene {
wires: Arc::downgrade(&all_wires),
clip_boundary_ndc,
preview_wires,
annotation_context_wires,
preview_hatches,
face3d_wires,
text_verts,