diff --git a/INTEGRATION_GAPS.md b/INTEGRATION_GAPS.md deleted file mode 100644 index 6ec695aa0..000000000 --- a/INTEGRATION_GAPS.md +++ /dev/null @@ -1,215 +0,0 @@ -# acadrust Integration Gaps - -Missing or incomplete integrations between acadrust entity definitions and H7CAD rendering/interaction systems. Ordered by priority within each category. - -Legend: ✅ Done · ⚠️ Partial · ❌ Not done - ---- - -## Entity Field Gaps (acadrust fields ignored in tessellation) - -### High Impact - -| Status | Entity | Ignored Field(s) | Effect | -|---|---|---|---| -| ✅ | **INSERT** | `column_count`, `row_count`, `column_spacing`, `row_spacing` | MINSERT handled by `acadrust::explode_from_document()` → `array_transforms()` | -| ✅ | **LWPolyline** | `constant_width`, vertex `start_width` / `end_width` | Implemented in `src/scene/mod.rs:3239` | -| ✅ | **Polyline (legacy)** | `Vertex2D.start_width` / `end_width` | Implemented in `src/scene/mod.rs` | - -### Medium Impact - -| Status | Entity | Ignored Field(s) | Effect | -|---|---|---|---| -| ✅ | **Dimension** | `DIMSCALE`, `DIMASZ`, `DIMEXO`, `DIMEXE` (from dimstyle) | Implemented in `src/scene/tessellate.rs:263` | -| ✅ | **Spline** | `weights` (rational NURBS control point weights) | NurbsCurve used when weights present — `src/entities/spline.rs` | -| ✅ | **Spline** | `flags.closed` / `flags.periodic` | Implemented in `src/entities/spline.rs:48` | -| ✅ | **Hatch** | `BoundaryEdge::Spline` tessellation | Implemented in `src/scene/mod.rs:1802` | -| ✅ | **MultiLeader** | `MultiLeaderPathType::Spline` | Implemented in `src/entities/multileader.rs:76` | -| ✅ | **RasterImage** | `clip_boundary` (polygonal/rectangular) | Implemented in `src/entities/raster_image.rs` — polygonal + rectangular clip | - -### Low Impact - -| Status | Entity | Ignored Field(s) | Effect | -|---|---|---|---| -| ✅ | **Arc / Circle / Line / LwPolyline / Polyline2D** | `thickness` | Extrudes along WCS normal — `src/entities/{arc,circle,line,lwpolyline,polyline}.rs` | -| ✅ | **LWPolyline** | `plinegen` flag | `plinegen=false` → `SegmentedLines` with NaN separators + distance reset in `wire_gpu.rs`; `plinegen=true` → `Contour` (continuous) | - ---- - -## Entity Type Gaps (entire types with missing subsystems) - -### Renders Nothing / Placeholder Only - -| Status | Entity | Notes | -|---|---|---| -| ✅ | **Face3D** | Full tessellation, grips, properties panel — `src/entities/mesh.rs` | -| ✅ | **OLE2Frame** | Bounding box + X mark with grips, properties panel, transform — `src/entities/ole2frame.rs` | - -### Wire Fallback Only (no full mesh) - -| Status | Entity | Missing | -|---|---|---| -| ✅ | **Solid3D / Region / Body** | SAT (text) + SAB (binary) both parsed — `SabReader::read` called when `is_binary` — tessellates plane/cone/sphere/torus faces — `src/scene/solid3d_tess.rs` | - -### Partial Render - -| Status | Entity | What works | What's missing | -|---|---|---|---| -| ✅ | **Viewport** (paper space) | Frame (active=gold, inactive=cyan), CPU projection + Cohen-Sutherland clipping, perspective, layer freeze per-viewport, MSPACE switching, pan/zoom/orbit, GPU scissor rect per viewport wire | Stencil masking for overlapping viewports (rare edge case) | - ---- - -## Systemic Gap — OCS→WCS Transform - -✅ **Done.** The DXF arbitrary-axis algorithm is implemented in `src/scene/transform.rs` (`ocs_axes`, `ocs_point_to_wcs`). Applied to: - -| Status | Entity | Location | -|---|---|---| -| ✅ | **Arc** | `src/entities/arc.rs` | -| ✅ | **Circle** | `src/entities/circle.rs` | -| ✅ | **Line** | `src/entities/line.rs` | -| ✅ | **Point** | `src/entities/point.rs` | -| ✅ | **Ellipse** | `src/entities/ellipse.rs` | -| ✅ | **LwPolyline** | `src/entities/lwpolyline.rs` | -| ✅ | **Spline** | DXF spec: spline control points are already in WCS (normal field unused) | -| ✅ | **Polyline2D** | `src/entities/polyline.rs` — elevation + normal applied | -| ✅ | **Polyline3D** | Vertices already in WCS per DXF spec (no OCS transform needed) | -| ✅ | **AttributeDefinition / AttributeEntity** | OCS→WCS applied to insertion snap point — `src/entities/attribute.rs` | -| ✅ | **Hatch** | OCS→WCS applied to all boundary edge types — `src/scene/tessellate.rs` | -| ✅ | **MLine / Leader** | Vertices already in WCS per DXF spec; normal field unused for coord transform | - -**Impact:** Low for typical 2D plan files (nearly all normals are `(0,0,1)`); high for 3D DXF files with entities on non-horizontal planes. - -The DXF arbitrary-axis algorithm (`src/scene/transform.rs:73`): -``` -if |Wx| < 1/64 and |Wy| < 1/64: - Ax = (0,1,0) × N -else: - Ax = (1,0,0) × N -Ax = normalize(Ax) -Ay = N × Ax -``` -Then transform each OCS point: `WCS = x*Ax + y*Ay + z*N` - ---- - -## Render Style Gaps (color, linetype, lineweight resolution) - -### High Impact - -| Status | Gap | Effect | Location | -|---|---|---|---| -| ✅ | **ByBlock color** resolved through INSERT chain | Implemented via `render_style_for_block_sub()` | `src/scene/render.rs:243` | -| ✅ | **ByBlock linetype** resolved through INSERT chain | Implemented | `src/scene/render.rs:260` | - -### Medium Impact - -| Status | Gap | Effect | Location | -|---|---|---|---| -| ✅ | **ByBlock lineweight** resolved from INSERT entity | Implemented | `src/scene/render.rs:266` | - ---- - -## Text & Style Gaps - -### Medium Impact - -| Status | Gap | Effect | Location | -|---|---|---|---| -| ✅ | **TextStyle `is_backward`** flag | Negative width_factor applied | `src/entities/text.rs:60` | -| ✅ | **TextStyle `is_upside_down`** flag | Rotation offset by π applied | `src/entities/text.rs:63` | - -### Low-Medium Impact - -| Status | Gap | Effect | Location | -|---|---|---|---| -| ✅ | **Complex linetype text shapes** | `LtSeg::Text` calls `cxf::tessellate_text_ex`, `LtSeg::Shape` calls `emit_shape` — `src/scene/complex_lt.rs` | - ---- - -## Polyline3D Vertex Type Gap - -✅ **Done.** `src/entities/polyline.rs:305` — VertexFlags (SPLINE_VERTEX flag 8, SPLINE_CONTROL flag 16) properly detected and used for wire/snap point selection. - ---- - -## Snap Point Gaps - -### Critical / High - -| Status | Entity | Missing Snap | Location | -|---|---|---|---| -| ✅ | **INSERT** | `Insertion` snap point | `src/scene/mod.rs` — appended after explode | -| ✅ | **INSERT** | Nested entity snap points | All sub-entity WireModels are flat-mapped into the scene wire list — snap sees them all | -| ✅ | **Hatch** | Snap points for circular arc boundaries | Center snaps added for CircularArc/EllipticArc edges — `src/scene/tessellate.rs` | - -### Medium - -| Status | Entity | Missing Snap | Location | -|---|---|---|---| -| ✅ | **Dimension** | Node snap hints on defpoints | `src/scene/tessellate.rs` — `dimension_snap_pts()` | -| ✅ | **Spline** | Fit/control points in snap_pts | `src/entities/spline.rs:38` | -| ✅ | **MultiLeader** | Vertices and connection points in snap_pts | `src/entities/multileader.rs` | -| ✅ | **MLine** | Vertices in snap_pts | `src/entities/mline.rs` | - -### Low - -| Status | Entity | Missing Snap | Location | -|---|---|---|---| -| ✅ | **Ellipse** (partial arc) | Endpoints in `key_vertices` → `SnapType::Endpoint` via snap engine | Correctly handled; `snap_pts` only needs Center | -| ✅ | **Hatch** | Elevation Z | Snap Z uses `hatch.elevation - world_offset.z` — `src/scene/tessellate.rs:627` | - ---- - -## Grip Gaps - -| Status | Entity | Missing Grip | Location | -|---|---|---|---| -| ✅ | **LWPolyline** | Midpoint grips for arc segments | `src/entities/lwpolyline.rs:162` | - ---- - -## Text Rendering Gaps - -| Status | Gap | Effect | Location | -|---|---|---|---| -| ✅ | **TextStyle `is_backward`** flag applied | Negative width_factor | `src/entities/text.rs:61` | -| ✅ | **TextStyle `is_upside_down`** flag applied | Rotation offset | `src/entities/text.rs:64` | -| ✅ | **Unicode characters** not in CXF fonts | Missing non-ASCII glyphs emit `eprintln!` warning (deduped per font+char) — `src/scene/cxf.rs` | - ---- - -## DXF Reader Unit Gaps (acadrust bugs we work around) - -These are fixed in our post-load `fix_dxf_dimension_rotations()` in `src/io/mod.rs`. - -| Status | Entity | Field | DXF Code | Fix Location | -|---|---|---|---|---| -| ✅ | **Dimension (Linear)** | `rotation` | 50 | `src/io/mod.rs:221` | -| ✅ | **Dimension (all)** | `text_rotation` | 53 | `src/scene/tessellate.rs::dimension_text_natural_rotation()` | -| ✅ | **AttributeEntity** | `rotation` | 50 | `src/io/mod.rs:224` | -| ✅ | **AttributeDefinition** | `rotation` | 50 | `src/io/mod.rs:219` | -| ✅ | **Shape** | `rotation` | 50 | Fixed in `fix_dxf_dimension_rotations()` — `src/io/mod.rs` | - ---- - -## Coverage Summary - -| Subsystem | Coverage | -|---|---| -| Tessellation | 34/41 entity types fully, 4 legacy fallback, 3 missing | -| Snap points | 36/41 (Face3D, Solid3D, Region, Body, OLE2Frame missing) | -| Grip points | 36/41 (same 5 missing) | -| Properties panel | 36/41 (same 5 missing) | -| Hit testing | 41/41 (all via fallback) | - -## Gap Status Summary - -| Status | Count | -|---|---| -| ✅ Done | 51 | -| ⚠️ Partial | 0 | -| ❌ Not done | 0 | -| **Total** | **51** | - -All gaps resolved. diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index 0f5b7dd75..82549afca 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -4,6 +4,7 @@ use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{ro_prop as ro, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; +use crate::scene::wire_model::SnapHint; use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; @@ -55,7 +56,12 @@ impl TruckConvertible for Face3D { Some(TruckEntity { object: TruckObject::Lines(pts), - snap_pts: vec![], + snap_pts: vec![ + (Vec3::from(p0), SnapHint::Node), + (Vec3::from(p1), SnapHint::Node), + (Vec3::from(p2), SnapHint::Node), + (Vec3::from(p3), SnapHint::Node), + ], tangent_geoms: vec![], key_vertices: vec![p0, p1, p2, p3], }) diff --git a/src/entities/mod.rs b/src/entities/mod.rs index b1aa9e247..5ec1230c7 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -21,6 +21,7 @@ pub mod underlay; pub mod ray; pub mod shape; pub mod solid; +pub mod solid3d; pub mod spline; pub mod text; pub(crate) mod text_support; diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs new file mode 100644 index 000000000..cf4adb0bc --- /dev/null +++ b/src/entities/solid3d.rs @@ -0,0 +1,167 @@ +// Grippable + PropertyEditable for Solid3D, Region, Body. +// +// Geometry lives in ACIS data — we cannot edit it via the properties panel. +// We expose the point_of_reference as a translate grip and show ACIS size +// as read-only info. Grip translate also updates wire points so the wire +// fallback stays in sync; the caller (scene/mod.rs apply_grip) translates +// the MeshModel vertices to match. + +use acadrust::entities::{Body, Region, Solid3D}; +use glam::Vec3; + +use crate::entities::common::{diamond_grip, ro_prop as ro}; +use crate::entities::traits::{Grippable, PropertyEditable}; +use crate::scene::object::{GripApply, GripDef, PropSection}; + +// ── shared helpers ──────────────────────────────────────────────────────────── + +fn por_to_vec3(v: &acadrust::types::Vector3) -> Vec3 { + Vec3::new(v.x as f32, v.y as f32, v.z as f32) +} + +fn translate_wires(wires: &mut Vec, d: Vec3) { + for wire in wires.iter_mut() { + for pt in wire.points.iter_mut() { + pt.x += d.x as f64; + pt.y += d.y as f64; + pt.z += d.z as f64; + } + } +} + +fn acis_size_str(has_data: bool, sat_len: usize, sab_len: usize, is_binary: bool) -> String { + if !has_data { + return "none".to_string(); + } + if is_binary { + format!("{} bytes (SAB)", sab_len) + } else { + format!("{} bytes (SAT)", sat_len) + } +} + +// ── Solid3D ─────────────────────────────────────────────────────────────────── + +impl Grippable for Solid3D { + fn grips(&self) -> Vec { + vec![diamond_grip(0, por_to_vec3(&self.point_of_reference))] + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + if grip_id != 0 { + return; + } + if let GripApply::Translate(d) = apply { + self.point_of_reference.x += d.x as f64; + self.point_of_reference.y += d.y as f64; + self.point_of_reference.z += d.z as f64; + translate_wires(&mut self.wires, d); + } + } +} + +impl PropertyEditable for Solid3D { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + let size = acis_size_str( + self.acis_data.has_data(), + self.acis_data.sat_data.len(), + self.acis_data.sab_data.len(), + self.acis_data.is_binary, + ); + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Ref Pt X", "s3d_px", format!("{:.4}", self.point_of_reference.x)), + ro("Ref Pt Y", "s3d_py", format!("{:.4}", self.point_of_reference.y)), + ro("Ref Pt Z", "s3d_pz", format!("{:.4}", self.point_of_reference.z)), + ro("ACIS Data", "s3d_acis", size), + ], + } + } + + fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} +} + +// ── Region ──────────────────────────────────────────────────────────────────── + +impl Grippable for Region { + fn grips(&self) -> Vec { + vec![diamond_grip(0, por_to_vec3(&self.point_of_reference))] + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + if grip_id != 0 { + return; + } + if let GripApply::Translate(d) = apply { + self.point_of_reference.x += d.x as f64; + self.point_of_reference.y += d.y as f64; + self.point_of_reference.z += d.z as f64; + translate_wires(&mut self.wires, d); + } + } +} + +impl PropertyEditable for Region { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + let size = acis_size_str( + self.acis_data.has_data(), + self.acis_data.sat_data.len(), + self.acis_data.sab_data.len(), + self.acis_data.is_binary, + ); + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Ref Pt X", "rgn_px", format!("{:.4}", self.point_of_reference.x)), + ro("Ref Pt Y", "rgn_py", format!("{:.4}", self.point_of_reference.y)), + ro("Ref Pt Z", "rgn_pz", format!("{:.4}", self.point_of_reference.z)), + ro("ACIS Data", "rgn_acis", size), + ], + } + } + + fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} +} + +// ── Body ────────────────────────────────────────────────────────────────────── + +impl Grippable for Body { + fn grips(&self) -> Vec { + vec![diamond_grip(0, por_to_vec3(&self.point_of_reference))] + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + if grip_id != 0 { + return; + } + if let GripApply::Translate(d) = apply { + self.point_of_reference.x += d.x as f64; + self.point_of_reference.y += d.y as f64; + self.point_of_reference.z += d.z as f64; + translate_wires(&mut self.wires, d); + } + } +} + +impl PropertyEditable for Body { + fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { + let size = acis_size_str( + self.acis_data.has_data(), + self.acis_data.sat_data.len(), + self.acis_data.sab_data.len(), + self.acis_data.is_binary, + ); + PropSection { + title: "Geometry".into(), + props: vec![ + ro("Ref Pt X", "bdy_px", format!("{:.4}", self.point_of_reference.x)), + ro("Ref Pt Y", "bdy_py", format!("{:.4}", self.point_of_reference.y)), + ro("Ref Pt Z", "bdy_pz", format!("{:.4}", self.point_of_reference.z)), + ro("ACIS Data", "bdy_acis", size), + ], + } + } + + fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} +} diff --git a/src/entities/traits.rs b/src/entities/traits.rs index b9eb0f613..8d8b7538d 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -87,6 +87,9 @@ impl EntityTypeOps for EntityType { EntityType::MLine(ml) => Grippable::grips(ml), EntityType::Tolerance(tol) => Grippable::grips(tol), EntityType::Solid(solid) => Grippable::grips(solid), + EntityType::Solid3D(s) => Grippable::grips(s), + EntityType::Region(r) => Grippable::grips(r), + EntityType::Body(b) => Grippable::grips(b), EntityType::Face3D(f) => Grippable::grips(f), EntityType::PolygonMesh(pm) => Grippable::grips(pm), EntityType::PolyfaceMesh(pfm) => Grippable::grips(pfm), @@ -177,6 +180,18 @@ impl EntityTypeOps for EntityType { solid, text_style_names, )), + EntityType::Solid3D(s) => Some(PropertyEditable::geometry_properties( + s, + text_style_names, + )), + EntityType::Region(r) => Some(PropertyEditable::geometry_properties( + r, + text_style_names, + )), + EntityType::Body(b) => Some(PropertyEditable::geometry_properties( + b, + text_style_names, + )), EntityType::Face3D(f) => Some(PropertyEditable::geometry_properties( f, text_style_names, @@ -267,6 +282,9 @@ impl EntityTypeOps for EntityType { EntityType::MLine(ml) => PropertyEditable::apply_geom_prop(ml, field, value), EntityType::Tolerance(tol) => PropertyEditable::apply_geom_prop(tol, field, value), EntityType::Solid(solid) => PropertyEditable::apply_geom_prop(solid, field, value), + EntityType::Solid3D(s) => PropertyEditable::apply_geom_prop(s, field, value), + EntityType::Region(r) => PropertyEditable::apply_geom_prop(r, field, value), + EntityType::Body(b) => PropertyEditable::apply_geom_prop(b, field, value), EntityType::Face3D(f) => PropertyEditable::apply_geom_prop(f, field, value), EntityType::PolygonMesh(pm) => PropertyEditable::apply_geom_prop(pm, field, value), EntityType::PolyfaceMesh(pfm) => PropertyEditable::apply_geom_prop(pfm, field, value), @@ -307,6 +325,9 @@ impl EntityTypeOps for EntityType { EntityType::MLine(ml) => Grippable::apply_grip(ml, grip_id, apply), EntityType::Tolerance(tol) => Grippable::apply_grip(tol, grip_id, apply), EntityType::Solid(solid) => Grippable::apply_grip(solid, grip_id, apply), + EntityType::Solid3D(s) => Grippable::apply_grip(s, grip_id, apply), + EntityType::Region(r) => Grippable::apply_grip(r, grip_id, apply), + EntityType::Body(b) => Grippable::apply_grip(b, grip_id, apply), EntityType::Face3D(f) => Grippable::apply_grip(f, grip_id, apply), EntityType::PolygonMesh(pm) => Grippable::apply_grip(pm, grip_id, apply), EntityType::PolyfaceMesh(pfm) => Grippable::apply_grip(pfm, grip_id, apply), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0e140e33d..fa32079c3 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2304,9 +2304,42 @@ impl Scene { // ── Grip editing ────────────────────────────────────────────────────── pub fn apply_grip(&mut self, handle: Handle, grip_id: usize, apply: GripApply) { + // For Solid3D / Region / Body, record the old point_of_reference so we + // can translate the pre-tessellated MeshModel by the same delta after + // the grip is applied (the ACIS data itself is not modified). + let old_por: Option<[f64; 3]> = match self.document.get_entity(handle) { + Some(EntityType::Solid3D(s)) => Some([s.point_of_reference.x, s.point_of_reference.y, s.point_of_reference.z]), + Some(EntityType::Region(r)) => Some([r.point_of_reference.x, r.point_of_reference.y, r.point_of_reference.z]), + Some(EntityType::Body(b)) => Some([b.point_of_reference.x, b.point_of_reference.y, b.point_of_reference.z]), + _ => None, + }; + if let Some(entity) = self.document.get_entity_mut(handle) { dispatch::apply_grip(entity, grip_id, apply); } + + // Translate MeshModel vertices by the same delta the grip applied. + if let Some(old) = old_por { + let new_por: Option<[f64; 3]> = match self.document.get_entity(handle) { + Some(EntityType::Solid3D(s)) => Some([s.point_of_reference.x, s.point_of_reference.y, s.point_of_reference.z]), + Some(EntityType::Region(r)) => Some([r.point_of_reference.x, r.point_of_reference.y, r.point_of_reference.z]), + Some(EntityType::Body(b)) => Some([b.point_of_reference.x, b.point_of_reference.y, b.point_of_reference.z]), + _ => None, + }; + if let Some(new) = new_por { + let dx = (new[0] - old[0]) as f32; + let dy = (new[1] - old[1]) as f32; + let dz = (new[2] - old[2]) as f32; + if let Some(mesh) = self.meshes.get_mut(&handle) { + for v in &mut mesh.verts { + v[0] += dx; + v[1] += dy; + v[2] += dz; + } + } + } + } + // Rebuild GPU hatch/solid model when a boundary vertex or corner moves. let hatch_offset = if self.current_layout == "Model" { self.world_offset } else { [0.0; 3] }; match self.document.get_entity(handle) { diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index 5ce020178..648e9a118 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -259,7 +259,24 @@ pub fn tessellate( // stored in the entity when present (e.g. from SOLVIEW output // or when the SAT kernel cannot parse the ACIS data). let wire_pts = solid_wire_fallback(entity, world_offset); - return WireModel::solid(name, wire_pts, color, selected); + let mut wm = WireModel::solid(name, wire_pts, color, selected); + // Add insertion snap at point_of_reference. + let [ox, oy, oz] = world_offset; + let por = match entity { + EntityType::Solid3D(s) => Some(&s.point_of_reference), + EntityType::Region(r) => Some(&r.point_of_reference), + EntityType::Body(b) => Some(&b.point_of_reference), + _ => None, + }; + if let Some(p) = por { + let sp = Vec3::new( + (p.x - ox) as f32, + (p.y - oy) as f32, + (p.z - oz) as f32, + ); + wm.snap_pts.push((sp, SnapHint::Insertion)); + } + return wm; } } }