diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index e01f82af0..a3411a7d4 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -2677,13 +2677,21 @@ impl OpenCADStudio { } } acadrust::EntityType::LwPolyline(p) => { - for v in &mut p.vertices { + let Some(mut world) = + crate::entities::curve::lwpolyline_world_xy(p) + else { + continue; + }; + for v in &mut world.vertices { if in_win(v.location.x, v.location.y) { v.location.x += dx; v.location.y += dy; stretched = true; } } + if stretched { + *p = world; + } } acadrust::EntityType::Polyline2D(p) => { for v in &mut p.vertices { diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index f5dc9e6c7..c26073815 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -857,33 +857,21 @@ impl OpenCADStudio { use acadrust::types::Vector3; let mut loops: Vec> = Vec::new(); for (_, e) in self.tabs[i].scene.selected_entities().iter() { - match e { + let supported = matches!( + e, acadrust::EntityType::LwPolyline(pl) - if pl.is_closed && pl.vertices.len() >= 3 => - { - loops.push( - pl.vertices - .iter() - .map(|v| Vector3::new(v.location.x, v.location.y, 0.0)) - .collect(), - ); - } - acadrust::EntityType::Circle(c) => { - let n = 64; - loops.push( - (0..n) - .map(|k| { - let a = std::f64::consts::TAU * k as f64 / n as f64; - Vector3::new( - c.center.x + c.radius * a.cos(), - c.center.y + c.radius * a.sin(), - c.center.z, - ) - }) - .collect(), - ); - } - _ => {} + if pl.is_closed && pl.vertices.len() >= 3 + ) || matches!(e, acadrust::EntityType::Circle(_)); + if supported { + let Some(curve) = crate::entities::curve::entity_curve(e) else { + continue; + }; + loops.push( + crate::entities::curve::curve_points(&curve) + .into_iter() + .map(|point| Vector3::new(point[0], point[1], point[2])) + .collect(), + ); } } if loops.is_empty() { diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 1ba66b3a1..c4ba816f5 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -258,31 +258,22 @@ impl super::OpenCADStudio { /// reusing the box primitive + the boolean union path. pub(super) fn solid_polysolid(&mut self, width: f64, height: f64) -> Task { let i = self.active_tab; - let found: Option<(Handle, Vec<[f64; 2]>, bool)> = self.tabs[i] + let found: Option<(Handle, Vec<[f64; 3]>)> = self.tabs[i] .scene .selected_entities() .iter() .filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h)) .find_map(|(h, e)| match e { - EntityType::LwPolyline(pl) => Some(( - *h, - pl.vertices - .iter() - .map(|v| [v.location.x, v.location.y]) - .collect(), - pl.is_closed, - )), + EntityType::LwPolyline(_) => crate::entities::curve::entity_curve(e) + .map(|curve| (*h, crate::entities::curve::curve_points(&curve))), _ => None, }); - let Some((handle, pts, closed)) = found else { + let Some((handle, pts)) = found else { self.command_line .push_error(crate::t!("POLYSOLID: select a polyline first.").as_ref()); return Task::none(); }; - let mut segs: Vec<([f64; 2], [f64; 2])> = pts.windows(2).map(|w| (w[0], w[1])).collect(); - if closed && pts.len() > 2 { - segs.push((pts[pts.len() - 1], pts[0])); - } + let segs: Vec<([f64; 3], [f64; 3])> = pts.windows(2).map(|w| (w[0], w[1])).collect(); let mut acc: Option = None; let mut used = 0usize; for (a, b) in &segs { @@ -302,7 +293,7 @@ impl super::OpenCADStudio { [cos, sin, 0.0], [-sin, cos, 0.0], [0.0, 0.0, 1.0], - [a[0], a[1], 0.0], + [a[0], a[1], a[2]], ) }) else { @@ -570,15 +561,22 @@ impl super::OpenCADStudio { .selected_entities() .iter() .filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h)) - .find_map(|(h, e)| match e { - EntityType::LwPolyline(pl) => Some(( + .find_map(|(h, e)| { + let EntityType::LwPolyline(_) = e else { + return None; + }; + let curve = crate::entities::curve::entity_curve(e)?; + let cadkernel::geom2d::Curve::Polyline(polyline) = &curve.curve else { + return None; + }; + Some(( *h, - pl.vertices + polyline + .vertices .iter() - .map(|v| [v.location.x, v.location.y, 0.0]) + .map(|vertex| curve.plane.point_at(vertex.position)) .collect(), - )), - _ => None, + )) }); let Some((handle, fit)) = found else { self.command_line @@ -595,7 +593,7 @@ impl super::OpenCADStudio { let p = |k: usize| glam::DVec3::new(fit[k][0], fit[k][1], fit[k][2]); // Catmull-Rom → cubic Bézier control points: [P0, b1,b2,P1, b1,b2,P2, …]. let mut ctrl: Vec = Vec::with_capacity(3 * m + 1); - ctrl.push(Vector3::new(fit[0][0], fit[0][1], 0.0)); + ctrl.push(Vector3::new(fit[0][0], fit[0][1], fit[0][2])); for seg in 0..m { let p0 = p(seg); let p1 = p(seg + 1); @@ -603,9 +601,9 @@ impl super::OpenCADStudio { let next = if seg + 2 <= m { p(seg + 2) } else { p1 }; let b1 = p0 + (p1 - prev) / 6.0; let b2 = p1 - (next - p0) / 6.0; - ctrl.push(Vector3::new(b1.x, b1.y, 0.0)); - ctrl.push(Vector3::new(b2.x, b2.y, 0.0)); - ctrl.push(Vector3::new(p1.x, p1.y, 0.0)); + ctrl.push(Vector3::new(b1.x, b1.y, b1.z)); + ctrl.push(Vector3::new(b2.x, b2.y, b2.z)); + ctrl.push(Vector3::new(p1.x, p1.y, p1.z)); } // Clamped piecewise-Bézier knots (degree 3): len == ctrl.len()+degree+1. let mut knots: Vec = vec![0.0; 4]; @@ -617,7 +615,10 @@ impl super::OpenCADStudio { spl.degree = 3; spl.control_points = ctrl; spl.knots = knots; - spl.fit_points = fit.iter().map(|q| Vector3::new(q[0], q[1], 0.0)).collect(); + spl.fit_points = fit + .iter() + .map(|q| Vector3::new(q[0], q[1], q[2])) + .collect(); // flags.rational defaults to false (non-rational) — exactly what we want. self.push_undo_snapshot(i, "SPLINEFIT"); self.tabs[i].scene.erase_entities(&[handle]); diff --git a/src/entities/curve.rs b/src/entities/curve.rs index b83d0cf02..23116cc41 100644 --- a/src/entities/curve.rs +++ b/src/entities/curve.rs @@ -352,6 +352,42 @@ pub fn entity_curve_xy(entity: &EntityType) -> Option { }) } +/// Re-expresses an axis-aligned LWPOLYLINE in world XY coordinates. +pub fn lwpolyline_world_xy(polyline: &LwPolylineEnt) -> Option { + let normal = normalized(polyline.normal); + if normal.x.abs() > 1e-12 || normal.y.abs() > 1e-12 || normal.z.abs() <= 1e-12 { + return None; + } + let Curve::Polyline(curve) = entity_curve_xy(&EntityType::LwPolyline(polyline.clone()))? + else { + return None; + }; + if curve.vertices.len() != polyline.vertices.len() { + return None; + } + + let mut world = polyline.clone(); + for (vertex, kernel) in world.vertices.iter_mut().zip(curve.vertices) { + vertex.location.x = kernel.position[0]; + vertex.location.y = kernel.position[1]; + vertex.bulge = kernel.bulge; + } + world.elevation = ocs_plane(polyline.normal, polyline.elevation).origin[2]; + world.thickness *= normal.z; + world.normal = Vector3::new(0.0, 0.0, 1.0); + Some(world) +} + +/// Normalizes only LWPOLYLINE entities; other types pass through unchanged. +pub fn entity_with_lwpolyline_world_xy(entity: &EntityType) -> EntityType { + match entity { + EntityType::LwPolyline(polyline) => lwpolyline_world_xy(polyline) + .map(EntityType::LwPolyline) + .unwrap_or_else(|| entity.clone()), + _ => entity.clone(), + } +} + /// World-space wire points sampled by the kernel's angular policy. pub fn curve_points(curve: &PlanarCurve) -> Vec<[f64; 3]> { curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE) diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 49f370bbe..452ae0975 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -1058,15 +1058,15 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline { .expect("an LwPolyline with at least two vertices has a planar curve"); let area = curve.curve.enclosed_area().abs(); let perimeter = curve.length(); - let (cx, cy) = curve + let center = curve .curve .enclosed_centroid() - .map(|point| (point[0], point[1])) .unwrap_or_else(|| { let x = p.vertices.iter().map(|v| v.location.x).sum::() / n as f64; let y = p.vertices.iter().map(|v| v.location.y).sum::() / n as f64; - (x, y) + [x, y] }); + let [cx, cy, _] = curve.plane.point_at(center); crate::entities::traits::MassProps { area, perimeter, diff --git a/src/modules/draw/modify/break_cmd.rs b/src/modules/draw/modify/break_cmd.rs index a236baf89..da7edb5c1 100644 --- a/src/modules/draw/modify/break_cmd.rs +++ b/src/modules/draw/modify/break_cmd.rs @@ -41,7 +41,10 @@ pub fn break_entity(entity: &EntityType, p1: DVec3, p2: DVec3) -> Option Some(break_line(line, p1, p2)), EntityType::Arc(arc) => Some(break_arc(arc, p1, p2)), EntityType::Circle(c) => Some(break_circle(c, p1, p2)), - EntityType::LwPolyline(p) => Some(break_lwpolyline(p, p1, p2)), + EntityType::LwPolyline(p) => { + let p = crate::entities::curve::lwpolyline_world_xy(p)?; + Some(break_lwpolyline(&p, p1, p2)) + } EntityType::Ellipse(e) => Some(break_ellipse(e, p1, p2)), EntityType::Spline(s) => Some(break_spline(s, p1, p2)), _ => None, diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index b756bcadf..fa1789c27 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -27,6 +27,7 @@ use acadrust::types::Vector3; use acadrust::{CadDocument, EntityType, Handle}; use crate::command::{CadCommand, CmdResult}; +use crate::entities::curve::lwpolyline_world_xy; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use glam::DVec3; use crate::t; @@ -188,6 +189,9 @@ pub fn normalize_entity_for_block(entity: EntityType) -> EntityType { } fn explode_lwpolyline(p: &LwPolyline) -> Vec { + let Some(p) = lwpolyline_world_xy(p) else { + return vec![]; + }; let n = p.vertices.len(); if n < 2 { return vec![]; diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index 04b3e7fb6..2f580f158 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1012,6 +1012,10 @@ pub struct FilletCommand { impl FilletCommand { pub fn new(radius: f64, all_entities: Vec) -> Self { + let all_entities: Vec = all_entities + .iter() + .map(crate::entities::curve::entity_with_lwpolyline_world_xy) + .collect(); let entity_index = ModifyEntityIndex::build(&all_entities); Self { radius: radius as f64, @@ -1392,6 +1396,10 @@ pub struct ChamferCommand { impl ChamferCommand { pub fn new(dist: f64, all_entities: Vec) -> Self { + let all_entities: Vec = all_entities + .iter() + .map(crate::entities::curve::entity_with_lwpolyline_world_xy) + .collect(); let entity_index = ModifyEntityIndex::build(&all_entities); Self { dist1: dist as f64, diff --git a/src/modules/draw/modify/join.rs b/src/modules/draw/modify/join.rs index 8d87a4868..b2bf2eae7 100644 --- a/src/modules/draw/modify/join.rs +++ b/src/modules/draw/modify/join.rs @@ -123,6 +123,7 @@ fn segs_of(e: &EntityType) -> Option> { }]) } EntityType::LwPolyline(p) => { + let p = crate::entities::curve::lwpolyline_world_xy(p)?; if p.is_closed || p.vertices.len() < 2 { return None; } diff --git a/src/modules/draw/modify/lengthen.rs b/src/modules/draw/modify/lengthen.rs index 5c7b21a8b..113ecbd6a 100644 --- a/src/modules/draw/modify/lengthen.rs +++ b/src/modules/draw/modify/lengthen.rs @@ -158,7 +158,10 @@ pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Op EntityType::Arc(a) => lengthen_arc(a, pick_pt, mode), EntityType::Ellipse(e) => lengthen_ellipse(e, pick_pt, mode), EntityType::Spline(s) => lengthen_spline(s, pick_pt, mode), - EntityType::LwPolyline(p) => lengthen_lwpoly(p, pick_pt, mode), + EntityType::LwPolyline(p) => { + let p = crate::entities::curve::lwpolyline_world_xy(p)?; + lengthen_lwpoly(&p, pick_pt, mode) + } _ => None, } } diff --git a/src/modules/draw/modify/offset.rs b/src/modules/draw/modify/offset.rs index 218f364b0..648786a12 100644 --- a/src/modules/draw/modify/offset.rs +++ b/src/modules/draw/modify/offset.rs @@ -10,7 +10,7 @@ // 3. Pick a point on the side to offset toward, or choose Multiple // to keep offsetting the newly created result at the same distance -use crate::entities::curve::entity_curve; +use crate::entities::curve::{entity_curve, lwpolyline_world_xy}; use crate::modules::draw::modify::spline_ops::spline_sample_xy; use acadrust::entities::LwVertex; use acadrust::entities::{ @@ -160,6 +160,9 @@ fn offset_arc(a: &ArcEnt, dist: f64, side_pt: Vec3) -> Option { /// offset passed to the algorithm is always ±1. This avoids fixed-epsilon /// failures on tiny drawings and on UTM-scale coordinates. fn offset_lwpolylines(p: &LwPolyline, dist: f64, side_pt: Vec3) -> Vec { + let Some(p) = lwpolyline_world_xy(p) else { + return Vec::new(); + }; let source = KernelPolyline { closed: p.is_closed, vertices: p diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 20e5abd8d..b170d8a7f 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -35,7 +35,7 @@ use cadkernel::geom2d::{ NurbsCurve, Ray as KernelRay, Tolerance as KernelTolerance, XLine as KernelXLine, }; -use crate::entities::curve::entity_curve_xy; +use crate::entities::curve::{entity_curve_xy, entity_with_lwpolyline_world_xy}; use crate::command::{CadCommand, CmdResult}; use crate::modules::draw::modify::spline_ops::{ @@ -2171,6 +2171,10 @@ pub struct TrimCommand { impl TrimCommand { pub fn new(all_entities: Vec) -> Self { + let all_entities: Vec = all_entities + .iter() + .map(entity_with_lwpolyline_world_xy) + .collect(); let entity_index = ModifyEntityIndex::build(&all_entities); let geos = build_geos(&all_entities); Self { @@ -2907,6 +2911,10 @@ pub struct ExtendCommand { impl ExtendCommand { pub fn new(all_entities: Vec) -> Self { + let all_entities: Vec = all_entities + .iter() + .map(entity_with_lwpolyline_world_xy) + .collect(); let entity_index = ModifyEntityIndex::build(&all_entities); let geos = build_geos(&all_entities); Self { @@ -3832,6 +3840,10 @@ pub struct ExtrimCommand { impl ExtrimCommand { pub fn new(all: Vec<(Handle, EntityType)>) -> Self { + let all = all + .into_iter() + .map(|(handle, entity)| (handle, entity_with_lwpolyline_world_xy(&entity))) + .collect(); Self { all, boundary: None, geos: Vec::new() } } }