From 6272722baa2b05cd30b9e86d746e1630d5d34795 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:40:40 +0300 Subject: [PATCH] Carry hatch origin through creation and pattern editing --- src/app/command_driver.rs | 37 +++------------------- src/app/update/command.rs | 16 ++-------- src/entities/hatch.rs | 58 +++++++++++----------------------- src/io/pdf_export.rs | 1 + src/modules/draw/draw/hatch.rs | 3 ++ src/scene/entity.rs | 6 ++++ src/scene/mod.rs | 1 + src/scene/model/hatch_model.rs | 2 ++ src/scene/preview.rs | 1 + src/scene/view/render.rs | 1 + src/ui/properties.rs | 1 + 11 files changed, 42 insertions(+), 85 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index e651ef0fb..8ecf348ec 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4545,11 +4545,6 @@ impl OpenCADStudio { if let Some(entry) = crate::scene::model::hatch_patterns::find(&name) { - let old_origin = hatch - .pattern - .lines - .first() - .map(|line| line.base_point); let mut pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry); crate::entities::hatch::scale_pattern_geometry( &mut pattern, @@ -4559,16 +4554,8 @@ impl OpenCADStudio { &mut pattern, (angle as f64).to_radians(), ); - if let (Some(old), Some(new)) = ( - old_origin, - pattern.lines.first().map(|line| line.base_point), - ) { - crate::entities::hatch::translate_pattern_geometry( - &mut pattern, - old.x - new.x, - old.y - new.y, - ); - } + let origin = hatch.pattern_origin(); + crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y); hatch.pattern = pattern; hatch.is_solid = matches!( entry.gpu, @@ -4582,30 +4569,16 @@ impl OpenCADStudio { let requested_scale = scale.max(1.0e-6) as f64; if hatch.pattern_scale > 1.0e-12 { let factor = requested_scale / hatch.pattern_scale; - crate::entities::hatch::scale_pattern_geometry( - &mut hatch.pattern, - factor, - ); + hatch.scale_pattern_about_origin(factor); } let requested_angle = (angle as f64).to_radians(); let delta = requested_angle - hatch.pattern_angle; - crate::entities::hatch::rotate_pattern_geometry( - &mut hatch.pattern, - delta, - ); + hatch.rotate_pattern_about_origin(delta); } hatch.pattern_scale = scale.max(1.0e-6) as f64; hatch.pattern_angle = (angle as f64).to_radians(); if let Some((x, y)) = origin { - if let Some(current) = - hatch.pattern.lines.first().map(|line| line.base_point) - { - crate::entities::hatch::translate_pattern_geometry( - &mut hatch.pattern, - x - current.x, - y - current.y, - ); - } + hatch.set_pattern_origin(acadrust::types::Vector2::new(x, y)); } if disassociate { for path in &mut hatch.paths { diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 0b927aabf..8aa11eb94 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1997,9 +1997,6 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if let Some(acadrust::EntityType::Hatch(dxf)) = self.tabs[i].scene.document.get_entity_mut(handle) { - let old_origin = dxf.pattern.lines.first().map(|line| { - (line.base_point.x, line.base_point.y) - }); let mut pattern = hatch_patterns::build_dxf_pattern(entry); // Stored pattern lines are final world-space // geometry. Preserve the selected hatch's scale, @@ -2013,17 +2010,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { &mut pattern, dxf.pattern_angle, ); - if let (Some((old_x, old_y)), Some(new_origin)) = - (old_origin, pattern.lines.first()) - { - let dx = old_x - new_origin.base_point.x; - let dy = old_y - new_origin.base_point.y; - crate::entities::hatch::translate_pattern_geometry( - &mut pattern, - dx, - dy, - ); - } + let origin = dxf.pattern_origin(); + crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y); dxf.pattern = pattern; dxf.is_solid = matches!( entry.gpu, diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 05036ee30..0acf8ebde 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -182,18 +182,14 @@ fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> { (n > 0.0).then(|| (sx / n, sy / n)) } -/// Scale the stored, final hatch-pattern geometry around the pattern origin. +/// Scale a catalog pattern about its intrinsic coordinate origin. /// Pattern lines loaded from DXF/DWG are rendered prebaked, so their metadata /// scale is not applied again by the renderer. pub(crate) fn scale_pattern_geometry( pattern: &mut acadrust::entities::HatchPattern, factor: f64, ) { - let (origin_x, origin_y) = pattern - .lines - .first() - .map(|line| (line.base_point.x, line.base_point.y)) - .unwrap_or((0.0, 0.0)); + let (origin_x, origin_y) = (0.0, 0.0); for line in pattern.lines.iter_mut() { line.base_point.x = origin_x + (line.base_point.x - origin_x) * factor; line.base_point.y = origin_y + (line.base_point.y - origin_y) * factor; @@ -205,18 +201,14 @@ pub(crate) fn scale_pattern_geometry( } } -/// Rotate the stored, final hatch-pattern geometry around the pattern origin. +/// Rotate a catalog pattern about its intrinsic coordinate origin. /// This keeps the line angle, base point and world-space offset in sync. pub(crate) fn rotate_pattern_geometry( pattern: &mut acadrust::entities::HatchPattern, angle: f64, ) { let (sin, cos) = angle.sin_cos(); - let (origin_x, origin_y) = pattern - .lines - .first() - .map(|line| (line.base_point.x, line.base_point.y)) - .unwrap_or((0.0, 0.0)); + let (origin_x, origin_y) = (0.0, 0.0); for line in pattern.lines.iter_mut() { line.angle += angle; let (x, y) = ( @@ -613,7 +605,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { _ => h.pattern_type, }; if requested != h.pattern_type { - let old_origin = h.pattern.lines.first().map(|line| line.base_point); + let old_origin = h.pattern_origin(); h.pattern_type = requested; h.is_solid = false; match requested { @@ -629,15 +621,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { crate::scene::model::hatch_patterns::build_dxf_pattern(entry); scale_pattern_geometry(&mut pattern, h.pattern_scale); rotate_pattern_geometry(&mut pattern, h.pattern_angle); - if let (Some(old), Some(new)) = - (old_origin, pattern.lines.first().map(|line| line.base_point)) - { - translate_pattern_geometry( - &mut pattern, - old.x - new.x, - old.y - new.y, - ); - } + translate_pattern_geometry(&mut pattern, old_origin.x, old_origin.y); h.pattern = pattern; } } @@ -695,7 +679,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { "pattern_angle" => { let angle = v.to_radians(); let delta = angle - h.pattern_angle; - rotate_pattern_geometry(&mut h.pattern, delta); + h.rotate_pattern_about_origin(delta); h.pattern_angle = angle; } // The stored pattern lines are the FINAL rendered geometry (offsets, @@ -706,7 +690,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { let old = h.pattern_scale; if old > 1e-12 { let k = v / old; - scale_pattern_geometry(&mut h.pattern, k); + h.scale_pattern_about_origin(k); } h.pattern_scale = v; } @@ -724,19 +708,17 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { } // Origin rows are relative offsets and return to zero after commit. "origin_x" => { - for line in h.pattern.lines.iter_mut() { - line.base_point.x += v; - } + let origin = h.pattern_origin(); + h.set_pattern_origin(acadrust::types::Vector2::new(origin.x + v, origin.y)); } "origin_y" => { - for line in h.pattern.lines.iter_mut() { - line.base_point.y += v; - } + let origin = h.pattern_origin(); + h.set_pattern_origin(acadrust::types::Vector2::new(origin.x, origin.y + v)); } "iso_pen_width" if v > 0.0 => { let old = h.pattern_scale; if old > 1e-12 { - scale_pattern_geometry(&mut h.pattern, v / old); + h.scale_pattern_about_origin(v / old); } h.pattern_scale = v; } @@ -876,10 +858,8 @@ impl Grippable for Hatch { GripApply::Absolute(point) => (point.x - gx, point.y - gy), GripApply::Translate(delta) => (delta.x, delta.y), }; - for line in self.pattern.lines.iter_mut() { - line.base_point.x += dx; - line.base_point.y += dy; - } + let origin = self.pattern_origin(); + self.set_pattern_origin(acadrust::types::Vector2::new(origin.x + dx, origin.y + dy)); return; } id += 1; @@ -1005,11 +985,11 @@ impl Grippable for Hatch { fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) { use crate::scene::model::object::GripMenuAction as A; if grip_id == 0 && matches!(action, A::OriginPoint) { - if let (Some((gx, gy)), Some((ox, oy))) = ( + if let (Some((gx, gy)), Some((_ox, _oy))) = ( boundary_centroid(self), self.pattern.lines.first().map(|line| (line.base_point.x, line.base_point.y)), ) { - translate_pattern_geometry(&mut self.pattern, gx - ox, gy - oy); + self.set_pattern_origin(acadrust::types::Vector2::new(gx, gy)); } } } @@ -1038,14 +1018,14 @@ impl Grippable for Hatch { A::HatchAngle => { let angle = value.to_radians(); let delta = angle - self.pattern_angle; - rotate_pattern_geometry(&mut self.pattern, delta); + self.rotate_pattern_about_origin(delta); self.pattern_angle = angle; } A::HatchScale => { if value > 0.0 { if self.pattern_scale > 1e-12 { let factor = value / self.pattern_scale; - scale_pattern_geometry(&mut self.pattern, factor); + self.scale_pattern_about_origin(factor); } self.pattern_scale = value; } diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 10b72da48..5dc8161e1 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -927,6 +927,7 @@ fn emit_wire_fills( } boundary.push(boundary[0]); let hatch = HatchModel { + pattern_origin: None, render_instance: wire.render_instance.clone(), world_origin: [0.0, 0.0], boundary: std::sync::Arc::new(boundary), diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 8fa7c244a..3ce2a4c1a 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -387,6 +387,7 @@ impl HatchCommand { } } return HatchModel { + pattern_origin: source.pattern_origin, render_instance: None, boundary: std::sync::Arc::new(rel), pattern, @@ -445,6 +446,7 @@ impl HatchCommand { } } HatchModel { + pattern_origin: Some(self.default_origin), render_instance: None, boundary: std::sync::Arc::new(rel), pattern, @@ -969,6 +971,7 @@ impl GradientCommand { 1.0e-6, ); HatchModel { + pattern_origin: None, render_instance: None, boundary: std::sync::Arc::new(rel), pattern: HatchPattern::Gradient { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2943e7458..4636b9390 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1674,6 +1674,7 @@ impl Scene { let fill_plane_boundary = Some(Arc::new(fill_plane.1)); let fill_plane = Some(fill_plane.0); models.push(HatchModel { + pattern_origin: None, render_instance, boundary: Arc::new(boundary), boundary_wcs: None, @@ -2107,6 +2108,7 @@ impl Scene { let storage = crate::entities::curve::ocs_plane(dxf.normal, dxf.elevation); Some(HatchModel { + pattern_origin: Some([dxf.pattern_origin().x, dxf.pattern_origin().y]), render_instance: None, boundary: std::sync::Arc::new(boundary_f32), boundary_wcs: None, @@ -2429,6 +2431,7 @@ impl Scene { }) .collect(); HatchModel { + pattern_origin: None, render_instance: None, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, @@ -2663,6 +2666,9 @@ impl Scene { // carries `world_origin: [0, 0]`, which after the world_offset removal // leaves the fill mis-placed and effectively invisible until a later // edit rebuilds it from the DXF — so keep the seed, don't overwrite it. + if let Some(origin) = model.pattern_origin { + dxf.record_pattern_origin(Vector2::new(origin[0], origin[1])); + } let mut entity = EntityType::Hatch(dxf); if let Some(layer) = layer { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 83771658a..f1457f85f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3794,6 +3794,7 @@ impl Scene { let ((x0, y0), (x1, y1)) = self.paper_limits()?; let (x0, y0, x1, y1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); Some(HatchModel { + pattern_origin: None, render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]), diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 1ae480b4d..8165bcdbd 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -208,6 +208,8 @@ pub struct FillPlane { /// A hatched region defined by a closed polygon boundary. #[derive(Clone, Debug)] pub struct HatchModel { + /// Recorded hatch origin in its fill plane, independent of intrinsic PAT line bases. + pub pattern_origin: Option<[f64; 2]>, pub render_instance: Option, /// World XY anchor (in the same offset-relative coordinate space as /// the rest of the scene — `world_offset` already subtracted, but diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 93b91c9cf..20bd00d73 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -154,6 +154,7 @@ impl Scene { return None; } let mut model = HatchModel { + pattern_origin: None, render_instance: None, world_origin: origin, boundary: std::sync::Arc::new(boundary), diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 331a536f5..efde7319e 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -2886,6 +2886,7 @@ impl Scene { } boundary.push(boundary[0]); hatches.push(HatchModel { + pattern_origin: None, render_instance: wire.render_instance.clone(), world_origin: [0.0, 0.0], boundary: Arc::new(boundary), diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 9033adb99..35b98867a 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -131,6 +131,7 @@ impl canvas::Program for HatchPatternPreview { } HatchPattern::Pattern(_) => { let model = HatchModel { + pattern_origin: None, render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(vec![