// 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::{ 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; /// 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, } /// 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 { 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; } 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)> { 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)?; (!pieces.is_empty()).then_some(( Profile { plane: planar.plane, pieces, }, closed, )) } /// 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 { 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::>>()? .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> { 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::>>()?, Curve::Nurbs(nurbs) => vec![Curve::Nurbs(nurbs.clone())], _ => return None, }; Some(pieces) } fn planar_polygon_entity(entity: &EntityType) -> Option { let (points, closed) = match entity { EntityType::Polyline3D(value) => ( value .vertices .iter() .map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]) .collect::>(), value.is_closed(), ), EntityType::Face3D(value) => ( value .corners() .into_iter() .map(|point| [point.x, point.y, point.z]) .collect::>(), true, ), EntityType::Solid(value) => ( value .boundary_corners() .into_iter() .map(|point| [point.x, point.y, point.z]) .collect::>(), true, ), EntityType::Region(value) => ( value .wires .first()? .points .iter() .map(|point| [point.x, point.y, point.z]) .collect::>(), 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::>>()?; Some(PlanarCurve::new( plane, Curve::Polyline(kernel::geom2d::Polyline { vertices, closed }), )) } /// 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>)> { 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 { let mut points = wire .points .iter() .map(|point| [point.x, point.y, point.z]) .collect::>(); 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::>>()?; 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 { 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 { 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 { 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 { 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 { 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 { if region_with_holes(entity) { 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 { if region_with_holes(entity) { 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::>(); 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 { 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 { 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 { 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 { 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 { 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 { if !angle.is_finite() || angle.abs() <= 1e-12 || !start_angle.is_finite() || region_with_holes(entity) { 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 { 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 { 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::>>()?; brep::loft(§ions) } /// Stores the source sections required to rebuild and edit a loft. pub fn loft_history(profiles: &[EntityType]) -> Option { let cross_sections = profiles .iter() .map(embedded_path) .collect::>>()?; 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 { 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::>(); 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, } } /// 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::() - 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())); } } 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) } /// Extrusion records set group 295 (see `extrusion_history`): their embedded /// entities stand where they are and the matrices only name their frames. A /// planar fallback profile stored in its plane's local frame is therefore /// re-expressed in its own OCS. fn embedded_in_place(entity: EmbeddedEntity, transform: &[f64; 16]) -> EmbeddedEntity { let frame = glam::DMat4::from_cols_array(transform); let EmbeddedEntity::LwPolyline(mut polyline) = entity else { return entity; }; if frame == glam::DMat4::IDENTITY { return EmbeddedEntity::LwPolyline(polyline); } let normal = frame.z_axis.truncate().normalize(); let (x, y) = crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z)); let x_axis = glam::DVec3::new(x.0, x.1, x.2); let y_axis = glam::DVec3::new(y.0, y.1, y.2); for vertex in &mut polyline.vertices { let local = glam::DVec3::new(vertex.location.x, vertex.location.y, 0.0); let world = frame.transform_point3(local); vertex.location = Vector2::new(world.dot(x_axis), world.dot(y_axis)); polyline.elevation = world.dot(normal); } polyline.normal = Vector3::new(normal.x, normal.y, normal.z); EmbeddedEntity::LwPolyline(polyline) } pub fn extrusion_history( profile: &EntityType, path: Option<&EntityType>, direction: [f64; 3], taper_angle: f64, reference_point: [f64; 3], ) -> Option { 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()), }; let sweep_entity = embedded_in_place(sweep_entity, &sweep_entity_transform); let path_entity = path_entity.map(|entity| embedded_in_place(entity, &path_entity_transform)); 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]), // Group 295: the profile frame is given, as the reference records // it. Left to be computed, the reference cannot open a drawing with // a tapered extrusion (ErrorStatus 53). flags_294_296: [false, true, false], ..SolidHistorySweep::default() })) } pub fn sweep_history( profile: &EntityType, path: &EntityType, taper_angle: f64, reference_point: [f64; 3], ) -> Option { 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 { 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::>>()?; 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::>(); 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()); } }