forked from 1b-agent/opencadstudio-upstream
feat(model): add exact solid modelling tools
This commit is contained in:
1 parent
fb2ea1e831
commit
06cd0ec6a9
51 files changed
+1487
-574
No files matched your search
@@ -1,11 +1,9 @@
|
||||
use acadrust::entities::{EmbeddedEntity, Solid3D};
|
||||
use acadrust::entities::Solid3D;
|
||||
use acadrust::objects::{
|
||||
SolidHistoryBox, SolidHistoryBrep, SolidHistoryCylinder, SolidHistoryLoft,
|
||||
SolidHistoryBox, SolidHistoryBrep, SolidHistoryCylinder,
|
||||
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid,
|
||||
SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus,
|
||||
SolidHistorySphere, SolidHistoryTorus,
|
||||
};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
use cadkernel::brep::Body;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
@@ -474,21 +472,6 @@ fn base(transform: [f64; 16]) -> SolidHistoryNodeBase {
|
||||
base
|
||||
}
|
||||
|
||||
fn embedded(entity: &EntityType) -> Option<EmbeddedEntity> {
|
||||
Some(match entity {
|
||||
EntityType::Point(value) => EmbeddedEntity::Point(value.clone()),
|
||||
EntityType::Line(value) => EmbeddedEntity::Line(value.clone()),
|
||||
EntityType::Arc(value) => EmbeddedEntity::Arc(value.clone()),
|
||||
EntityType::Circle(value) => EmbeddedEntity::Circle(value.clone()),
|
||||
EntityType::Ellipse(value) => EmbeddedEntity::Ellipse(value.clone()),
|
||||
EntityType::Spline(value) => EmbeddedEntity::Spline(value.clone()),
|
||||
EntityType::LwPolyline(value) => EmbeddedEntity::LwPolyline(value.clone()),
|
||||
EntityType::Ray(value) => EmbeddedEntity::Ray(value.clone()),
|
||||
EntityType::XLine(value) => EmbeddedEntity::XLine(value.clone()),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn box_op(
|
||||
transform: [f64; 16],
|
||||
length: f64,
|
||||
@@ -593,7 +576,7 @@ pub fn pyramid_op(
|
||||
}
|
||||
|
||||
pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
||||
let acis_data = crate::scene::convert::acis_export::planar_solid_to_sat(body)
|
||||
let acis_data = crate::scene::convert::acis_export::solid_to_sat(body)
|
||||
.map(|document| {
|
||||
let mut solid = Solid3D::new();
|
||||
solid.set_sat_document(&document);
|
||||
@@ -607,56 +590,3 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
||||
..SolidHistoryBrep::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extrusion_op(profile: &EntityType, height: f64) -> SolidHistoryOperation {
|
||||
SolidHistoryOperation::Extrusion(SolidHistorySweep {
|
||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
||||
operation_major: 1,
|
||||
direction: Vector3::new(0.0, 0.0, height),
|
||||
sweep_entity: embedded(profile),
|
||||
scale_factor: 1.0,
|
||||
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
..SolidHistorySweep::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sweep_op(profile: &EntityType, path: &EntityType) -> SolidHistoryOperation {
|
||||
SolidHistoryOperation::Sweep(SolidHistorySweep {
|
||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
||||
operation_major: 1,
|
||||
sweep_entity: embedded(profile),
|
||||
path_entity: embedded(path),
|
||||
scale_factor: 1.0,
|
||||
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
..SolidHistorySweep::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn loft_op(profiles: &[EntityType]) -> SolidHistoryOperation {
|
||||
SolidHistoryOperation::Loft(SolidHistoryLoft {
|
||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
||||
operation_major: 1,
|
||||
cross_sections: profiles.iter().filter_map(embedded).collect(),
|
||||
..SolidHistoryLoft::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn revolve_op(
|
||||
profile: &EntityType,
|
||||
axis_start: [f64; 3],
|
||||
axis_end: [f64; 3],
|
||||
angle: f64,
|
||||
) -> SolidHistoryOperation {
|
||||
let direction = glam::DVec3::from_array(axis_end) - glam::DVec3::from_array(axis_start);
|
||||
SolidHistoryOperation::Revolve(SolidHistoryRevolve {
|
||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
||||
operation_major: 1,
|
||||
axis_point: Vector3::new(axis_start[0], axis_start[1], axis_start[2]),
|
||||
direction: Vector2::new(direction.x, direction.y),
|
||||
revolve_angle: angle,
|
||||
sweep_entity: embedded(profile),
|
||||
..SolidHistoryRevolve::default()
|
||||
})
|
||||
}
|
||||
@@ -1,19 +1,6 @@
|
||||
// B-rep construction for the Model tab's primitives, plus tessellation into
|
||||
// the renderer's `MeshLodSet`.
|
||||
//
|
||||
// The bodies come from the geometry kernel, which builds each primitive the
|
||||
// way ACIS records it: an analytic surface with singular vertices where the
|
||||
// surface has them, rather than a mesh or a spline that happens to look like
|
||||
// one. That is what lets a solid built here be written back out as exact
|
||||
// geometry instead of a facetted approximation — see `acis_bridge`.
|
||||
//
|
||||
// Everything is oriented Z-up with the footprint on the z = base plane, to
|
||||
// match acadrust's `acis::primitives`.
|
||||
//
|
||||
// The resulting `Body` is cached per entity handle on the Scene so the
|
||||
// Design-group boolean tools can run on it.
|
||||
// Kernel B-rep construction and display tessellation.
|
||||
|
||||
use cadkernel::brep::{self, Body};
|
||||
use cadkernel::brep::{self, Body, Curve3, EdgeKey, FaceKey, Surface};
|
||||
|
||||
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
||||
|
||||
@@ -223,6 +210,65 @@ pub fn edge_wires(body: &Body) -> Vec<acadrust::entities::Wire> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// B-rep edge nearest a world-space surface pick.
|
||||
pub fn nearest_edge(body: &Body, pick: [f64; 3]) -> Option<EdgeKey> {
|
||||
let pick = cadkernel::space::Vec3::from(pick);
|
||||
body.edge_keys()
|
||||
.filter_map(|key| {
|
||||
let edge = body.edges.get(key)?;
|
||||
let curve = body.curves.get(edge.curve)?;
|
||||
let nearest = if matches!(curve, Curve3::Line(_)) {
|
||||
let start = cadkernel::space::Vec3::from(curve.point_at(edge.start_parameter));
|
||||
let end = cadkernel::space::Vec3::from(curve.point_at(edge.end_parameter));
|
||||
let span = end - start;
|
||||
let length2 = span.dot(span);
|
||||
let along = if length2 > 0.0 {
|
||||
(pick - start).dot(span) / length2
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
.clamp(0.0, 1.0);
|
||||
pick.distance(start + span * along)
|
||||
} else {
|
||||
(0..=64)
|
||||
.map(|step| {
|
||||
let t = edge.start_parameter
|
||||
+ (edge.end_parameter - edge.start_parameter) * step as f64 / 64.0;
|
||||
pick.distance(cadkernel::space::Vec3::from(curve.point_at(t)))
|
||||
})
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
};
|
||||
Some((key, nearest))
|
||||
})
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(key, _)| key)
|
||||
}
|
||||
|
||||
/// Planar face nearest a world-space surface pick.
|
||||
pub fn nearest_planar_face(body: &Body, pick: [f64; 3]) -> Option<FaceKey> {
|
||||
let face = body
|
||||
.face_keys()
|
||||
.filter_map(|key| {
|
||||
let face = body.faces.get(key)?;
|
||||
let surface = body.surfaces.get(face.surface)?;
|
||||
Some((key, surface.distance_to(pick).abs()))
|
||||
})
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(key, _)| key)?;
|
||||
let node = body.faces.get(face)?;
|
||||
matches!(body.surfaces.get(node.surface)?, Surface::Plane(_)).then_some(face)
|
||||
}
|
||||
|
||||
/// Outward normal of a planar face.
|
||||
pub fn planar_face_normal(body: &Body, face: FaceKey) -> Option<[f64; 3]> {
|
||||
let face = body.faces.get(face)?;
|
||||
let Surface::Plane(plane) = body.surfaces.get(face.surface)? else {
|
||||
return None;
|
||||
};
|
||||
let normal = cadkernel::space::Vec3::from(plane.normal()?);
|
||||
Some(if face.forward { normal } else { -normal }.to_array())
|
||||
}
|
||||
|
||||
// ── Boolean operations ──────────────────────────────────────────────────────
|
||||
|
||||
/// Which CSG to apply. Mirrors `model::boolean_cmd::BoolOp` but kept local so
|
||||
@@ -246,17 +292,12 @@ pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option<Body> {
|
||||
Bool::Subtract => brep::Operation::Difference,
|
||||
Bool::Intersect => brep::Operation::Intersection,
|
||||
};
|
||||
brep::combine(a.clone(), b.clone(), how, TOL).ok()
|
||||
let tolerance = brep::operation_tolerance(&[a, b]);
|
||||
brep::combine(a.clone(), b.clone(), how, tolerance).ok()
|
||||
}
|
||||
|
||||
// ── Tessellation ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Tessellate a `Body` into a single-LOD `MeshLodSet` (world-space, before
|
||||
/// world_offset is applied by the caller).
|
||||
pub fn mesh_from_solid(body: &Body, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||
mesh_from_tessellation(tessellation(body), color)
|
||||
}
|
||||
|
||||
fn mesh_from_tessellation(
|
||||
tessellation: brep::mesh::BodyMesh,
|
||||
color: [f32; 4],
|
||||
|
||||
+221
-89
@@ -1,20 +1,8 @@
|
||||
// EXTRUDE / REVOLVE: turning a drawn profile into a solid.
|
||||
//
|
||||
// The profile comes from `entities::curve`, which is where every entity's
|
||||
// geometry is already defined once, so a circle, an arc-bulged polyline and a
|
||||
// closed spline all arrive as the same thing: a plane plus a chain of curves
|
||||
// in that plane's coordinates. The kernel sweeps that chain into analytic
|
||||
// surfaces — a straight run into a plane, an arc into a cylinder, a profile
|
||||
// turned about an axis into a cone, a sphere or a torus — so the result can
|
||||
// be written back out as ACIS rather than as facets.
|
||||
//
|
||||
// A spline profile has no analytic side wall, so it is refused rather than
|
||||
// approximated into a surface nobody asked for. That is the kernel's answer
|
||||
// and this module passes it on.
|
||||
// Exact profile sweeps stored as kernel B-reps and ACIS.
|
||||
|
||||
use cadkernel::brep::{self, Body};
|
||||
use cadkernel::geom2d::Curve;
|
||||
use cadkernel::space::{PlanarCurve, Plane};
|
||||
use cadkernel::geom2d::{offset_polyline, Arc, Curve, EllipseArc, Line, Polyline};
|
||||
use cadkernel::space::{PlanarCurve, Plane, Vec3};
|
||||
use acadrust::EntityType;
|
||||
|
||||
use crate::entities::curve::entity_curve;
|
||||
@@ -40,13 +28,17 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
|
||||
return None;
|
||||
}
|
||||
let pieces = match &planar.curve {
|
||||
// A polyline is already a chain. Its own segments carry the bulges,
|
||||
// so an arc stays an arc rather than becoming a run of chords.
|
||||
Curve::Polyline(_) => planar.curve.segments(),
|
||||
// Everything else closed on itself is cut into quarters, which is
|
||||
// both the fewest pieces a chain may have and the fewest that leave
|
||||
// each one unambiguous about which way round it goes.
|
||||
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
|
||||
Curve::Arc(arc) => split_arc(*arc),
|
||||
Curve::Ellipse(arc) => split_ellipse(*arc),
|
||||
Curve::Nurbs(curve) => (0..4)
|
||||
.map(|part| {
|
||||
curve
|
||||
.trimmed(part as f64 / 4.0, (part + 1) as f64 / 4.0)
|
||||
.map(Curve::Nurbs)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
_ => return None,
|
||||
};
|
||||
(pieces.len() >= 3).then_some(Profile {
|
||||
@@ -57,7 +49,6 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
|
||||
|
||||
/// A circle as four arcs.
|
||||
fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
|
||||
use cadkernel::geom2d::Arc;
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
(0..4)
|
||||
.map(|quarter| {
|
||||
@@ -72,6 +63,34 @@ fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
|
||||
.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
|
||||
@@ -86,6 +105,13 @@ pub fn extruded(entity: &EntityType, height: f64) -> Option<Body> {
|
||||
)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@@ -108,85 +134,191 @@ pub fn revolved(
|
||||
)
|
||||
}
|
||||
|
||||
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
||||
|
||||
/// SWEEP through the kernel's tolerance-driven mesh API.
|
||||
pub fn swept(profile: &EntityType, path: &EntityType, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||
let max_angle = cadkernel::tessellation::DEFAULT_ANGLE;
|
||||
let surface = brep::mesh::sweep_surface(
|
||||
&entity_curve(profile)?,
|
||||
&entity_curve(path)?,
|
||||
max_angle,
|
||||
)?;
|
||||
mesh_set(surface, color, 1e-9)
|
||||
/// 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 path = entity_curve(path)?;
|
||||
let pieces = match &path.curve {
|
||||
Curve::Line(line) => vec![Curve::Line(*line)],
|
||||
Curve::Arc(arc) => vec![Curve::Arc(*arc)],
|
||||
Curve::Polyline(_) => path.curve.segments(),
|
||||
_ => return None,
|
||||
};
|
||||
brep::sweep_along(profile.plane, &profile.pieces, path.plane, &pieces)
|
||||
}
|
||||
|
||||
/// LOFT through the kernel's tolerance-driven mesh API.
|
||||
pub fn lofted(profiles: &[EntityType], color: [f32; 4]) -> Option<MeshLodSet> {
|
||||
let curves: Vec<PlanarCurve> = profiles.iter().filter_map(entity_curve).collect();
|
||||
let max_angle = cadkernel::tessellation::DEFAULT_ANGLE;
|
||||
mesh_set(brep::mesh::loft_surface(&curves, max_angle)?, color, 1e-9)
|
||||
/// 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)
|
||||
}
|
||||
|
||||
fn mesh_set(
|
||||
surface: brep::mesh::SurfaceMesh,
|
||||
color: [f32; 4],
|
||||
precision: f64,
|
||||
) -> Option<MeshLodSet> {
|
||||
if surface.mesh.is_empty() {
|
||||
/// Builds an exact wall solid around a polyline centreline.
|
||||
pub fn polysolid(entity: &EntityType, width: f64, height: f64) -> Option<Body> {
|
||||
if !width.is_finite() || !height.is_finite() || width <= 0.0 || height.abs() <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let mut verts = Vec::with_capacity(surface.mesh.positions.len());
|
||||
let mut verts_low = Vec::with_capacity(surface.mesh.positions.len());
|
||||
for point in &surface.mesh.positions {
|
||||
push_point(&mut verts, &mut verts_low, *point);
|
||||
let planar = entity_curve(entity)?;
|
||||
let Curve::Polyline(source) = planar.curve else {
|
||||
return None;
|
||||
};
|
||||
let (left_pick, right_pick) = offset_picks(&source, width)?;
|
||||
let mut left = offset_polyline(&source, width * 0.5, left_pick);
|
||||
let mut right = offset_polyline(&source, width * 0.5, right_pick);
|
||||
if left.len() != 1 || right.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let silhouette = surface.silhouette_source(precision);
|
||||
let mut set = MeshLodSet::from_single(MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
verts_low,
|
||||
normals: surface
|
||||
.mesh
|
||||
.normals
|
||||
.iter()
|
||||
.map(|normal| [normal[0] as f32, normal[1] as f32, normal[2] as f32])
|
||||
.collect(),
|
||||
indices: surface
|
||||
.mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|index| *index as u32)
|
||||
.collect(),
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
});
|
||||
{
|
||||
let (high, low) = (&mut set.edge_verts, &mut set.edge_verts_low);
|
||||
for edge in surface.edges {
|
||||
for segment in edge.windows(2) {
|
||||
for point in segment {
|
||||
push_point(high, low, *point);
|
||||
}
|
||||
}
|
||||
let left = left.remove(0);
|
||||
let right = right.remove(0);
|
||||
let normal = Vec3::from(planar.plane.normal()?);
|
||||
let direction = (normal * height).to_array();
|
||||
|
||||
if source.closed {
|
||||
if !left.closed || !right.closed {
|
||||
return None;
|
||||
}
|
||||
let mut profiles = [left, right]
|
||||
.into_iter()
|
||||
.map(|polyline| {
|
||||
let area = Curve::Polyline(polyline.clone()).enclosed_area().abs();
|
||||
(area, Curve::Polyline(polyline).segments())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
profiles.sort_by(|first, second| second.0.total_cmp(&first.0));
|
||||
if profiles[0].0 <= profiles[1].0 || profiles[1].0 <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
return brep::extrude_region(
|
||||
planar.plane,
|
||||
&[profiles.remove(0).1, profiles.remove(0).1],
|
||||
direction,
|
||||
);
|
||||
}
|
||||
set.curved_gens
|
||||
.push(super::mesh_model::CurvedGen { source: silhouette });
|
||||
Some(set)
|
||||
|
||||
if left.closed || right.closed {
|
||||
return None;
|
||||
}
|
||||
let left_start = left.vertices.first()?.position;
|
||||
let left_end = left.vertices.last()?.position;
|
||||
let right_start = right.vertices.first()?.position;
|
||||
let right_end = right.vertices.last()?.position;
|
||||
let mut outline = Curve::Polyline(left).segments();
|
||||
outline.push(Curve::Line(Line {
|
||||
start: left_end,
|
||||
end: right_end,
|
||||
}));
|
||||
let mut back = Curve::Polyline(right).segments();
|
||||
back.reverse();
|
||||
outline.extend(back);
|
||||
outline.push(Curve::Line(Line {
|
||||
start: right_start,
|
||||
end: left_start,
|
||||
}));
|
||||
brep::extrude(planar.plane, &outline, direction)
|
||||
}
|
||||
|
||||
fn push_point(high: &mut Vec<[f32; 3]>, low: &mut Vec<[f32; 3]>, point: [f64; 3]) {
|
||||
let coarse = [point[0] as f32, point[1] as f32, point[2] as f32];
|
||||
high.push(coarse);
|
||||
low.push([
|
||||
(point[0] - coarse[0] as f64) as f32,
|
||||
(point[1] - coarse[1] as f64) as f32,
|
||||
(point[2] - coarse[2] as f64) as f32,
|
||||
]);
|
||||
fn offset_picks(polyline: &Polyline, width: f64) -> Option<([f64; 2], [f64; 2])> {
|
||||
let start = polyline.vertices.first()?.position;
|
||||
let next = polyline.vertices.get(1)?.position;
|
||||
let along = if let Some(arc) = polyline.segment_arc(0) {
|
||||
let near = arc.sample(1e-6);
|
||||
[near[0] - start[0], near[1] - start[1]]
|
||||
} else {
|
||||
[next[0] - start[0], next[1] - start[1]]
|
||||
};
|
||||
let length = along[0].hypot(along[1]);
|
||||
if length <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let side = [-along[1] / length * width, along[0] / length * width];
|
||||
Some((
|
||||
[start[0] + side[0], start[1] + side[1]],
|
||||
[start[0] - side[0], start[1] - side[1]],
|
||||
))
|
||||
}
|
||||
|
||||
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)]
|
||||
|
||||
Reference in new issue
Block a user