fix(dimarc): complete merge integration

Preserve source-plane geometry and associative arc parameters while combining the latest properties and tolerance workflows.
This commit is contained in:
Hakan Seven committed 2026-08-27 01:00:23 +03:00
commit 6ef2d75e84
67 files changed
+4773 -883

No files matched your search

+9
View File
@@ -24,6 +24,7 @@ pub struct TextStroke {
pub origin: [f64; 2],
pub color: Option<[f32; 3]>,
pub fill_tris: Vec<[f32; 2]>,
pub plane: Option<TextPlane>,
/// Layout inputs to rebuild this run as per-glyph SDF quads (see
/// `scene::text::glyph_quads`). `Some` on runs wired for the SDF text
/// renderer; `None` leaves the run to the stroke path only. Heights are
@@ -32,6 +33,14 @@ pub struct TextStroke {
pub run: Option<GlyphRun>,
}
#[derive(Clone, Copy, Debug)]
pub struct TextPlane {
pub origin: [f64; 3],
pub scale_origin: [f64; 3],
pub x_axis: [f64; 3],
pub y_axis: [f64; 3],
}
/// Per-run text-layout inputs needed to reproduce a run as SDF glyph quads.
#[derive(Clone, Debug)]
pub struct GlyphRun {
+1
View File
@@ -416,6 +416,7 @@ fn tessellate_entity_inner(
| EntityType::MText(_)
| EntityType::Dimension(_)
| EntityType::MultiLeader(_)
| EntityType::Tolerance(_)
) {
crate::scene::annotative::effective_annotation_scale_for(
document,
+197 -37
View File
@@ -78,6 +78,81 @@ fn oriented_text_corners(
]
}
fn oriented_mtext_corner_groups(
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
text: &acadrust::MText,
rotation: f64,
pad: f64,
annotation_scale: f64,
) -> Vec<[[f64; 2]; 4]> {
let columns = &text.column_data;
let count = crate::entities::text_support::clamp_mtext_column_count(columns.column_count)
as usize;
if columns.column_type == 0 || count <= 1 || columns.width <= 0.0 {
return vec![oriented_text_corners(
verts,
[text.insertion_point.x, text.insertion_point.y],
rotation,
pad,
)];
}
let width = columns.width * annotation_scale;
let gutter = columns.gutter.max(0.0) * annotation_scale;
let total_width = width * count as f64 + gutter * count.saturating_sub(1) as f64;
let anchor = match text.attachment_point {
acadrust::entities::mtext::AttachmentPoint::TopCenter
| acadrust::entities::mtext::AttachmentPoint::MiddleCenter
| acadrust::entities::mtext::AttachmentPoint::BottomCenter => 0.5,
acadrust::entities::mtext::AttachmentPoint::TopRight
| acadrust::entities::mtext::AttachmentPoint::MiddleRight
| acadrust::entities::mtext::AttachmentPoint::BottomRight => 1.0,
_ => 0.0,
};
let block_left = -anchor * total_width;
let origin = [text.insertion_point.x, text.insertion_point.y];
let (sin_r, cos_r) = rotation.sin_cos();
let mut bounds = vec![[f64::MAX, f64::MAX, f64::MIN, f64::MIN]; count];
for vertex in verts {
let x = vertex.pos[0] as f64 + vertex.pos_low[0] as f64 - origin[0];
let y = vertex.pos[1] as f64 + vertex.pos_low[1] as f64 - origin[1];
let local_x = x * cos_r + y * sin_r;
let local_y = -x * sin_r + y * cos_r;
let stride = width + gutter;
let physical = ((local_x - block_left) / stride)
.floor()
.clamp(0.0, count.saturating_sub(1) as f64) as usize;
bounds[physical][0] = bounds[physical][0].min(local_x);
bounds[physical][1] = bounds[physical][1].min(local_y);
bounds[physical][2] = bounds[physical][2].max(local_x);
bounds[physical][3] = bounds[physical][3].max(local_y);
}
let to_world = |x: f64, y: f64| {
[
origin[0] + x * cos_r - y * sin_r,
origin[1] + x * sin_r + y * cos_r,
]
};
bounds
.into_iter()
.filter(|bounds| bounds[0] <= bounds[2] && bounds[1] <= bounds[3])
.map(|bounds| {
let [left, bottom, right, top] = [
bounds[0] - pad,
bounds[1] - pad,
bounds[2] + pad,
bounds[3] + pad,
];
[
to_world(left, bottom),
to_world(right, bottom),
to_world(right, top),
to_world(left, top),
]
})
.collect()
}
pub(crate) fn explicit_mtext_background(entity: &EntityType) -> Option<[f32; 4]> {
let EntityType::MText(text) = entity else {
return None;
@@ -633,7 +708,14 @@ pub fn tessellate(
// stay a roughly constant on-screen size; otherwise the header-driven path.
let te = crate::entities::point::relative_render(entity, document, world_per_pixel)
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
.or_else(|| convert(entity, document));
.or_else(|| match entity {
EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale(
text,
document,
anno_scale,
)),
_ => convert(entity, document),
});
if let Some(te) = te {
match te.object {
// ── Text / MText: pre-tessellated glyph strokes ───────────────
@@ -733,9 +815,32 @@ pub fn tessellate(
}
bin_first[bi] = false;
for &[x, y] in stroke {
let xv = x as f64 * anno + slx_v;
let yv = y as f64 * anno + sly_v;
let (h, l) = split_ds_xyz(xv, yv, elev_v);
let world = if let Some(plane) = group.plane {
let scaled_origin = [
plane.scale_origin[0]
+ (plane.origin[0] - plane.scale_origin[0]) * anno,
plane.scale_origin[1]
+ (plane.origin[1] - plane.scale_origin[1]) * anno,
plane.scale_origin[2]
+ (plane.origin[2] - plane.scale_origin[2]) * anno,
];
let x = x as f64 * anno;
let y = y as f64 * anno;
[
scaled_origin[0]
+ plane.x_axis[0] * x
+ plane.y_axis[0] * y,
scaled_origin[1]
+ plane.x_axis[1] * x
+ plane.y_axis[1] * y,
scaled_origin[2]
+ plane.x_axis[2] * x
+ plane.y_axis[2] * y,
]
} else {
[x as f64 * anno + slx_v, y as f64 * anno + sly_v, elev_v]
};
let (h, l) = split_ds_xyz(world[0], world[1], world[2]);
bins[bi].pts.push(h);
bins[bi].pts_low.push(l);
}
@@ -743,9 +848,32 @@ pub fn tessellate(
// 2. Process fill triangles
for &[x, y] in &group.fill_tris {
let xv = x as f64 * anno + slx_v;
let yv = y as f64 * anno + sly_v;
let (h, l) = split_ds_xyz(xv, yv, elev_v);
let world = if let Some(plane) = group.plane {
let scaled_origin = [
plane.scale_origin[0]
+ (plane.origin[0] - plane.scale_origin[0]) * anno,
plane.scale_origin[1]
+ (plane.origin[1] - plane.scale_origin[1]) * anno,
plane.scale_origin[2]
+ (plane.origin[2] - plane.scale_origin[2]) * anno,
];
let x = x as f64 * anno;
let y = y as f64 * anno;
[
scaled_origin[0]
+ plane.x_axis[0] * x
+ plane.y_axis[0] * y,
scaled_origin[1]
+ plane.x_axis[1] * x
+ plane.y_axis[1] * y,
scaled_origin[2]
+ plane.x_axis[2] * x
+ plane.y_axis[2] * y,
]
} else {
[x as f64 * anno + slx_v, y as f64 * anno + sly_v, elev_v]
};
let (h, l) = split_ds_xyz(world[0], world[1], world[2]);
bins[bi].fill_tris.push(h);
bins[bi].fill_tris_low.push(l);
}
@@ -793,14 +921,35 @@ pub fn tessellate(
run.bold,
&run.text,
);
crate::scene::pipeline::text_gpu::push_glyph_vertices(
&mut sdf_verts,
&quads,
[slx_v, sly_v, elev_v],
anno,
gcolor,
0.0,
);
if let Some(plane) = group.plane {
let scaled_origin = [
plane.scale_origin[0]
+ (plane.origin[0] - plane.scale_origin[0]) * anno,
plane.scale_origin[1]
+ (plane.origin[1] - plane.scale_origin[1]) * anno,
plane.scale_origin[2]
+ (plane.origin[2] - plane.scale_origin[2]) * anno,
];
crate::scene::pipeline::text_gpu::push_glyph_vertices_on_plane(
&mut sdf_verts,
&quads,
scaled_origin,
plane.x_axis,
plane.y_axis,
anno,
gcolor,
0.0,
);
} else {
crate::scene::pipeline::text_gpu::push_glyph_vertices(
&mut sdf_verts,
&quads,
[slx_v, sly_v, elev_v],
anno,
gcolor,
0.0,
);
}
}
}
}
@@ -849,11 +998,12 @@ pub fn tessellate(
.unwrap_or(m.rotation);
let text_height = m.height * anno;
let pad = (m.background_scale - 1.0).max(0.0) * text_height;
let corners = oriented_text_corners(
let corner_groups = oriented_mtext_corner_groups(
&sdf_verts,
[m.insertion_point.x, m.insertion_point.y],
m,
text_rotation,
pad,
anno,
);
// Fill / mask — two triangles behind the glyphs.
if has_fill {
@@ -862,13 +1012,18 @@ pub fn tessellate(
} else {
color_or_inherit(&m.background_color, bg_color)
};
let mut ft = Vec::with_capacity(6);
let mut ftl = Vec::with_capacity(6);
for &k in &[0usize, 1, 2, 0, 2, 3] {
let (h, lo) =
split_ds_xyz(corners[k][0], corners[k][1], elev_v);
ft.push(h);
ftl.push(lo);
let mut ft = Vec::with_capacity(6 * corner_groups.len());
let mut ftl = Vec::with_capacity(6 * corner_groups.len());
for corners in &corner_groups {
for &k in &[0usize, 1, 2, 0, 2, 3] {
let (h, lo) = split_ds_xyz(
corners[k][0],
corners[k][1],
elev_v,
);
ft.push(h);
ftl.push(lo);
}
}
wires.push(WireModel {
point_marker: None,
@@ -907,19 +1062,24 @@ pub fn tessellate(
// Text frame — a closed rectangle in the text
// colour around the same box.
if has_frame {
let loop_xy = [
corners[0],
corners[1],
corners[2],
corners[3],
corners[0],
];
let mut fp = Vec::with_capacity(5);
let mut fpl = Vec::with_capacity(5);
for &[x, y] in &loop_xy {
let (h, lo) = split_ds_xyz(x, y, elev_v);
fp.push(h);
fpl.push(lo);
let mut fp = Vec::with_capacity(6 * corner_groups.len());
let mut fpl = Vec::with_capacity(6 * corner_groups.len());
for (group_index, corners) in corner_groups.iter().enumerate() {
if group_index > 0 {
fp.push([f32::NAN; 3]);
fpl.push([0.0; 3]);
}
for &[x, y] in &[
corners[0],
corners[1],
corners[2],
corners[3],
corners[0],
] {
let (h, lo) = split_ds_xyz(x, y, elev_v);
fp.push(h);
fpl.push(lo);
}
}
wires.push(WireModel {
point_marker: None,
-4
View File
@@ -134,10 +134,6 @@ fn apply_text_defaults(doc: &CadDocument, entity: &mut EntityType) {
if text.oblique_angle.abs() <= 1.0e-9 {
text.oblique_angle = resolved.oblique_angle;
}
if text.generation_flags == 0 {
text.generation_flags = (if resolved.flags.backward { 2 } else { 0 })
| (if resolved.flags.upside_down { 4 } else { 0 });
}
}
EntityType::MText(text) => {
if resolved.height > 1.0e-9 {
+66 -6
View File
@@ -18,6 +18,7 @@ use super::{ChangeKind, Scene};
pub(crate) const POLYLINE_ARC_CENTER_MARKER: i32 = -4;
const POLYLINE_ARC_POINT_MARKER_BASE: i32 = -5;
pub(crate) const ARC_DIMENSION_POINT_MARKER: i32 = -1;
pub(crate) fn polyline_arc_point_marker(segment: i32) -> i32 {
POLYLINE_ARC_POINT_MARKER_BASE - segment.max(0)
@@ -341,11 +342,19 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option<Vector3> {
let source = *reference.xrefs.first()?;
let entity = scene.document.get_entity(source)?;
if reference.main_gs_marker == ARC_DIMENSION_POINT_MARKER {
let radial = radial_source_for_marker(entity, 0)?;
let sweep = positive_sweep(radial.start_angle, radial.end_angle);
let angle = radial.start_angle + sweep * reference.osnap_distance.clamp(0.0, 1.0);
return Some(radial.point_at_angle(angle));
}
if let Some(segment) =
polyline_arc_segment_from_point_marker(reference.main_gs_marker)
{
let radial = radial_source_for_marker(entity, segment)?;
return Some(radial.point_at_angle(reference.osnap_distance));
let sweep = positive_sweep(radial.start_angle, radial.end_angle);
let angle = radial.start_angle + sweep * reference.osnap_distance.clamp(0.0, 1.0);
return Some(radial.point_at_angle(angle));
}
if reference.main_gs_marker == POLYLINE_ARC_CENTER_MARKER {
let segment = reference.osnap_distance.round().max(0.0) as usize;
@@ -489,6 +498,33 @@ fn point_on_radial_circle(
]))
}
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes((
normal.x, normal.y, normal.z,
));
Plane::from_axes(
[origin.x, origin.y, origin.z],
[x_axis.0, x_axis.1, x_axis.2],
[y_axis.0, y_axis.1, y_axis.2],
)
}
fn remap_plane_offset(
old_plane: Plane,
new_plane: Plane,
old_origin: Vector3,
old_point: Vector3,
new_origin: Vector3,
) -> Vector3 {
let old_origin = old_plane.project(dpoint(old_origin)).unwrap_or([0.0; 2]);
let old_point = old_plane.project(dpoint(old_point)).unwrap_or(old_origin);
let new_origin_uv = new_plane.project(dpoint(new_origin)).unwrap_or([0.0; 2]);
vector3(new_plane.point_at([
new_origin_uv[0] + old_point[0] - old_origin[0],
new_origin_uv[1] + old_point[1] - old_origin[1],
]))
}
pub(crate) fn dimension_is_associative(
document: &acadrust::CadDocument,
dimension: Handle,
@@ -987,6 +1023,7 @@ impl Scene {
let old_center = arc.center_point;
let old_definition = arc.definition_point;
let old_plane = plane_from_normal(old_center, arc.base.normal);
let old_source_radius = old_center.distance(&arc.first_extension_point);
let old_dim_radius = old_center.distance(&old_definition);
let radial_offset = old_dim_radius - old_source_radius;
@@ -996,7 +1033,7 @@ impl Scene {
arc.arc_end_parameter,
) * 0.5;
let old_definition_angle =
angle_about_plane(radial.plane, old_center, old_definition);
angle_about_plane(old_plane, old_center, old_definition);
let definition_angle_offset =
signed_angle_delta(old_definition_angle - old_mid);
@@ -1015,7 +1052,27 @@ impl Scene {
dim_radius,
middle + definition_angle_offset,
);
let delta = definition - old_definition;
let text_middle = remap_plane_offset(
old_plane,
radial.plane,
old_definition,
arc.base.text_middle_point,
definition,
);
let insertion = remap_plane_offset(
old_plane,
radial.plane,
old_definition,
arc.base.insertion_point,
definition,
);
let second_leader = remap_plane_offset(
old_plane,
radial.plane,
old_definition,
arc.second_leader_point,
definition,
);
arc.center_point = center;
arc.first_extension_point = first;
@@ -1024,9 +1081,12 @@ impl Scene {
arc.arc_end_parameter = end;
arc.definition_point = definition;
arc.base.definition_point = definition;
if let Some(normal) = radial.plane.normal() {
arc.base.normal = vector3(normal);
}
if arc.base.text_user_positioned {
arc.base.text_middle_point = arc.base.text_middle_point + delta;
arc.base.insertion_point = arc.base.insertion_point + delta;
arc.base.text_middle_point = text_middle;
arc.base.insertion_point = insertion;
} else {
arc.base.text_middle_point = definition;
arc.base.insertion_point = definition;
@@ -1034,7 +1094,7 @@ impl Scene {
if arc.has_leader {
arc.first_leader_point =
point_on_radial_circle(radial, dim_radius, middle);
arc.second_leader_point = arc.second_leader_point + delta;
arc.second_leader_point = second_leader;
}
arc.base.actual_measurement = arc.measurement();
}
+4 -1
View File
@@ -203,8 +203,11 @@ impl Scene {
/// If any handle belongs to a selectable group, also select every member.
pub fn expand_selection_for_groups(&mut self, handles: &[Handle]) {
let previous_len = self.selected.len();
self.selected
.extend(self.handles_expanded_for_selectable_groups(handles));
self.bump_selection();
if self.selected.len() != previous_len {
self.bump_selection_set();
}
}
}
+11
View File
@@ -1486,6 +1486,9 @@ pub struct Scene {
/// pick only refreshes the GPU xray overlay (cheap) instead of bumping
/// `geometry_epoch` and re-tessellating the whole model.
pub selection_generation: u64,
/// Cached fingerprint of `selected`, recomputed lazily after mutations.
selection_fingerprint_cache: u64,
selection_fingerprint_dirty: bool,
/// Cached tessellation of all visible entity wires for the current layout.
/// Keyed by `(geometry_epoch, camera_generation)` so a camera change
/// invalidates the cull-dependent wire list as well as a geometry change.
@@ -1865,6 +1868,8 @@ impl Scene {
projection_bounds_epoch: std::cell::Cell::new(0),
block_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
selection_generation: 0,
selection_fingerprint_cache: 0,
selection_fingerprint_dirty: false,
wire_cache: RefCell::new(None),
interaction_index_cache: RefCell::new(Vec::new()),
interaction_index_pending_key: std::cell::Cell::new(None),
@@ -2618,6 +2623,11 @@ impl Scene {
self.selection_generation = self.selection_generation.wrapping_add(1);
}
pub(crate) fn bump_selection_set(&mut self) {
self.selection_fingerprint_dirty = true;
self.bump_selection();
}
/// Milliseconds after the last camera change during which the view counts as
/// "actively navigating" for interaction-LOD purposes.
const NAV_SETTLE_MS: u128 = 130;
@@ -4582,6 +4592,7 @@ impl Scene {
.extend(self.selected.iter().copied());
self.selected.clear();
self.selected_order.clear();
self.bump_selection_set();
self.bump_entities(&changes);
}
+45
View File
@@ -151,6 +151,51 @@ pub fn push_glyph_vertices(
}
}
#[allow(clippy::too_many_arguments)]
pub fn push_glyph_vertices_on_plane(
out: &mut Vec<TextVertex>,
quads: &[GlyphQuad],
origin: [f64; 3],
x_axis: [f64; 3],
y_axis: [f64; 3],
anno: f64,
color: [f32; 4],
draw_depth: f32,
) {
for q in quads {
let mk = |ci: usize, uv: [f32; 2]| -> TextVertex {
let c = q.corners[ci];
let x = c[0] as f64 * anno;
let y = c[1] as f64 * anno;
let world = [
origin[0] + x_axis[0] * x + y_axis[0] * y,
origin[1] + x_axis[1] * x + y_axis[1] * y,
origin[2] + x_axis[2] * x + y_axis[2] * y,
];
let (xh, xl) = split_ds(world[0]);
let (yh, yl) = split_ds(world[1]);
let (zh, zl) = split_ds(world[2]);
TextVertex {
pos: [xh, yh, zh],
pos_low: [xl, yl, zl],
uv,
color,
draw_depth,
}
};
let bl = [q.uv_min[0], q.uv_max[1]];
let br = [q.uv_max[0], q.uv_max[1]];
let tr = [q.uv_max[0], q.uv_min[1]];
let tl = [q.uv_min[0], q.uv_min[1]];
out.push(mk(0, bl));
out.push(mk(1, br));
out.push(mk(2, tr));
out.push(mk(0, bl));
out.push(mk(2, tr));
out.push(mk(3, tl));
}
}
/// Slide every glyph vertex by a world-space delta, re-splitting the
/// double-single position. Lets a grip drag move already-shaped text by
/// translating the drag-start glyphs each frame instead of re-tessellating
+67 -10
View File
@@ -1,6 +1,9 @@
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::*;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
impl Scene {
// ── Selection ─────────────────────────────────────────────────────────
/// Treat a classic LEADER and its attached annotation as one logical object.
@@ -37,8 +40,11 @@ impl Scene {
}
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
let mut changed = false;
if exclusive {
changed = self.selected.len() != handles.len()
|| handles.iter().any(|handle| !self.selected.contains(handle));
self.selected.clear();
self.selected_order.clear();
}
@@ -46,15 +52,35 @@ impl Scene {
for handle in handles {
if self.selected.insert(handle) {
self.selected_order.push(handle);
changed = true;
}
}
self.bump_selection();
if changed {
self.bump_selection_set();
}
}
pub fn deselect_all(&mut self) {
if self.selected.is_empty() {
return;
}
self.selected.clear();
self.selected_order.clear();
self.bump_selection();
self.bump_selection_set();
}
pub(crate) fn selection_fingerprint(&mut self) -> u64 {
if self.selection_fingerprint_dirty {
let mut fingerprint = self.selected.len() as u64;
for handle in &self.selected {
let mut hasher = DefaultHasher::new();
handle.hash(&mut hasher);
fingerprint ^= hasher.finish();
}
self.selection_fingerprint_cache = fingerprint;
self.selection_fingerprint_dirty = false;
}
self.selection_fingerprint_cache
}
pub(crate) fn selected_handles_in_order(&self) -> Vec<Handle> {
@@ -103,7 +129,7 @@ impl Scene {
order.extend(added);
self.selected = selected;
self.selected_order = order;
self.bump_selection();
self.bump_selection_set();
}
}
@@ -118,7 +144,7 @@ impl Scene {
}
if changed {
self.bump_selection();
self.bump_selection_set();
}
}
@@ -199,7 +225,7 @@ impl Scene {
}
}
if added > 0 {
self.bump_selection();
self.bump_selection_set();
}
added
}
@@ -223,7 +249,9 @@ impl Scene {
self.selected_order.push(h);
}
}
self.bump_selection();
if self.selected != prev {
self.bump_selection_set();
}
self.selected.len()
}
@@ -709,7 +737,8 @@ impl Scene {
let mut handle_set: HashSet<Handle> = HashSet::default();
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
let mut highlight_changed = false;
let mut selection_changed = false;
let mut hover_changed = false;
for &h in &erase_handles {
// Objects on a locked layer can't be erased.
@@ -724,11 +753,11 @@ impl Scene {
self.delete_solid_history(h);
self.remember_removed_cache_categories(h);
self.document.remove_entity_arc(h);
highlight_changed |= self.selected.remove(&h);
selection_changed |= self.selected.remove(&h);
self.selected_order.retain(|selected| *selected != h);
if self.hover_highlight == Some(h) {
self.hover_highlight = None;
highlight_changed = true;
hover_changed = true;
}
self.hatches.remove(&h);
self.images.remove(&h);
@@ -738,7 +767,9 @@ impl Scene {
handle_set.insert(h);
erased.push((h, ChangeKind::Removed));
}
if highlight_changed {
if selection_changed {
self.bump_selection_set();
} else if hover_changed {
self.bump_selection();
}
// Capture exactly the group objects that this erase will rewrite, plus
@@ -838,3 +869,29 @@ impl Scene {
restored
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selection_fingerprint_tracks_final_set_only() {
let mut scene = Scene::default();
let first = Handle::new(1);
let second = Handle::new(2);
scene.select_entity(first, false);
scene.select_entity(second, false);
let fingerprint = scene.selection_fingerprint();
assert!(!scene.selection_fingerprint_dirty);
scene.deselect_all();
scene.select_entity(second, false);
scene.select_entity(first, false);
assert!(scene.selection_fingerprint_dirty);
assert_eq!(scene.selection_fingerprint(), fingerprint);
scene.set_hover_highlight(Some(Handle::new(3)));
assert!(!scene.selection_fingerprint_dirty);
assert_eq!(scene.selection_fingerprint(), fingerprint);
}
}