forked from 1b-agent/opencadstudio-upstream
963 lines
33 KiB
Rust
963 lines
33 KiB
Rust
// Exact profile sweeps stored as kernel B-reps and ACIS.
|
|
|
|
use codec::entities::{EmbeddedEntity, LwPolyline, LwVertex, Spline};
|
|
use codec::objects::{
|
|
SolidHistoryLoft, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryRevolve,
|
|
SolidHistorySweep,
|
|
};
|
|
use codec::types::{Vector2, Vector3};
|
|
use codec::EntityType;
|
|
use kernel::brep::{self, Body};
|
|
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,
|
|
};
|
|
use crate::entities::curve::entity_curve;
|
|
|
|
/// A drawn profile, as the kernel wants it: the plane it lies in and the
|
|
/// chain of pieces closing a loop in that plane.
|
|
pub struct Profile {
|
|
pub plane: Plane,
|
|
pub pieces: Vec<Curve>,
|
|
}
|
|
|
|
/// The closed profile an entity describes, or `None` when it does not
|
|
/// describe one.
|
|
///
|
|
/// A single closed curve — a circle, an ellipse, a closed polyline — is what
|
|
/// a sweep needs. The kernel wants it as a chain of at least three pieces,
|
|
/// which a polyline already is and which a circle is not, so a round 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> {
|
|
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
|
|
if !planar.curve.is_closed() {
|
|
return None;
|
|
}
|
|
let pieces = profile_pieces(&planar.curve, true)?;
|
|
(pieces.len() >= 3).then_some(Profile {
|
|
plane: planar.plane,
|
|
pieces,
|
|
})
|
|
}
|
|
|
|
/// 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)> {
|
|
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
|
|
let closed = planar.curve.is_closed();
|
|
let pieces = profile_pieces(&planar.curve, closed)?;
|
|
(!pieces.is_empty()).then_some((
|
|
Profile {
|
|
plane: planar.plane,
|
|
pieces,
|
|
},
|
|
closed,
|
|
))
|
|
}
|
|
|
|
fn profile_pieces(curve: &Curve, closed: bool) -> Option<Vec<Curve>> {
|
|
let pieces = match curve {
|
|
Curve::Polyline(_) => curve.segments(),
|
|
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
|
|
Curve::Arc(arc) if closed => split_arc(*arc),
|
|
Curve::Arc(arc) => vec![Curve::Arc(*arc)],
|
|
Curve::Ellipse(arc) if closed => split_ellipse(*arc),
|
|
Curve::Ellipse(arc) => vec![Curve::Ellipse(*arc)],
|
|
Curve::Line(line) => vec![Curve::Line(*line)],
|
|
Curve::Nurbs(nurbs) if closed => (0..4)
|
|
.map(|part| {
|
|
nurbs
|
|
.trimmed(part as f64 / 4.0, (part + 1) as f64 / 4.0)
|
|
.map(Curve::Nurbs)
|
|
})
|
|
.collect::<Option<Vec<_>>>()?,
|
|
Curve::Nurbs(nurbs) => vec![Curve::Nurbs(nurbs.clone())],
|
|
_ => return None,
|
|
};
|
|
Some(pieces)
|
|
}
|
|
|
|
fn planar_polygon_entity(entity: &EntityType) -> Option<PlanarCurve> {
|
|
let (points, closed) = match entity {
|
|
EntityType::Polyline3D(value) => (
|
|
value
|
|
.vertices
|
|
.iter()
|
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
|
.collect::<Vec<_>>(),
|
|
value.is_closed(),
|
|
),
|
|
EntityType::Face3D(value) => (
|
|
value
|
|
.corners()
|
|
.into_iter()
|
|
.map(|point| [point.x, point.y, point.z])
|
|
.collect::<Vec<_>>(),
|
|
true,
|
|
),
|
|
EntityType::Solid(value) => (
|
|
value
|
|
.boundary_corners()
|
|
.into_iter()
|
|
.map(|point| [point.x, point.y, point.z])
|
|
.collect::<Vec<_>>(),
|
|
true,
|
|
),
|
|
EntityType::Region(value) => (
|
|
value
|
|
.wires
|
|
.first()?
|
|
.points
|
|
.iter()
|
|
.map(|point| [point.x, point.y, point.z])
|
|
.collect::<Vec<_>>(),
|
|
true,
|
|
),
|
|
_ => return None,
|
|
};
|
|
let mut points = points;
|
|
if closed && points.len() > 2 && points.first() == points.last() {
|
|
points.pop();
|
|
}
|
|
if points.len() < if closed { 3 } else { 2 } {
|
|
return None;
|
|
}
|
|
let origin = Vec3::from(points[0]);
|
|
let first = Vec3::from(points[1]) - origin;
|
|
let normal = points[2..]
|
|
.iter()
|
|
.find_map(|point| first.cross(Vec3::from(*point) - origin).normalize())
|
|
.or_else(|| (!closed).then(|| first.cross(Vec3::Z).normalize().unwrap_or(Vec3::Y)))?;
|
|
let plane = Plane::orthonormal(points[0], first.to_array(), normal.to_array())?;
|
|
let scale = points
|
|
.iter()
|
|
.flatten()
|
|
.map(|value| value.abs())
|
|
.fold(1.0_f64, f64::max);
|
|
if points
|
|
.iter()
|
|
.any(|point| !plane.contains(*point, scale * 1e-9))
|
|
{
|
|
return None;
|
|
}
|
|
let vertices = points
|
|
.iter()
|
|
.map(|point| {
|
|
plane
|
|
.project(*point)
|
|
.map(|position| kernel::geom2d::PolylineVertex {
|
|
position,
|
|
bulge: 0.0,
|
|
})
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
Some(PlanarCurve::new(
|
|
plane,
|
|
Curve::Polyline(kernel::geom2d::Polyline { vertices, closed }),
|
|
))
|
|
}
|
|
|
|
fn region_profiles(entity: &EntityType) -> Option<(Plane, Vec<Vec<Curve>>)> {
|
|
let EntityType::Region(region) = entity else {
|
|
return None;
|
|
};
|
|
let plane = planar_polygon_entity(entity)?.plane;
|
|
let mut profiles = Vec::with_capacity(region.wires.len());
|
|
for wire in ®ion.wires {
|
|
let mut points = wire
|
|
.points
|
|
.iter()
|
|
.map(|point| [point.x, point.y, point.z])
|
|
.collect::<Vec<_>>();
|
|
if points.len() > 2 && points.first() == points.last() {
|
|
points.pop();
|
|
}
|
|
if points.len() < 3 {
|
|
return None;
|
|
}
|
|
let scale = points
|
|
.iter()
|
|
.flatten()
|
|
.map(|value| value.abs())
|
|
.fold(1.0_f64, f64::max);
|
|
if points
|
|
.iter()
|
|
.any(|point| !plane.contains(*point, scale * 1e-9))
|
|
{
|
|
return None;
|
|
}
|
|
let vertices = points
|
|
.iter()
|
|
.map(|point| {
|
|
plane
|
|
.project(*point)
|
|
.map(|position| kernel::geom2d::PolylineVertex {
|
|
position,
|
|
bulge: 0.0,
|
|
})
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
profiles.push(
|
|
Curve::Polyline(kernel::geom2d::Polyline {
|
|
vertices,
|
|
closed: true,
|
|
})
|
|
.segments(),
|
|
);
|
|
}
|
|
(!profiles.is_empty()).then_some((plane, profiles))
|
|
}
|
|
|
|
/// A circle as four arcs.
|
|
fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
|
|
use std::f64::consts::FRAC_PI_2;
|
|
(0..4)
|
|
.map(|quarter| {
|
|
let start = FRAC_PI_2 * quarter as f64;
|
|
Curve::Arc(Arc {
|
|
centre,
|
|
radius,
|
|
start_angle: start,
|
|
end_angle: start + FRAC_PI_2,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn split_arc(arc: Arc) -> Vec<Curve> {
|
|
let start = arc.start_angle;
|
|
let step = arc.sweep() / 4.0;
|
|
(0..4)
|
|
.map(|part| {
|
|
Curve::Arc(Arc {
|
|
start_angle: start + step * part as f64,
|
|
end_angle: start + step * (part + 1) as f64,
|
|
..arc
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn split_ellipse(arc: EllipseArc) -> Vec<Curve> {
|
|
let start = arc.start_parameter;
|
|
let step = arc.sweep() / 4.0;
|
|
(0..4)
|
|
.map(|part| {
|
|
Curve::Ellipse(EllipseArc {
|
|
ellipse: arc.ellipse,
|
|
start_parameter: start + step * part as f64,
|
|
end_parameter: start + step * (part + 1) as f64,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// EXTRUDE: drag the profile `height` along its own plane's normal.
|
|
///
|
|
/// `None` for a profile that does not close, encloses nothing, or holds a
|
|
/// piece with no analytic side wall.
|
|
pub fn extruded(entity: &EntityType, height: f64) -> Option<Body> {
|
|
let profile = profile_of(entity)?;
|
|
let normal = profile.plane.normal()?;
|
|
extruded_direction(
|
|
entity,
|
|
[normal[0] * height, normal[1] * height, normal[2] * height],
|
|
0.0,
|
|
)
|
|
}
|
|
|
|
pub fn extruded_direction(
|
|
entity: &EntityType,
|
|
direction: [f64; 3],
|
|
taper_angle: f64,
|
|
) -> Option<Body> {
|
|
if let Some((plane, profiles)) =
|
|
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
|
|
{
|
|
if taper_angle.abs() > 1e-12 {
|
|
return None;
|
|
}
|
|
return brep::extrude_region(plane, &profiles, direction);
|
|
}
|
|
let profile = profile_of(entity)?;
|
|
brep::extrude_tapered(profile.plane, &profile.pieces, direction, taper_angle)
|
|
}
|
|
|
|
pub fn extruded_surface(
|
|
entity: &EntityType,
|
|
direction: [f64; 3],
|
|
taper_angle: f64,
|
|
) -> Option<Body> {
|
|
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
|
|
return None;
|
|
}
|
|
let (profile, _) = extrusion_profile_of(entity)?;
|
|
brep::extrude_surface_tapered(profile.plane, &profile.pieces, direction, taper_angle)
|
|
}
|
|
|
|
pub fn extruded_along_path(
|
|
entity: &EntityType,
|
|
path: &EntityType,
|
|
taper_angle: f64,
|
|
) -> Option<Body> {
|
|
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
|
|
if taper_angle.abs() > 1e-12 {
|
|
return None;
|
|
}
|
|
return extruded_direction(entity, straight_path_direction(path)?, 0.0);
|
|
}
|
|
let profile = profile_of(entity)?;
|
|
if let EntityType::Polyline3D(path) = path {
|
|
let points = path
|
|
.vertices
|
|
.iter()
|
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
|
.collect::<Vec<_>>();
|
|
if taper_angle.abs() > 1e-12 {
|
|
if points.len() != 2 {
|
|
return None;
|
|
}
|
|
return brep::extrude_tapered(
|
|
profile.plane,
|
|
&profile.pieces,
|
|
(Vec3::from(points[1]) - Vec3::from(points[0])).to_array(),
|
|
taper_angle,
|
|
);
|
|
}
|
|
return brep::sweep_along_polyline3d(profile.plane, &profile.pieces, &points);
|
|
}
|
|
if taper_angle.abs() <= 1e-12 {
|
|
return swept(entity, path);
|
|
}
|
|
let curve = entity_curve(path)?;
|
|
let Curve::Line(line) = curve.curve else {
|
|
return None;
|
|
};
|
|
let direction =
|
|
Vec3::from(curve.plane.point_at(line.end)) - Vec3::from(curve.plane.point_at(line.start));
|
|
brep::extrude_tapered(
|
|
profile.plane,
|
|
&profile.pieces,
|
|
direction.to_array(),
|
|
taper_angle,
|
|
)
|
|
}
|
|
|
|
pub fn straight_path_direction(path: &EntityType) -> Option<[f64; 3]> {
|
|
if let EntityType::Polyline3D(path) = path {
|
|
if path.vertices.len() != 2 {
|
|
return None;
|
|
}
|
|
let start = &path.vertices[0].position;
|
|
let end = &path.vertices[1].position;
|
|
return Some([end.x - start.x, end.y - start.y, end.z - start.z]);
|
|
}
|
|
let curve = entity_curve(path)?;
|
|
let Curve::Line(line) = curve.curve else {
|
|
return None;
|
|
};
|
|
Some(
|
|
(Vec3::from(curve.plane.point_at(line.end)) - Vec3::from(curve.plane.point_at(line.start)))
|
|
.to_array(),
|
|
)
|
|
}
|
|
|
|
/// Signed distance of a drag along a profile's normal.
|
|
pub fn projected_drag(entity: &EntityType, from: glam::DVec3, to: glam::DVec3) -> Option<f64> {
|
|
let normal = entity_curve(entity)?.plane.normal()?;
|
|
let distance = (to - from).dot(glam::DVec3::from_array(normal));
|
|
(distance.is_finite() && distance.abs() > 1e-6).then_some(distance)
|
|
}
|
|
|
|
/// REVOLVE: turn the profile about the axis from `from` to `to` by `angle`
|
|
/// radians.
|
|
///
|
|
/// The axis has to lie in the profile's plane — a profile and an axis that do
|
|
/// not share one sweep into surfaces with no analytic form, and the kernel
|
|
/// refuses rather than approximating them.
|
|
pub fn revolved(
|
|
entity: &EntityType,
|
|
from: [f64; 3],
|
|
to: [f64; 3],
|
|
angle: f64,
|
|
start_angle: f64,
|
|
) -> Option<Body> {
|
|
let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
|
|
let body = if let Some((plane, profiles)) =
|
|
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
|
|
{
|
|
brep::revolve_region(plane, &profiles, from, axis, angle)?
|
|
} else {
|
|
let profile = profile_of(entity)?;
|
|
brep::revolve(profile.plane, &profile.pieces, from, axis, angle)?
|
|
};
|
|
turn_revolved_body(body, from, axis, start_angle)
|
|
}
|
|
|
|
/// REVOLVE sheet mode for an open or closed planar profile.
|
|
pub fn revolved_surface(
|
|
entity: &EntityType,
|
|
from: [f64; 3],
|
|
to: [f64; 3],
|
|
angle: f64,
|
|
start_angle: f64,
|
|
) -> Option<Body> {
|
|
let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
|
|
let body = if let Some((plane, profiles)) =
|
|
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
|
|
{
|
|
brep::revolve_surface_region(plane, &profiles, from, axis, angle)?
|
|
} else {
|
|
let (profile, _) = extrusion_profile_of(entity)?;
|
|
brep::revolve_surface(profile.plane, &profile.pieces, from, axis, angle)?
|
|
};
|
|
turn_revolved_body(body, from, axis, start_angle)
|
|
}
|
|
|
|
fn turn_revolved_body(body: Body, pivot: [f64; 3], axis: [f64; 3], angle: f64) -> Option<Body> {
|
|
if !angle.is_finite() {
|
|
return None;
|
|
}
|
|
if angle.abs() <= 1e-12 {
|
|
return Some(body);
|
|
}
|
|
let axis = Vec3::from(axis).normalize()?;
|
|
let rotate = |vector: Vec3| {
|
|
let (sin, cos) = angle.sin_cos();
|
|
vector * cos + axis.cross(vector) * sin + axis * axis.dot(vector) * (1.0 - cos)
|
|
};
|
|
let pivot = Vec3::from(pivot);
|
|
let placement = brep::Placement {
|
|
x_axis: rotate(Vec3::X).to_array(),
|
|
y_axis: rotate(Vec3::Y).to_array(),
|
|
z_axis: rotate(Vec3::Z).to_array(),
|
|
origin: (pivot - rotate(pivot)).to_array(),
|
|
};
|
|
brep::transform(&body, &placement)
|
|
}
|
|
|
|
fn profile_center(profile: &Profile) -> Option<Vec3> {
|
|
let count = profile.pieces.len();
|
|
let sum = profile.pieces.iter().try_fold(Vec3::ZERO, |sum, piece| {
|
|
let point = Vec3::from(profile.plane.point_at(piece.point_at(0.0)));
|
|
point.is_finite().then_some(sum + point)
|
|
})?;
|
|
(count > 0).then_some(sum / count as f64)
|
|
}
|
|
|
|
/// Stores the source profile and canonical axis required to rebuild and edit
|
|
/// a revolved solid.
|
|
pub fn revolve_history(
|
|
entity: &EntityType,
|
|
from: [f64; 3],
|
|
to: [f64; 3],
|
|
angle: f64,
|
|
start_angle: f64,
|
|
) -> Option<SolidHistoryOperation> {
|
|
if !angle.is_finite()
|
|
|| angle.abs() <= 1e-12
|
|
|| !start_angle.is_finite()
|
|
|| matches!(entity, EntityType::Region(region) if region.wires.len() > 1)
|
|
{
|
|
return None;
|
|
}
|
|
let profile = profile_of(entity)?;
|
|
let from = Vec3::from(from);
|
|
let direction = (Vec3::from(to) - from).normalize()?;
|
|
let center = profile_center(&profile)?;
|
|
let axis_point = from + direction * (center - from).dot(direction);
|
|
let (sweep_entity, transform) = embedded_planar_entity(entity)?;
|
|
let transform = glam::DMat4::from_cols_array(&transform);
|
|
let inverse = transform.inverse();
|
|
if !transform.is_finite() || !inverse.is_finite() || transform.determinant().abs() <= 1e-12 {
|
|
return None;
|
|
}
|
|
let axis_point = inverse.transform_point3(glam::DVec3::from_array(axis_point.to_array()));
|
|
let direction = inverse
|
|
.transform_vector3(glam::DVec3::from_array(direction.to_array()))
|
|
.try_normalize()?;
|
|
let mut base = SolidHistoryNodeBase::new(1);
|
|
base.transform = transform.to_cols_array();
|
|
Some(SolidHistoryOperation::Revolve(SolidHistoryRevolve {
|
|
base,
|
|
operation_major: 1,
|
|
axis_point: Vector3::new(axis_point.x, axis_point.y, axis_point.z),
|
|
direction: Vector3::new(direction.x, direction.y, direction.z),
|
|
revolve_angle: angle,
|
|
start_angle,
|
|
flag_290: true,
|
|
sweep_entity: Some(sweep_entity),
|
|
..SolidHistoryRevolve::default()
|
|
}))
|
|
}
|
|
|
|
/// SWEEP as an exact B-rep along straight and circular path pieces.
|
|
pub fn swept(profile: &EntityType, path: &EntityType) -> Option<Body> {
|
|
let profile = profile_of(profile)?;
|
|
let mut path = entity_curve(path)?;
|
|
let pieces = match &path.curve {
|
|
Curve::Line(line) => vec![Curve::Line(*line)],
|
|
Curve::Arc(arc) => vec![Curve::Arc(*arc)],
|
|
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
|
|
Curve::Polyline(_) => path.curve.segments(),
|
|
Curve::Ellipse(_) | Curve::Nurbs(_) => {
|
|
let scale = path
|
|
.curve
|
|
.point_at(0.0)
|
|
.into_iter()
|
|
.chain(path.curve.point_at(1.0))
|
|
.map(f64::abs)
|
|
.fold(1.0_f64, f64::max);
|
|
path.curve
|
|
.tessellate_within(scale * 1e-4)
|
|
.windows(2)
|
|
.filter(|pair| pair[0] != pair[1])
|
|
.map(|pair| {
|
|
Curve::Line(Line {
|
|
start: pair[0],
|
|
end: pair[1],
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
_ => return None,
|
|
};
|
|
let profile_center = profile
|
|
.pieces
|
|
.iter()
|
|
.map(|piece| Vec3::from(profile.plane.point_at(piece.point_at(0.0))))
|
|
.fold(Vec3::ZERO, |sum, point| sum + point)
|
|
/ profile.pieces.len() as f64;
|
|
let path_start = Vec3::from(path.plane.point_at(path.curve.point_at(0.0)));
|
|
path.plane.origin = (Vec3::from(path.plane.origin) + profile_center - path_start).to_array();
|
|
brep::sweep_along(profile.plane, &profile.pieces, path.plane, &pieces)
|
|
}
|
|
|
|
/// LOFT as an exact B-rep through compatible closed sections.
|
|
pub fn lofted(profiles: &[EntityType]) -> Option<Body> {
|
|
if let Some(body) = circular_loft(profiles) {
|
|
return Some(body);
|
|
}
|
|
let sections = profiles
|
|
.iter()
|
|
.map(|entity| {
|
|
let profile = profile_of(entity)?;
|
|
Some((profile.plane, profile.pieces))
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
brep::loft(§ions)
|
|
}
|
|
|
|
/// Stores the source sections required to rebuild and edit a loft.
|
|
pub fn loft_history(profiles: &[EntityType]) -> Option<SolidHistoryOperation> {
|
|
let cross_sections = profiles
|
|
.iter()
|
|
.map(embedded_path)
|
|
.collect::<Option<Vec<_>>>()?;
|
|
if cross_sections.len() < 2 {
|
|
return None;
|
|
}
|
|
let mut base = SolidHistoryNodeBase::new(1);
|
|
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
|
Some(SolidHistoryOperation::Loft(SolidHistoryLoft {
|
|
base,
|
|
operation_major: 1,
|
|
cross_sections,
|
|
guides: Vec::new(),
|
|
..SolidHistoryLoft::default()
|
|
}))
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum PolysolidJustification {
|
|
Left,
|
|
Center,
|
|
Right,
|
|
}
|
|
|
|
pub fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
|
|
match entity {
|
|
EntityType::Line(value) => Some(EmbeddedEntity::Line(value.clone())),
|
|
EntityType::Arc(value) => Some(EmbeddedEntity::Arc(value.clone())),
|
|
EntityType::Circle(value) => Some(EmbeddedEntity::Circle(value.clone())),
|
|
EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())),
|
|
EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())),
|
|
EntityType::Polyline2D(value) => {
|
|
let mut polyline = LwPolyline::new();
|
|
polyline.elevation = value.elevation;
|
|
polyline.normal = value.normal;
|
|
polyline.is_closed = value.is_closed();
|
|
polyline.vertices = value
|
|
.vertices
|
|
.iter()
|
|
.map(|vertex| {
|
|
let mut converted =
|
|
LwVertex::new(Vector2::new(vertex.location.x, vertex.location.y));
|
|
converted.bulge = vertex.bulge;
|
|
converted
|
|
})
|
|
.collect();
|
|
Some(EmbeddedEntity::LwPolyline(polyline))
|
|
}
|
|
EntityType::Spline(value) => Some(EmbeddedEntity::Spline(value.clone())),
|
|
EntityType::Helix(value) => Some(EmbeddedEntity::Spline(value.spline.clone())),
|
|
EntityType::Polyline3D(value) if value.vertices.len() >= 2 => {
|
|
let mut points = value
|
|
.vertices
|
|
.iter()
|
|
.map(|vertex| vertex.position)
|
|
.collect::<Vec<_>>();
|
|
if value.is_closed() && points.first() != points.last() {
|
|
points.push(*points.first()?);
|
|
}
|
|
let mut spline = Spline::from_control_points(1, points);
|
|
spline.flags.linear = true;
|
|
spline.flags.planar = false;
|
|
spline.flags.closed = value.is_closed();
|
|
Some(EmbeddedEntity::Spline(spline))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
|
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
|
|
return None;
|
|
}
|
|
if !matches!(entity, EntityType::Polyline3D(_)) {
|
|
if let Some(embedded) = embedded_path(entity) {
|
|
return Some((embedded, glam::DMat4::IDENTITY.to_cols_array()));
|
|
}
|
|
}
|
|
let (profile, closed) = extrusion_profile_of(entity)?;
|
|
let mut polyline = LwPolyline::new();
|
|
for piece in &profile.pieces {
|
|
let Curve::Line(line) = piece else {
|
|
return None;
|
|
};
|
|
polyline
|
|
.vertices
|
|
.push(LwVertex::new(Vector2::new(line.start[0], line.start[1])));
|
|
}
|
|
if !closed {
|
|
let Curve::Line(last) = profile.pieces.last()? else {
|
|
return None;
|
|
};
|
|
polyline
|
|
.vertices
|
|
.push(LwVertex::new(Vector2::new(last.end[0], last.end[1])));
|
|
}
|
|
polyline.is_closed = closed;
|
|
let normal = profile.plane.normal()?;
|
|
let transform = glam::DMat4::from_cols(
|
|
glam::DVec4::new(
|
|
profile.plane.x_axis[0],
|
|
profile.plane.x_axis[1],
|
|
profile.plane.x_axis[2],
|
|
0.0,
|
|
),
|
|
glam::DVec4::new(
|
|
profile.plane.y_axis[0],
|
|
profile.plane.y_axis[1],
|
|
profile.plane.y_axis[2],
|
|
0.0,
|
|
),
|
|
glam::DVec4::new(normal[0], normal[1], normal[2], 0.0),
|
|
glam::DVec4::new(
|
|
profile.plane.origin[0],
|
|
profile.plane.origin[1],
|
|
profile.plane.origin[2],
|
|
1.0,
|
|
),
|
|
)
|
|
.to_cols_array();
|
|
Some((EmbeddedEntity::LwPolyline(polyline), transform))
|
|
}
|
|
|
|
pub fn embedded_revolve_profile(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
|
embedded_planar_entity(entity)
|
|
}
|
|
|
|
pub fn extrusion_history(
|
|
profile: &EntityType,
|
|
path: Option<&EntityType>,
|
|
direction: [f64; 3],
|
|
taper_angle: f64,
|
|
reference_point: [f64; 3],
|
|
) -> Option<SolidHistoryOperation> {
|
|
let signed_height = Vec3::from(direction).length();
|
|
let mut base = SolidHistoryNodeBase::new(1);
|
|
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
|
let (sweep_entity, sweep_entity_transform) = embedded_planar_entity(profile)?;
|
|
let (path_entity, path_entity_transform) = match path {
|
|
Some(path) => {
|
|
let (entity, transform) = embedded_planar_entity(path)?;
|
|
(Some(entity), transform)
|
|
}
|
|
None => (None, glam::DMat4::IDENTITY.to_cols_array()),
|
|
};
|
|
Some(SolidHistoryOperation::Extrusion(SolidHistorySweep {
|
|
base,
|
|
operation_major: 1,
|
|
direction: Vector3::new(direction[0], direction[1], direction[2]),
|
|
sweep_entity: Some(sweep_entity),
|
|
path_entity,
|
|
draft_angle: taper_angle,
|
|
start_draft_distance: 0.0,
|
|
end_draft_distance: signed_height,
|
|
scale_factor: 1.0,
|
|
sweep_entity_transform,
|
|
path_entity_transform,
|
|
reference_point: Vector3::new(reference_point[0], reference_point[1], reference_point[2]),
|
|
..SolidHistorySweep::default()
|
|
}))
|
|
}
|
|
|
|
pub fn sweep_history(
|
|
profile: &EntityType,
|
|
path: &EntityType,
|
|
taper_angle: f64,
|
|
reference_point: [f64; 3],
|
|
) -> Option<SolidHistoryOperation> {
|
|
let mut base = SolidHistoryNodeBase::new(1);
|
|
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
|
let (sweep_entity, sweep_entity_transform) = embedded_planar_entity(profile)?;
|
|
Some(SolidHistoryOperation::Sweep(SolidHistorySweep {
|
|
base,
|
|
operation_major: 1,
|
|
sweep_entity: Some(sweep_entity),
|
|
path_entity: Some(embedded_path(path)?),
|
|
draft_angle: taper_angle,
|
|
scale_factor: 1.0,
|
|
sweep_entity_transform,
|
|
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
|
reference_point: Vector3::new(reference_point[0], reference_point[1], reference_point[2]),
|
|
..SolidHistorySweep::default()
|
|
}))
|
|
}
|
|
|
|
/// Builds a history-backed wall solid along a supported planar curve.
|
|
pub fn polysolid(
|
|
entity: &EntityType,
|
|
width: f64,
|
|
height: f64,
|
|
justification: PolysolidJustification,
|
|
) -> Option<(Body, SolidHistoryOperation)> {
|
|
if !width.is_finite() || !height.is_finite() || width <= 0.0 || height.abs() <= 1e-12 {
|
|
return None;
|
|
}
|
|
let planar = entity_curve(entity)?;
|
|
let start_local = planar.curve.point_at(0.0);
|
|
let tangent_local = Vec3::from([
|
|
planar.curve.tangent_at(0.0)[0],
|
|
planar.curve.tangent_at(0.0)[1],
|
|
0.0,
|
|
])
|
|
.normalize()?;
|
|
let start = Vec3::from(planar.plane.point_at(start_local));
|
|
let tangent = (Vec3::from(planar.plane.x_axis) * tangent_local.x
|
|
+ Vec3::from(planar.plane.y_axis) * tangent_local.y)
|
|
.normalize()?;
|
|
let normal = Vec3::from(planar.plane.normal()?).normalize()?;
|
|
let side = normal.cross(tangent).normalize()?;
|
|
let offset = match justification {
|
|
PolysolidJustification::Left => 0.0,
|
|
PolysolidJustification::Center => -width * 0.5,
|
|
PolysolidJustification::Right => -width,
|
|
};
|
|
let profile_origin = start + side * offset;
|
|
let profile_plane = crate::command::WorkingPlane::new(
|
|
glam::DVec3::from_array(profile_origin.to_array()),
|
|
glam::DVec3::from_array(side.to_array()),
|
|
glam::DVec3::from_array(normal.to_array()),
|
|
);
|
|
let mut profile = LwPolyline::new();
|
|
profile.vertices = [[0.0, 0.0], [width, 0.0], [width, height], [0.0, height]]
|
|
.into_iter()
|
|
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
|
|
.collect();
|
|
profile.is_closed = true;
|
|
let EntityType::LwPolyline(profile) =
|
|
profile_plane.place_entity(EntityType::LwPolyline(profile))
|
|
else {
|
|
return None;
|
|
};
|
|
let mut base = SolidHistoryNodeBase::new(1);
|
|
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
|
let operation = SolidHistoryOperation::Sweep(SolidHistorySweep {
|
|
base,
|
|
operation_major: 1,
|
|
direction: Vector3::new(normal.x * height, normal.y * height, normal.z * height),
|
|
sweep_entity: Some(EmbeddedEntity::LwPolyline(profile)),
|
|
path_entity: Some(embedded_path(entity)?),
|
|
scale_factor: 1.0,
|
|
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
|
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
|
reference_point: Vector3::new(start.x, start.y, start.z),
|
|
..SolidHistorySweep::default()
|
|
});
|
|
let body = kernel::acis::rebuild_body(&operation).ok()?;
|
|
Some((body, operation))
|
|
}
|
|
|
|
fn circular_loft(profiles: &[EntityType]) -> Option<Body> {
|
|
if profiles.len() < 2 {
|
|
return None;
|
|
}
|
|
let sections = profiles
|
|
.iter()
|
|
.map(|entity| {
|
|
let planar = entity_curve(entity)?;
|
|
let Curve::Circle(circle) = planar.curve else {
|
|
return None;
|
|
};
|
|
Some((
|
|
Vec3::from(planar.plane.point_at(circle.centre)),
|
|
circle.radius,
|
|
Vec3::from(planar.plane.normal()?),
|
|
Vec3::from(planar.plane.x_axis),
|
|
))
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
let first = sections.first()?;
|
|
let direction = (sections.last()?.0 - first.0).normalize()?;
|
|
let scale = sections
|
|
.iter()
|
|
.map(|(centre, radius, _, _)| centre.length().max(*radius))
|
|
.fold(1.0_f64, f64::max);
|
|
let tolerance = scale * 1e-9;
|
|
let mut heights = Vec::with_capacity(sections.len());
|
|
for (centre, radius, normal, _) in §ions {
|
|
if !radius.is_finite() || *radius <= tolerance || normal.dot(direction).abs() < 1.0 - 1e-9 {
|
|
return None;
|
|
}
|
|
let offset = *centre - first.0;
|
|
if (offset - direction * offset.dot(direction)).length() > tolerance {
|
|
return None;
|
|
}
|
|
heights.push(offset.dot(direction));
|
|
}
|
|
if heights
|
|
.windows(2)
|
|
.any(|pair| pair[1] <= pair[0] + tolerance)
|
|
{
|
|
return None;
|
|
}
|
|
let radial_seed = first.3 - direction * first.3.dot(direction);
|
|
let radial = radial_seed.normalize()?;
|
|
let profile_plane =
|
|
Plane::from_axes(first.0.to_array(), radial.to_array(), direction.to_array());
|
|
let mut points = Vec::with_capacity(sections.len() + 2);
|
|
points.push([0.0, 0.0]);
|
|
points.extend(
|
|
sections
|
|
.iter()
|
|
.zip(&heights)
|
|
.map(|((_, radius, _, _), height)| [*radius, *height]),
|
|
);
|
|
points.push([0.0, *heights.last()?]);
|
|
let profile = (0..points.len())
|
|
.map(|index| {
|
|
Curve::Line(Line {
|
|
start: points[index],
|
|
end: points[(index + 1) % points.len()],
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
brep::revolve(
|
|
profile_plane,
|
|
&profile,
|
|
first.0.to_array(),
|
|
direction.to_array(),
|
|
std::f64::consts::TAU,
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use codec::entities::{Circle, LwPolyline, LwVertex};
|
|
use codec::types::{Vector2, Vector3};
|
|
|
|
fn square(size: f64) -> EntityType {
|
|
let mut polyline = LwPolyline::new();
|
|
for corner in [[0.0, 0.0], [size, 0.0], [size, size], [0.0, size]] {
|
|
polyline.add_vertex(LwVertex::new(Vector2::new(corner[0], corner[1])));
|
|
}
|
|
polyline.is_closed = true;
|
|
EntityType::LwPolyline(polyline)
|
|
}
|
|
|
|
fn volume(body: &Body) -> f64 {
|
|
crate::scene::model::solid_model::volume(body)
|
|
}
|
|
|
|
#[test]
|
|
fn a_closed_polyline_extrudes_into_a_prism() {
|
|
let solid = extruded(&square(10.0), 4.0).expect("a prism");
|
|
assert!((volume(&solid) - 400.0).abs() < 1e-6, "{}", volume(&solid));
|
|
}
|
|
|
|
#[test]
|
|
fn a_circle_extrudes_into_a_cylinder() {
|
|
// Cut into quarter arcs on the way, so the wall is four cylinder
|
|
// patches rather than a run of flat chords — the volume says which.
|
|
let mut circle = Circle::new();
|
|
circle.center = Vector3::new(0.0, 0.0, 0.0);
|
|
circle.radius = 5.0;
|
|
let solid = extruded(&EntityType::Circle(circle), 3.0).expect("a cylinder");
|
|
let expected = std::f64::consts::PI * 25.0 * 3.0;
|
|
let got = volume(&solid);
|
|
assert!(got > 0.98 * expected, "{got} vs {expected}");
|
|
assert!(got <= expected * 1.000_001, "{got} vs {expected}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_square_revolved_about_its_own_edge_is_a_tube() {
|
|
// The axis runs up the square's left side, so what it sweeps is a
|
|
// solid cylinder of radius ten and height ten.
|
|
let solid = revolved(
|
|
&square(10.0),
|
|
[0.0, 0.0, 0.0],
|
|
[0.0, 10.0, 0.0],
|
|
std::f64::consts::TAU,
|
|
0.0,
|
|
);
|
|
// The profile is in the XY plane and the axis lies in it, so this is
|
|
// a revolution about the y axis: radius ten, height ten.
|
|
let solid = solid.expect("a cylinder");
|
|
let expected = std::f64::consts::PI * 100.0 * 10.0;
|
|
let got = volume(&solid);
|
|
assert!(got > 0.98 * expected, "{got} vs {expected}");
|
|
}
|
|
|
|
#[test]
|
|
fn an_open_profile_is_refused() {
|
|
let mut polyline = LwPolyline::new();
|
|
for corner in [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]] {
|
|
polyline.add_vertex(LwVertex::new(Vector2::new(corner[0], corner[1])));
|
|
}
|
|
polyline.is_closed = false;
|
|
assert!(extruded(&EntityType::LwPolyline(polyline), 4.0).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn an_axis_off_the_profiles_plane_is_refused() {
|
|
assert!(revolved(
|
|
&square(10.0),
|
|
[0.0, 0.0, 0.0],
|
|
[0.0, 1.0, 1.0],
|
|
std::f64::consts::TAU,
|
|
0.0,
|
|
)
|
|
.is_none());
|
|
}
|
|
}
|