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

+4 -2
View File
@@ -7,9 +7,11 @@ use codec::objects::{SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryN
use codec::{EntityType, Handle};
use kernel::brep::Body;
fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
pub(crate) fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
match entity {
EntityType::Region(region) => Some(EmbeddedEntity::Region(region.clone())),
// A region as the curve round it, as the reference records it.
EntityType::Region(region) => Some(super::sweep_model::region_boundary_entity(entity)
.map_or_else(|| EmbeddedEntity::Region(region.clone()), |(curve, _)| curve)),
EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())),
_ => embedded_path(entity),
}
+1
View File
@@ -15,6 +15,7 @@ pub mod point_cloud;
pub mod presspull_model;
pub mod solid_history;
pub mod solid_model;
pub mod surface_sources;
mod sweep_command_model;
pub mod sweep_model;
pub mod underlay_vector;
+5 -2
View File
@@ -850,8 +850,11 @@ pub fn extrusion_surface_data(value: &SolidHistorySweep) -> Option<codec::entiti
sweep_entity: Some(profile),
options: SurfaceSweepOptions {
draft_angle: value.draft_angle,
draft_start_distance: value.start_draft_distance,
draft_end_distance: value.end_draft_distance,
// The reference keeps no draft distances on an extruded
// surface; a distance there reads as a taper when it
// re-extrudes the surface from a changed profile.
draft_start_distance: 0.0,
draft_end_distance: 0.0,
twist_angle: value.twist_angle,
scale_factor: value.scale_factor,
align_angle: value.align_angle,
+121
View File
@@ -0,0 +1,121 @@
//! Associative extruded, revolved and lofted surfaces (SURFACEASSOCIATIVITY):
//! the sources and values each surface was made from, kept with the surface
//! so it follows edits of its sources and the drawing's associative network
//! describes it on save.
use codec::xdata::XDataValue;
use codec::{EntityType, Handle};
use glam::DVec3;
/// The application name under which a surface keeps its sources.
pub const SURFACE_SOURCES_APP: &str = "OCS_SURFACE_SOURCES";
/// What an associative surface was made from.
#[derive(Clone, Debug, PartialEq)]
pub enum SurfaceSources {
/// EXTRUDE: the profile, the height along its normal (or the length of a
/// given direction) and the taper angle.
Extrude { profile: Handle, height: f64, direction: Option<DVec3>, taper: f64 },
/// REVOLVE: the profile, the axis (an axis object or two points), the
/// angle and the start angle.
Revolve { profile: Handle, axis: Option<Handle>, start: DVec3, end: DVec3, angle: f64, start_angle: f64 },
/// LOFT: the cross sections in order.
Loft { sections: Vec<Handle> },
}
impl SurfaceSources {
/// Every entity the surface reads.
pub fn handles(&self) -> Vec<Handle> {
match self {
SurfaceSources::Extrude { profile, .. } => vec![*profile],
SurfaceSources::Revolve { profile, axis, .. } => std::iter::once(*profile).chain(*axis).collect(),
SurfaceSources::Loft { sections } => sections.clone(),
}
}
}
/// Keep the sources with the surface.
pub fn link_surface_sources(document: &mut codec::CadDocument, surface: Handle, sources: &SurfaceSources) {
if surface.is_null() {
return;
}
let point = |p: DVec3| XDataValue::Point3D(codec::types::Vector3::new(p.x, p.y, p.z));
let values = match sources {
SurfaceSources::Extrude { profile, height, direction, taper } => {
let mut values = vec![
XDataValue::String("EXTRUDE".into()),
XDataValue::Handle(*profile),
XDataValue::Real(*height),
XDataValue::Real(*taper),
];
values.extend(direction.map(point));
values
}
SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => {
let mut values = vec![
XDataValue::String("REVOLVE".into()),
XDataValue::Handle(*profile),
point(*start),
point(*end),
XDataValue::Real(*angle),
XDataValue::Real(*start_angle),
];
values.extend(axis.map(XDataValue::Handle));
values
}
SurfaceSources::Loft { sections } => std::iter::once(XDataValue::String("LOFT".into()))
.chain(sections.iter().map(|handle| XDataValue::Handle(*handle)))
.collect(),
};
crate::scene::view::dispatch::set_entity_xdata(document, surface, SURFACE_SOURCES_APP, Some(values));
}
/// The sources an associative surface keeps.
pub fn surface_sources(entity: &EntityType) -> Option<SurfaceSources> {
let record = entity
.common()
.extended_data
.records()
.iter()
.find(|record| record.application_name == SURFACE_SOURCES_APP)?;
let mut kind = None;
let (mut handles, mut reals, mut points) = (Vec::new(), Vec::new(), Vec::new());
for value in &record.values {
match value {
XDataValue::String(text) if kind.is_none() => kind = Some(text.as_str()),
XDataValue::Handle(handle) => handles.push(*handle),
XDataValue::Real(value) => reals.push(*value),
XDataValue::Point3D(p) => points.push(DVec3::new(p.x, p.y, p.z)),
_ => {}
}
}
match kind? {
"EXTRUDE" => Some(SurfaceSources::Extrude {
profile: *handles.first()?,
height: *reals.first()?,
taper: *reals.get(1)?,
direction: points.first().copied(),
}),
"REVOLVE" => Some(SurfaceSources::Revolve {
profile: *handles.first()?,
axis: handles.get(1).copied(),
start: *points.first()?,
end: *points.get(1)?,
angle: *reals.first()?,
start_angle: *reals.get(1)?,
}),
"LOFT" => (handles.len() >= 2).then_some(SurfaceSources::Loft { sections: handles }),
_ => None,
}
}
/// The two ends of an axis object (a line).
pub fn axis_points(entity: &EntityType) -> Option<(DVec3, DVec3)> {
match entity {
EntityType::Line(line) => Some((
DVec3::new(line.start.x, line.start.y, line.start.z),
DVec3::new(line.end.x, line.end.y, line.end.z),
)),
_ => None,
}
}
+149 -20
View File
@@ -14,13 +14,11 @@ 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(),
)),
EntityType::Polyline3D(value) => {
Some((polyline_wire(value)?, 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),
}
}
@@ -580,13 +578,7 @@ pub fn resweep_from_sources(
/// parameter: line segment (23: start, vector to the end), arc (11:
/// centre, normal, reference axis, radius, start and end angle, 0) or a
/// composite of those (47: count, then each part's type and values).
pub fn assoc_edge_curve(
entity: &EntityType,
) -> Option<(
i32,
codec::objects::AssocSubcurveKind,
Vec<codec::objects::AssocCurveValue>,
)> {
pub fn assoc_edge_curve(entity: &EntityType) -> Option<AssocCurve> {
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
use codec::types::Vector3 as P;
let point = |p: glam::DVec3| V::Point(P::new(p.x, p.y, p.z));
@@ -729,17 +721,154 @@ pub fn assoc_edge_curve(
}
Some((47, K::None, values))
}
// Ellipse (17): centre, unit major axis, unit minor axis (the normal
// across the major one), major and minor radius, start and end
// parameter, 0.
EntityType::Ellipse(ellipse) => {
let major = glam::DVec3::new(ellipse.major_axis.x, ellipse.major_axis.y, ellipse.major_axis.z);
let normal = glam::DVec3::new(ellipse.normal.x, ellipse.normal.y, ellipse.normal.z).try_normalize()?;
let radius = major.length();
let axis = major.try_normalize()?;
Some((17, K::Ellipse, vec![
point(glam::DVec3::new(ellipse.center.x, ellipse.center.y, ellipse.center.z)),
point(axis),
point(normal.cross(axis)),
V::Real(radius),
V::Real(radius * ellipse.minor_axis_ratio),
V::Real(ellipse.start_parameter),
V::Real(ellipse.end_parameter),
V::Real(0.0),
]))
}
// NURBS (42): rational flag, a second flag (0), degree, knot
// tolerance, then knots, weights (empty unless rational) and control
// points, each as its length, the length reserved (at least 8) and the
// growth step 8, followed by the items.
EntityType::Spline(spline) => {
// A fit-point spline as the curve through its points, its knots
// in the fit parameter (chord length) as the reference keeps them.
let curve = crate::entities::spline::nurbs3(spline)?;
let mut knots = curve.knots().to_vec();
if crate::entities::spline::uses_fit_method(spline) && spline.knot_parameterization == 0 {
let chord: f64 = spline.fit_points.windows(2)
.map(|pair| ((pair[1].x - pair[0].x).powi(2) + (pair[1].y - pair[0].y).powi(2) + (pair[1].z - pair[0].z).powi(2)).sqrt())
.sum();
let (low, high) = (knots[0], knots[knots.len() - 1]);
if high - low > 0.0 && chord > 0.0 && (high - low - chord).abs() > 1e-9 * chord {
knots.iter_mut().for_each(|knot| *knot = (*knot - low) / (high - low) * chord);
}
}
let points = curve.control_points();
if points.len() < 2 || knots.is_empty() { return None; }
let tolerance = if spline.knot_tolerance > 0.0 { spline.knot_tolerance } else { 1e-9 };
let points = points.iter().map(|p| glam::DVec3::from_array(*p)).collect::<Vec<_>>();
Some(nurbs_curve(curve.degree(), tolerance, &knots, curve.weights(), &points))
}
_ => None,
}
}
/// The arbitrary-axis X direction of an entity normal.
fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 {
let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 {
glam::DVec3::Y
} else {
glam::DVec3::Z
/// A NURBS curve's edge parameter values (42): rational flag, a second flag
/// (0), degree, knot tolerance, then knots, weights (empty unless rational)
/// and control points, each as its length, the length reserved (at least
/// 8) and the growth step 8, followed by the items.
fn nurbs_curve(degree: usize, tolerance: f64, knots: &[f64], weights: &[f64], points: &[glam::DVec3]) -> AssocCurve {
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
let rational = weights.iter().any(|weight| (weight - 1.0).abs() > 1e-12);
let header = |values: &mut Vec<V>, count: usize| {
values.extend([V::Int(count as i32), V::Int(if count == 0 { 0 } else { count.max(8) } as i32), V::Int(8)]);
};
let mut values = vec![V::Bool(rational), V::Bool(false), V::Int(degree as i32), V::Real(tolerance)];
header(&mut values, knots.len());
values.extend(knots.iter().map(|knot| V::Real(*knot)));
let weights = if rational { weights } else { &[] };
header(&mut values, weights.len());
values.extend(weights.iter().map(|weight| V::Real(*weight)));
header(&mut values, points.len());
values.extend(points.iter().map(|p| V::Point(codec::types::Vector3::new(p.x, p.y, p.z))));
(42, K::Nurb3d, values)
}
/// An edge parameter's curve: kind, subcurve kind and values.
pub type AssocCurve = (i32, codec::objects::AssocSubcurveKind, Vec<codec::objects::AssocCurveValue>);
/// A region's edges as an associative action reads them, loop by loop, each
/// loop from its second edge on as the reference lists them: lines (23),
/// circles and arcs (11) measured from the arbitrary axis of the region's
/// normal, elliptical arcs (17) and NURBS curves (42).
pub fn region_edge_curves(entity: &EntityType) -> Option<Vec<Vec<AssocCurve>>> {
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
use kernel::geom2d::Curve;
let EntityType::Region(_) = entity else { return None };
let Some((plane, loops, true)) = super::presspull_model::profile_geometry(entity) else { return None };
let at = |uv: [f64; 2]| glam::DVec3::from_array(plane.point_at(uv));
let normal = glam::DVec3::from_array(plane.x_axis).cross(glam::DVec3::from_array(plane.y_axis)).try_normalize()?;
let axis = arbitrary_x(normal);
let across = normal.cross(axis);
let point = |p: glam::DVec3| V::Point(codec::types::Vector3::new(p.x, p.y, p.z));
let arc = |centre: glam::DVec3, radius: f64, start: f64, end: f64| (11, K::Arc, vec![
point(centre), point(normal), point(axis), V::Real(radius), V::Real(start), V::Real(end), V::Real(0.0),
]);
let curve = |piece: &Curve| -> Option<AssocCurve> {
match piece {
Curve::Line(line) => {
let (a, b) = (at(line.start), at(line.end));
Some((23, K::LineSegment3d, vec![point(a), point(b - a)]))
}
Curve::Circle(circle) => Some(arc(at(circle.centre), circle.radius, 0.0, std::f64::consts::TAU)),
Curve::Arc(value) => {
let centre = at(value.centre);
let offset = at([
value.centre[0] + value.start_angle.cos(),
value.centre[1] + value.start_angle.sin(),
]) - centre;
let start = offset.dot(across).atan2(offset.dot(axis));
Some(arc(centre, value.radius, start, start + value.sweep()))
}
Curve::Ellipse(value) => {
let ellipse = value.ellipse;
let centre = at(ellipse.centre);
let tip = [ellipse.centre[0] + ellipse.major_axis[0], ellipse.centre[1] + ellipse.major_axis[1]];
let major = (at(tip) - centre).try_normalize()?;
let end = if value.end_parameter > value.start_parameter {
value.end_parameter
} else {
value.end_parameter + std::f64::consts::TAU
};
Some((17, K::Ellipse, vec![
point(centre), point(major), point(normal.cross(major)),
V::Real(ellipse.major_radius), V::Real(ellipse.minor_radius),
V::Real(value.start_parameter), V::Real(end), V::Real(0.0),
]))
}
Curve::Nurbs(value) => {
let points = value.control_points().iter().map(|p| at(*p)).collect::<Vec<_>>();
Some(nurbs_curve(value.degree(), 1e-9, value.knots(), value.weights(), &points))
}
_ => None,
}
};
loops
.iter()
.map(|pieces| {
let mut curves = pieces.iter().map(curve).collect::<Option<Vec<_>>>()?;
if curves.len() > 1 {
curves.rotate_left(1);
}
Some(curves)
})
.collect()
}
/// Whether an associative action can read this source: a region's edges
/// or the entity's own curve.
pub fn assoc_source_supported(entity: &EntityType) -> bool {
region_edge_curves(entity).is_some() || assoc_edge_curve(entity).is_some()
}
/// The arbitrary-axis X direction of an entity normal.
pub(crate) fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 {
let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 { glam::DVec3::Y } else { glam::DVec3::Z };
world.cross(normal).normalize()
}
+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()));