Merge pull request #1679 from ramox81/codex/surface-associativity-sources

Associative extruded, revolved and lofted surfaces; spline and ellipse sources

# Conflicts:
#	src/app/command_driver/solid3d.rs
#	src/scene/dwg_native_constraints.rs
#	src/scene/model/mod.rs
#	src/scene/model/sweep_command_model.rs
#	src/scene/model/sweep_model.rs
#	src/scene/modify.rs
This commit is contained in:
Hakan Seven committed 2026-10-12 00:14:34 +03:00
commit 5bb0efd93e
12 files changed
+1106 -228

No files matched your search

+137 -10
View File
@@ -12,11 +12,7 @@ use kernel::geom2d::{Arc, Curve, EllipseArc, Line};
use kernel::space::{PlanarCurve, Plane, Vec3};
pub use super::sweep_command_model::{
assoc_edge_curve, is_sweep_path, is_sweep_profile, link_sweep_expressions, link_sweep_sources,
resweep_from_sources, scaled_sweep_path_refusal, set_surface_associativity, spatial_profile,
spatial_sweep_record, surface_associativity, surface_sweep_record, sweep_expressions,
sweep_record, sweep_refusal_code, sweep_selection_options, sweep_sources, swept_surface_entity,
swept_with_options, SWEEP_SOURCES_APP,
is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, link_sweep_sources, sweep_sources, SWEEP_SOURCES_APP, resweep_from_sources, surface_associativity, set_surface_associativity, assoc_edge_curve, assoc_source_supported, region_edge_curves, AssocCurve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
};
use crate::entities::curve::entity_curve;
@@ -36,6 +32,9 @@ pub struct Profile {
/// is handed over as the arcs it is made of rather than as one curve with
/// nowhere for the sweep to start.
pub fn profile_of(entity: &EntityType) -> Option<Profile> {
if let Some(profile) = region_loop(entity) {
return (profile.pieces.len() >= 3).then_some(profile);
}
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
if !planar.curve.is_closed() {
return None;
@@ -50,6 +49,9 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
/// An entity-level planar curve suitable for either a solid profile or an
/// open surface profile.
pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> {
if let Some(profile) = region_loop(entity) {
return Some((profile, true));
}
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
let closed = planar.curve.is_closed();
let pieces = profile_pieces(&planar.curve, closed)?;
@@ -62,6 +64,30 @@ pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> {
))
}
/// A region of one boundary loop as that loop, read from the region's exact
/// edges (its display wires hold one wire per edge), a lone closed edge split
/// as a closed curve is.
fn region_loop(entity: &EntityType) -> Option<Profile> {
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 single = boundary.len() == 1;
let pieces = boundary
.iter()
.map(|piece| profile_pieces(piece, single))
.collect::<Option<Vec<_>>>()?
.concat();
Some(Profile { plane, pieces })
}
/// Whether a region has more than one boundary loop.
fn region_with_holes(entity: &EntityType) -> bool {
matches!(entity, EntityType::Region(_)) && region_profiles(entity).is_some_and(|(_, loops)| loops.len() > 1)
}
fn profile_pieces(curve: &Curve, closed: bool) -> Option<Vec<Curve>> {
let pieces = match curve {
Curve::Polyline(_) => curve.segments(),
@@ -164,10 +190,16 @@ fn planar_polygon_entity(entity: &EntityType) -> Option<PlanarCurve> {
))
}
/// A region's boundary loops: from its modeler edges when it has them (its
/// display wires then hold one wire per edge), else from its wires.
fn region_profiles(entity: &EntityType) -> Option<(Plane, Vec<Vec<Curve>>)> {
let EntityType::Region(region) = entity else {
return None;
};
if region.acis_data.has_data() {
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
return (!loops.is_empty()).then_some((plane, loops));
}
let plane = planar_polygon_entity(entity)?.plane;
let mut profiles = Vec::with_capacity(region.wires.len());
for wire in &region.wires {
@@ -295,7 +327,7 @@ pub fn extruded_surface(
direction: [f64; 3],
taper_angle: f64,
) -> Option<Body> {
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
if region_with_holes(entity) {
return None;
}
let (profile, _) = extrusion_profile_of(entity)?;
@@ -307,7 +339,7 @@ pub fn extruded_along_path(
path: &EntityType,
taper_angle: f64,
) -> Option<Body> {
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
if region_with_holes(entity) {
if taper_angle.abs() > 1e-12 {
return None;
}
@@ -464,7 +496,7 @@ pub fn revolve_history(
if !angle.is_finite()
|| angle.abs() <= 1e-12
|| !start_angle.is_finite()
|| matches!(entity, EntityType::Region(region) if region.wires.len() > 1)
|| region_with_holes(entity)
{
return None;
}
@@ -627,10 +659,105 @@ pub fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
}
}
fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
/// A region of one boundary loop as the curve the reference records for it
/// as a profile: a circle (one edge, or arcs of one circle making a whole
/// turn) or whole ellipse as itself, a loop of lines and arcs as a closed
/// polyline in its own coordinate system about the region's normal, 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::Arc(first), rest @ ..]
if rest.iter().all(|piece| matches!(piece, Curve::Arc(arc)
if (arc.centre[0] - first.centre[0]).hypot(arc.centre[1] - first.centre[1]) <= 1e-9 * (1.0 + first.radius)
&& (arc.radius - first.radius).abs() <= 1e-9 * (1.0 + first.radius)))
&& (boundary.iter().map(|piece| match piece { Curve::Arc(arc) => arc.sweep(), _ => 0.0 }).sum::<f64>()
- std::f64::consts::TAU).abs() <= 1e-9 =>
{
let mut value = codec::entities::Circle::new();
value.center = world(plane.point_at(first.centre));
value.radius = first.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
};
// The polyline in its object coordinates: the arbitrary axes of the normal.
let axis_normal = glam::DVec3::new(normal.x, normal.y, normal.z);
let axis_x = super::sweep_command_model::arbitrary_x(axis_normal);
let axis_y = axis_normal.cross(axis_x);
let mut polyline = LwPolyline::new();
polyline.normal = normal;
polyline.elevation = glam::DVec3::from_array(plane.origin).dot(axis_normal);
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 point = glam::DVec3::from_array(plane.point_at(from));
let mut vertex = LwVertex::new(Vector2::new(point.dot(axis_x), point.dot(axis_y)));
vertex.bulge = bulge;
polyline.vertices.push(vertex);
at = to;
}
polyline.is_closed = true;
Some((EmbeddedEntity::LwPolyline(polyline), identity))
}
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()));