diff --git a/Cargo.lock b/Cargo.lock index bd3dfc372..94740fe19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#4bda0711bd1f7219806c616ef0852ab92fa8c030" +source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#81ebfee1deeee1750a6ccc0c1d82a9d69d488f3a" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/src/entities/shape.rs b/src/entities/shape.rs index 3c615e9f2..583f75579 100644 --- a/src/entities/shape.rs +++ b/src/entities/shape.rs @@ -57,9 +57,16 @@ fn shape_marker( impl TruckConvertible for Shape { fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - let ox = self.insertion_point.x; - let oy = self.insertion_point.y; - let oz = self.insertion_point.z; + // SHAPE insertion point is OCS (planar entity) — map through the + // arbitrary axis so a mirrored shape's marker lands where it renders. + let (ox, oy, oz) = crate::scene::view::transform::ocs_point_to_wcs( + ( + self.insertion_point.x, + self.insertion_point.y, + self.insertion_point.z, + ), + (self.normal.x, self.normal.y, self.normal.z), + ); let size = self.size.abs().max(0.5); let snap_pt = glam::DVec3::new(ox, oy, oz); diff --git a/src/entities/solid.rs b/src/entities/solid.rs index b65581d7a..597773433 100644 --- a/src/entities/solid.rs +++ b/src/entities/solid.rs @@ -17,20 +17,23 @@ use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; -fn v3(v: &acadrust::types::Vector3) -> [f64; 3] { - [v.x, v.y, v.z] -} - fn dvec3(v: &acadrust::types::Vector3) -> glam::DVec3 { glam::DVec3::new(v.x, v.y, v.z) } impl TruckConvertible for Solid { fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - let p0 = v3(&self.first_corner); - let p1 = v3(&self.second_corner); - let p2 = v3(&self.third_corner); - let p3 = v3(&self.fourth_corner); + // SOLID corners are OCS (planar entity) — map through the arbitrary + // axis so mirrored solids (normal 0,0,-1) land on the correct side. + let n = (self.normal.x, self.normal.y, self.normal.z); + let w = |v: &acadrust::types::Vector3| -> [f64; 3] { + let (x, y, z) = crate::scene::view::transform::ocs_point_to_wcs((v.x, v.y, v.z), n); + [x, y, z] + }; + let p0 = w(&self.first_corner); + let p1 = w(&self.second_corner); + let p2 = w(&self.third_corner); + let p3 = w(&self.fourth_corner); // DXF SOLID vertex order: 1-2-4-3 (Z-shaped), render as closed quad outline. // AutoCAD stores corners in "Z" order: p0-p1 are top edge, p2-p3 are bottom edge, @@ -49,12 +52,12 @@ impl TruckConvertible for Solid { p0, ]; - let dv = |c: &acadrust::types::Vector3| glam::DVec3::new(c.x, c.y, c.z); + let dvp = |p: [f64; 3]| glam::DVec3::new(p[0], p[1], p[2]); let snap = vec![ - (dv(&self.first_corner), SnapHint::Node), - (dv(&self.second_corner), SnapHint::Node), - (dv(&self.third_corner), SnapHint::Node), - (dv(&self.fourth_corner), SnapHint::Node), + (dvp(p0), SnapHint::Node), + (dvp(p1), SnapHint::Node), + (dvp(p2), SnapHint::Node), + (dvp(p3), SnapHint::Node), ]; // Fill the visual quad p0→p1→p3→p2 (Z-order) as two triangles; a diff --git a/src/scene/entity.rs b/src/scene/entity.rs index c9d9e0f94..b4a9f80c9 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1806,11 +1806,20 @@ impl Scene { // `hatch_model_from_dxf` uses. Casting the absolute WCS corner straight // to f32 costs ~0.06 units of resolution at UTM magnitudes (~1e6), so // the quad snapped to a grid and the fill drifted off its outline. + // SOLID corners are stored in OCS (like ARC/LWPOLYLINE) — map through + // the arbitrary axis so a mirrored solid (normal 0,0,-1) fills on the + // correct side of the drawing. + let n = (solid.normal.x, solid.normal.y, solid.normal.z); + let w = |v: &acadrust::types::Vector3| -> [f64; 2] { + let (x, y, _) = + crate::scene::view::transform::ocs_point_to_wcs((v.x, v.y, v.z), n); + [x, y] + }; let corners: [[f64; 2]; 4] = [ - [solid.first_corner.x, solid.first_corner.y], - [solid.second_corner.x, solid.second_corner.y], - [solid.fourth_corner.x, solid.fourth_corner.y], - [solid.third_corner.x, solid.third_corner.y], + w(&solid.first_corner), + w(&solid.second_corner), + w(&solid.fourth_corner), + w(&solid.third_corner), ]; let mut min = [f64::INFINITY; 2]; let mut max = [f64::NEG_INFINITY; 2]; diff --git a/src/scene/view/dispatch.rs b/src/scene/view/dispatch.rs index 661646a9f..c1bc6c197 100644 --- a/src/scene/view/dispatch.rs +++ b/src/scene/view/dispatch.rs @@ -28,8 +28,41 @@ pub fn prop_current_vertex() -> usize { PROP_CURRENT_VERTEX.with(|c| c.get()) } +/// Extrusion normal of a planar (OCS-stored) entity when it isn't the +/// default +Z. `None` = the entity stores WCS coordinates (LINE, POINT, +/// ELLIPSE, MTEXT, ...) or maps its own OCS internally (INSERT) — grips pass +/// through untouched. Per-entity grip code works in raw field space, so the +/// dispatch layer converts: display grips OCS→WCS, incoming edits WCS→OCS. +fn planar_ocs_normal(entity: &EntityType) -> Option<(f64, f64, f64)> { + let n = match entity { + EntityType::Arc(e) => e.normal, + EntityType::Circle(e) => e.normal, + EntityType::LwPolyline(e) => e.normal, + EntityType::Polyline2D(e) => e.normal, + EntityType::Solid(e) => e.normal, + EntityType::Text(e) => e.normal, + EntityType::AttributeDefinition(e) => e.normal, + EntityType::Hatch(e) => e.normal, + EntityType::Shape(e) => e.normal, + _ => return None, + }; + if n.x == 0.0 && n.y == 0.0 && n.z == 1.0 { + None + } else { + Some((n.x, n.y, n.z)) + } +} + pub fn grips(entity: &EntityType) -> Vec { - EntityTypeOps::grips(entity) + let mut grips = EntityTypeOps::grips(entity); + if let Some(n) = planar_ocs_normal(entity) { + for g in &mut grips { + let (x, y, z) = + super::transform::ocs_point_to_wcs((g.world.x, g.world.y, g.world.z), n); + g.world = glam::DVec3::new(x, y, z); + } + } + grips } pub fn properties_sectioned( @@ -193,6 +226,21 @@ pub fn apply_geom_prop(entity: &mut EntityType, field: &str, value: &str) { } pub fn apply_grip(entity: &mut EntityType, grip_id: usize, apply: crate::scene::model::object::GripApply) { + use crate::scene::model::object::GripApply; + // Grip drags arrive in world space; per-entity apply code writes raw + // fields, which for planar entities live in OCS — convert first (the + // pure-rotation map handles points and deltas alike). + let apply = match (planar_ocs_normal(entity), apply) { + (Some(n), GripApply::Absolute(p)) => { + let (x, y, z) = super::transform::wcs_point_to_ocs((p.x, p.y, p.z), n); + GripApply::Absolute(glam::DVec3::new(x, y, z)) + } + (Some(n), GripApply::Translate(d)) => { + let (x, y, z) = super::transform::wcs_point_to_ocs((d.x, d.y, d.z), n); + GripApply::Translate(glam::DVec3::new(x, y, z)) + } + (None, a) => a, + }; EntityTypeOps::apply_grip(entity, grip_id, apply); } diff --git a/src/scene/view/transform.rs b/src/scene/view/transform.rs index b7e7f73a7..a94b75e66 100644 --- a/src/scene/view/transform.rs +++ b/src/scene/view/transform.rs @@ -134,6 +134,22 @@ pub fn ocs_point_to_wcs(ocs: (f64, f64, f64), normal: (f64, f64, f64)) -> (f64, ) } +/// Inverse of [`ocs_point_to_wcs`]: express a WCS point (or direction — the +/// map is a pure rotation, so both use the same math) in the OCS frame of +/// `normal`. Used when writing user edits made in world space (grip drags) +/// back into a planar entity's OCS-stored coordinates. +#[inline] +pub fn wcs_point_to_ocs(wcs: (f64, f64, f64), normal: (f64, f64, f64)) -> (f64, f64, f64) { + let (ax, ay) = ocs_axes(normal); + let (wx, wy, wz) = wcs; + let (nx, ny, nz) = normal; + ( + wx * ax.0 + wy * ax.1 + wz * ax.2, + wx * ay.0 + wy * ay.1 + wz * ay.2, + wx * nx + wy * ny + wz * nz, + ) +} + #[cfg(test)] mod mirror_delegation_tests { use super::*;