forked from 1b-agent/opencadstudio-upstream
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:
commit
6ef2d75e84
67 files changed
+4773
-883
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user