forked from 1b-agent/opencadstudio-upstream
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:
commit
5bb0efd93e
12 files changed
+1106
-228
No files matched your search
+137
-10
@@ -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 ®ion.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()));
|
||||
|
||||
Reference in new issue
Block a user