From c227de26c5daf4458f9389879af9f127c49d9c50 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:54:52 +0300 Subject: [PATCH] Record a region profile as its boundary curve and step A swept, extruded or revolved surface made from a region records the region as the curve round it, as the reference does: a lone circle or whole ellipse as itself, a loop of lines and arcs as a closed polyline in the region's plane. A region recorded as a region made the reference refuse to open the drawing (modeling error 58000). A region read by an associative action carries the step its edge names belong to (ACAD_STEPID), so the reference finds the edges again and the surface follows the region. Co-Authored-By: Claude Opus 5.5 --- src/scene/dwg_native_constraints.rs | 5 ++ src/scene/model/sweep_command_model.rs | 5 +- src/scene/model/sweep_model.rs | 88 ++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index 87376b4b7..68f65065c 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -2851,6 +2851,11 @@ fn source_reads( }; let sizes = loops.iter().map(Vec::len).collect::>(); let ids = register_region_subents(document, &sizes)?; + // The region carries the step its edge names belong to; without it (and + // without named edges in its modeler data) the reference cannot find + // the edges again. + crate::scene::view::dispatch::set_entity_xdata(document, source, "ACAD_STEPID", + Some(vec![codec::xdata::XDataValue::Integer32(ids.first()? - 1)])); let curves = loops.into_iter().flatten().collect::>(); let pairs = curves.iter().map(|_| (document.allocate_handle(), document.allocate_handle())).collect::>(); for (index, (curve, (edge, read))) in curves.into_iter().zip(pairs.iter().copied()).enumerate() { diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs index fd21e3e66..d6f3d0ba1 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -12,7 +12,10 @@ use super::sweep_model::{embedded_path, embedded_revolve_profile}; /// A 3D polyline profile or path as the reference records it: a wire body. fn embedded_sweep_profile(entity: &EntityType) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> { match entity { - EntityType::Region(region) => Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array())), + // A region as the curve round it, as the reference records it. + EntityType::Region(region) => super::sweep_model::region_boundary_entity(entity).or_else(|| { + Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array())) + }), EntityType::Polyline3D(value) => Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array())), _ => embedded_revolve_profile(entity), } diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 16b66c16a..4c1c944a5 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -662,10 +662,98 @@ pub fn embedded_path(entity: &EntityType) -> Option { } } +/// The frame placing a plane's coordinates in the world: its axes, normal +/// and origin as matrix columns. +fn plane_frame(plane: &Plane) -> Option<[f64; 16]> { + let normal = plane.normal()?; + let column = |v: [f64; 3], w: f64| glam::DVec4::new(v[0], v[1], v[2], w); + Some(glam::DMat4::from_cols( + column(plane.x_axis, 0.0), + column(plane.y_axis, 0.0), + column(normal, 0.0), + column(plane.origin, 1.0), + ) + .to_cols_array()) +} + +/// A region of one boundary loop as the curve the reference records for it +/// as a profile: a lone circle or whole ellipse as itself, a loop of lines +/// and arcs as a closed polyline in the region's plane (with that plane's +/// frame), read from the region's modeler edges. +pub fn region_boundary_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> { + let EntityType::Region(region) = entity else { return None }; + if !region.acis_data.has_data() { + return None; + } + let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?; + let [boundary] = loops.as_slice() else { return None }; + let world = |v: [f64; 3]| Vector3::new(v[0], v[1], v[2]); + let normal = world(plane.normal()?); + let identity = glam::DMat4::IDENTITY.to_cols_array(); + match boundary.as_slice() { + [Curve::Circle(circle)] => { + let mut value = codec::entities::Circle::new(); + value.center = world(plane.point_at(circle.centre)); + value.radius = circle.radius; + value.normal = normal; + return Some((EmbeddedEntity::Circle(value), identity)); + } + [Curve::Ellipse(arc)] if (arc.sweep() - std::f64::consts::TAU).abs() <= 1e-9 => { + let ellipse = arc.ellipse; + let centre = Vec3::from(plane.point_at(ellipse.centre)); + let tip = Vec3::from(plane.point_at([ + ellipse.centre[0] + ellipse.major_axis[0] * ellipse.major_radius, + ellipse.centre[1] + ellipse.major_axis[1] * ellipse.major_radius, + ])); + let mut value = codec::entities::Ellipse::new(); + value.center = world(centre.to_array()); + value.major_axis = world((tip - centre).to_array()); + value.minor_axis_ratio = ellipse.minor_radius / ellipse.major_radius; + value.start_parameter = arc.start_parameter; + value.end_parameter = arc.start_parameter + std::f64::consts::TAU; + value.normal = normal; + return Some((EmbeddedEntity::Ellipse(value), identity)); + } + _ => {} + } + // Walk the loop head to tail; an arc run against its direction turns + // clockwise (negative bulge). + let close = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).hypot(a[1] - b[1]) <= 1e-9 * (1.0 + a[0].abs().max(a[1].abs())); + let ends = |piece: &Curve| match piece { + Curve::Line(line) => Some((line.start, line.end, 0.0)), + Curve::Arc(arc) => { + let at = |angle: f64| [arc.centre[0] + arc.radius * angle.cos(), arc.centre[1] + arc.radius * angle.sin()]; + Some((at(arc.start_angle), at(arc.end_angle), (arc.sweep() / 4.0).tan())) + } + _ => None, + }; + let (first_start, first_end, _) = ends(boundary.first()?)?; + let second = boundary.get(1).and_then(ends); + let mut at = if second.is_some_and(|(start, end, _)| close(first_start, start) || close(first_start, end)) { + first_end + } else { + first_start + }; + let mut polyline = LwPolyline::new(); + for piece in boundary { + let (start, end, bulge) = ends(piece)?; + let (from, to, bulge) = if close(start, at) { (start, end, bulge) } else if close(end, at) { (end, start, -bulge) } else { return None }; + let mut vertex = LwVertex::new(Vector2::new(from[0], from[1])); + vertex.bulge = bulge; + polyline.vertices.push(vertex); + at = to; + } + polyline.is_closed = true; + Some((EmbeddedEntity::LwPolyline(polyline), plane_frame(&plane)?)) +} + fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> { if region_with_holes(entity) { return None; } + if let Some(found) = region_boundary_entity(entity) { + return Some(found); + } if !matches!(entity, EntityType::Polyline3D(_)) { if let Some(embedded) = embedded_path(entity) { return Some((embedded, glam::DMat4::IDENTITY.to_cols_array()));