diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 088fc1333..91491dfde 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4794,7 +4794,7 @@ impl OpenCADStudio { second_ends, } => { use crate::scene::parametric_constraints::{ - resolve_point, set_resolved_point, two_lines_placement, ConstraintKind, + resolve_point, two_lines_placed_entity, ConstraintKind, }; let scope = self.tabs[i].current_parametric_scope(); let refs = [first_line, second_line]; @@ -4806,38 +4806,16 @@ impl OpenCADStudio { return self.apply_cmd_result(CmdResult::ReportError(message.to_string())); } // The reference's placement of the second line, written - // before the Parallel constraint holds it there. - let placed = { - let document = &self.tabs[i].scene.document; - let world = |reference: crate::scene::parametric_constraints::ParametricRef| { - let entity = document.get_entity(reference.entity)?; - let point = resolve_point(entity, reference.marker?)?; - Some(glam::DVec3::new(point.x, point.y, point.z)) - }; - let ends = |pair: [crate::scene::parametric_constraints::ParametricRef; 2]| { - Some([world(pair[0])?, world(pair[1])?]) - }; - ends(first_ends) - .zip(ends(second_ends)) - .and_then(|(first, second)| two_lines_placement(first, first_pick, second)) - }; - let placed_entity = placed.and_then(|points| { - let mut entity = self.tabs[i] - .scene - .document - .get_entity(second_line.entity) - .cloned()?; - let written = second_ends.iter().zip(points).all(|(reference, point)| { - reference.marker.is_some_and(|marker| { - set_resolved_point( - &mut entity, - marker, - acadrust::types::Vector3::new(point.x, point.y, point.z), - ) - }) - }); - written.then_some(entity) - }); + // before the Parallel constraint holds it there. Built out of + // line and boxed: this arm's locals would otherwise sit in + // `apply_cmd_result`'s frame, which is on every click's stack. + let placed_entity = two_lines_placed_entity( + &self.tabs[i].scene.document, + first_ends, + first_pick, + second_line, + second_ends, + ); let already = self.tabs[i] .scene .parametric_constraint_set(scope) @@ -4861,11 +4839,7 @@ impl OpenCADStudio { }); let pending = self.begin_undo(i, "Parallel constraint", 1, true); if let Some(entity) = placed_entity { - let before = self.tabs[i].scene.document.get_entity_arc(second_line.entity); - self.tabs[i] - .scene - .record_undo_before(second_line.entity, before); - self.tabs[i].scene.update_entity(entity); + self.tabs[i].scene.replace_entity_recorded(entity); } self.tabs[i] .scene @@ -6286,6 +6260,7 @@ impl OpenCADStudio { if from_menu { match cmd.as_str() { "DCRADIUS" => self.dim_constraint_last = "Radius", + "DCDIAMETER" => self.dim_constraint_last = "Diameter", "DCCONVERT" => self.dim_constraint_last = "Convert", _ => {} } diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 48480816e..e2f170d19 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1398,10 +1398,10 @@ impl OpenCADStudio { } if conversions.is_empty() { self.command_line - .push_output("0 associative dimensions converted"); + .push_output(crate::t!("0 associative dimensions converted").as_ref()); self.command_line.push_output( - format!("{refused} associative dimension(s) could not be converted") - .as_str(), + crate::tf!("{refused} associative dimension(s) could not be converted") + .as_ref(), ); self.tabs[i].scene.deselect_all(); self.refresh_properties(); @@ -1502,9 +1502,9 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.refresh_properties(); self.command_line - .push_output(format!("{converted} associative dimensions converted").as_str()); + .push_output(crate::tf!("{converted} associative dimensions converted").as_ref()); self.command_line.push_output( - format!("{refused} associative dimension(s) could not be converted").as_str(), + crate::tf!("{refused} associative dimension(s) could not be converted").as_ref(), ); if let Some(pd) = pending { self.commit_undo_delta(i, pd); diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index f9dcd2b1f..75cea5769 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -525,28 +525,47 @@ pub(crate) fn measured_expression(value: f64) -> String { /// Writes a constraint point's position: a line's start (0) or end (1), a /// polyline's vertex by index. Other entities and markers are left alone. +/// +/// The inverse of [`resolve_point`], which reads polyline vertices in WCS +/// through the polyline's OCS: the world point goes back into the OCS the +/// vertex is stored in (a mirrored polyline has a -Z normal, and writing the +/// world X into its OCS X would flip the vertex), the elevation stays the +/// polyline's own, and the closing segment's end marker names vertex 0. pub(crate) fn set_resolved_point( entity: &mut acadrust::EntityType, marker: i32, point: Vector3, ) -> bool { + if marker < 0 { + return false; + } + let index = polyline_vertex_index(entity, marker as usize).unwrap_or(marker as usize); + let to_ocs = |normal: Vector3| { + crate::scene::view::transform::wcs_point_to_ocs( + (point.x, point.y, point.z), + (normal.x, normal.y, normal.z), + ) + }; match entity { acadrust::EntityType::Line(line) => match marker { 0 => line.start = point, 1 => line.end = point, _ => return false, }, - acadrust::EntityType::LwPolyline(polyline) if marker >= 0 => { - let Some(vertex) = polyline.vertices.get_mut(marker as usize) else { + acadrust::EntityType::LwPolyline(polyline) => { + let (x, y, _) = to_ocs(polyline.normal); + let Some(vertex) = polyline.vertices.get_mut(index) else { return false; }; - vertex.location = acadrust::types::Vector2::new(point.x, point.y); + vertex.location = acadrust::types::Vector2::new(x, y); } - acadrust::EntityType::Polyline2D(polyline) if marker >= 0 => { - let Some(vertex) = polyline.vertices.get_mut(marker as usize) else { + acadrust::EntityType::Polyline2D(polyline) => { + let (x, y, _) = to_ocs(polyline.normal); + let Some(vertex) = polyline.vertices.get_mut(index) else { return false; }; - vertex.location = point; + vertex.location.x = x; + vertex.location.y = y; } _ => return false, } @@ -585,6 +604,36 @@ pub(crate) fn two_lines_placement( Some([s0 + shift, s0 + heading * length + shift]) } +/// The second line of an Aligned 2Lines pick moved to where +/// [`two_lines_placement`] puts it, or `None` when either line cannot be +/// resolved or written back. +/// +/// Out of line and boxed on purpose: the caller is an arm of the command +/// driver's result dispatcher, whose debug-build frame is on the stack of +/// every click, and an `EntityType` held there is paid for on each of them. +#[inline(never)] +pub(crate) fn two_lines_placed_entity( + document: &acadrust::CadDocument, + first_ends: [ParametricRef; 2], + first_pick: glam::DVec3, + second_line: ParametricRef, + second_ends: [ParametricRef; 2], +) -> Option> { + let world = |reference: ParametricRef| { + let point = resolve_point(document.get_entity(reference.entity)?, reference.marker?)?; + Some(glam::DVec3::new(point.x, point.y, point.z)) + }; + let ends = |pair: [ParametricRef; 2]| Some([world(pair[0])?, world(pair[1])?]); + let points = two_lines_placement(ends(first_ends)?, first_pick, ends(second_ends)?)?; + let mut entity = Box::new(document.get_entity(second_line.entity)?.clone()); + let written = second_ends.iter().zip(points).all(|(reference, point)| { + reference.marker.is_some_and(|marker| { + set_resolved_point(&mut entity, marker, Vector3::new(point.x, point.y, point.z)) + }) + }); + written.then_some(entity) +} + /// Moves a dynamic dimension's extension origins to `first`/`second`, /// keeping its dimension line where it was; true when anything moved. fn dynamic_dimension_follow_points( @@ -2400,6 +2449,17 @@ impl super::Scene { let _ = self.document.layers.add(layer); } + /// Replace an entity in place inside an open undo recording, keeping the + /// previous version for undo. Out of line so a boxed entity handed over by + /// the command driver never lands in its frame. + #[inline(never)] + pub(crate) fn replace_entity_recorded(&mut self, entity: Box) { + let handle = entity.common().handle; + let before = self.document.get_entity_arc(handle); + self.record_undo_before(handle, before); + self.update_entity(*entity); + } + /// True when an angular constraint drives with the parameter `name`. pub(crate) fn parameter_is_angular(&self, name: &str) -> bool { self.parametric_constraints