fix(render): close remaining OCS/WCS gaps for mirrored entities

Follow-up to the LINE/POINT WCS fix - the same bug class elsewhere:

- 2D SOLID rendered its OCS corners raw in both the wire path and the
  shared fill builder (solid_hatch_model); SHAPE placed its marker at
  the raw OCS insertion. Both now map through the arbitrary axis.
- Grips: central wrap in view/dispatch.rs shows planar entities' grips
  at their world position (OCS→WCS) and converts drags back (WCS→OCS
  via the new wcs_point_to_ocs) - per-entity grip code stays in raw
  field space, INSERT keeps handling its own OCS.
- Pulls acadrust 81ebfee: OCS-aware bounding boxes, translate,
  transform and mirror for the ten planar entity types, so MOVE /
  ROTATE / MIRROR and culling work on mirrored entities too.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-21 17:58:09 +03:00
1 parent 59a0f5239c
commit 87049967a2
6 files changed
+105 -22

No files matched your search

Generated
+1 -1
View File
@@ -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",
+10 -3
View File
@@ -57,9 +57,16 @@ fn shape_marker(
impl TruckConvertible for Shape {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
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);
+16 -13
View File
@@ -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<TruckEntity> {
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
+13 -4
View File
@@ -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];
+49 -1
View File
@@ -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<GripDef> {
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);
}
+16
View File
@@ -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::*;