forked from 1b-agent/opencadstudio-upstream
270 lines
9.2 KiB
Rust
270 lines
9.2 KiB
Rust
// 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.
|
|
|
|
use cadkernel::brep::{self, Body};
|
|
use cadkernel::geom2d::Curve;
|
|
use cadkernel::space::{PlanarCurve, Plane};
|
|
use acadrust::EntityType;
|
|
|
|
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: PlanarCurve = entity_curve(entity)?;
|
|
if !planar.curve.is_closed() {
|
|
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),
|
|
_ => return None,
|
|
};
|
|
(pieces.len() >= 3).then_some(Profile {
|
|
plane: planar.plane,
|
|
pieces,
|
|
})
|
|
}
|
|
|
|
/// 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| {
|
|
let start = FRAC_PI_2 * quarter as f64;
|
|
Curve::Arc(Arc {
|
|
centre,
|
|
radius,
|
|
start_angle: start,
|
|
end_angle: start + FRAC_PI_2,
|
|
})
|
|
})
|
|
.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()?;
|
|
brep::extrude(
|
|
profile.plane,
|
|
&profile.pieces,
|
|
[normal[0] * height, normal[1] * height, normal[2] * height],
|
|
)
|
|
}
|
|
|
|
/// 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,
|
|
) -> Option<Body> {
|
|
let profile = profile_of(entity)?;
|
|
brep::revolve(
|
|
profile.plane,
|
|
&profile.pieces,
|
|
from,
|
|
[to[0] - from[0], to[1] - from[1], to[2] - from[2]],
|
|
angle,
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
|
|
fn mesh_set(
|
|
surface: brep::mesh::SurfaceMesh,
|
|
color: [f32; 4],
|
|
precision: f64,
|
|
) -> Option<MeshLodSet> {
|
|
if surface.mesh.is_empty() {
|
|
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 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
set.curved_gens
|
|
.push(super::mesh_model::CurvedGen { source: silhouette });
|
|
Some(set)
|
|
}
|
|
|
|
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,
|
|
]);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use acadrust::entities::{Circle, LwPolyline, LwVertex};
|
|
use acadrust::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,
|
|
);
|
|
// 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
|
|
)
|
|
.is_none());
|
|
}
|
|
}
|