Merge reviewed modeling PRs with Unicode and PRESSPULL fixes

This commit is contained in:
Hakan Seven committed 2026-09-04 19:49:55 +03:00
commit 90761904a9
24 files changed
+3528 -498

No files matched your search

+167 -55
View File
@@ -54,10 +54,84 @@ pub(crate) use boundary::{
// other tessellation code); re-exported here so this root and sibling topic
// modules (each does `use super::*`) keep referencing them unqualified.
pub(crate) use convert::tess::{
entity_aabb, entity_world_aabb_f64, is_unindexable_entity, tessellate_entity,
entity_aabb, entity_world_aabb_f64, is_unindexable_entity,
tessellate_entity_dim_text,
};
/// Keep construction-history curves in the same per-entity tessellation memo
/// as their parent, so navigation never rebuilds them on every frame.
#[allow(clippy::too_many_arguments)]
pub(crate) fn tessellate_entity(
document: &CadDocument,
selected: &HashSet<Handle>,
active_viewport: Option<Handle>,
bg_color: [f32; 4],
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
entity: &EntityType,
block_cache: Option<&cache::block_cache::BlockCache>,
view_aabb: Option<[f32; 4]>,
world_per_pixel: Option<f32>,
paper_space: bool,
) -> Vec<WireModel> {
let mut wires = convert::tess::tessellate_entity(
document, selected, active_viewport, bg_color, anno_scale,
annotation_scale_handle, entity, block_cache, view_aabb,
world_per_pixel, paper_space,
);
if matches!(entity, EntityType::Solid3D(_) | EntityType::Surface(_)) {
let handle = entity.common().handle;
for source in model::solid_history::loft_visible_history_entities(document, handle) {
// Call the raw converter, not this wrapper: sources must not expand
// the parent's history again through their shared selection handle.
let mut construction = if let EntityType::Region(region) = &source {
// Regions normally render through the mesh layer, so the raw
// wire converter emits nothing. Decode every exact boundary,
// including holes, then sample only for this cached display.
let mut boundaries = Vec::new();
if let Ok(cadkernel::brep::LoftSection::Profile { plane, wires, .. }) =
cadkernel::acis::loft_section_geometry(&[
acadrust::entities::EmbeddedEntity::Region(region.clone()),
])
{
let (color, _, _, line_weight_px, _) =
view::render::render_style_for_viewport(document, &source, active_viewport);
for boundary in wires {
for curve in boundary {
let points = curve.tessellate(64.0 / std::f64::consts::TAU)
.into_iter().map(|point| plane.point_at(point));
let (points, points_low) = convert::tessellate::points_to_ds(points);
let mut wire = WireModel::solid(
handle.value().to_string(), points, color, selected.contains(&handle),
);
wire.points_low = points_low;
wire.line_weight_px = line_weight_px;
boundaries.push(wire);
}
}
}
boundaries
} else {
convert::tess::tessellate_entity(
document, selected, active_viewport, bg_color, anno_scale,
annotation_scale_handle, &source, block_cache, view_aabb,
world_per_pixel, paper_space,
)
};
for wire in &mut construction {
wire.name = handle.value().to_string();
if !wire.selected {
wire.color = [1.0, 0.25, 0.25, wire.color[3]];
}
wire.fill_tris.clear();
wire.fill_tris_low.clear();
}
wires.extend(construction);
}
}
wires
}
/// Result of `Scene::entity_index()`. The wire path queries `tree` for
/// view-rect candidates and also always emits `unbounded_handles`
/// (entities with no usable bbox — legacy `UNBOUNDED_AABB` sentinel).
@@ -2731,12 +2805,12 @@ impl Scene {
})
}
/// Cache and display a Model-tab kernel solid.
pub fn register_solid_model(
&mut self,
/// Prepare display geometry without changing the entity or resident caches.
pub(crate) fn prepare_solid_model_display(
&self,
handle: Handle,
solid: cadkernel::brep::Body,
) -> bool {
solid: &cadkernel::brep::Body,
) -> Option<(MeshLodSet, Vec<acadrust::entities::Wire>, [f64; 3])> {
let color = self
.document
.get_entity(handle)
@@ -2746,67 +2820,105 @@ impl Scene {
let chordal_deflection =
crate::entities::solid3d::display_deflection(&self.document.header, facet_resolution);
let isolines = self.document.header.isolines.max(0) as usize;
let displayed = if let Some((mut set, wires, center)) =
let (mut set, wires, center) =
crate::scene::model::solid_model::display_from_solid(
&solid,
solid,
color,
facet_resolution,
chordal_deflection,
isolines,
)?;
let name = handle.value().to_string();
for mesh in &mut set.lods {
mesh.name = name.clone();
}
if let Some(entity) = self.document.get_entity(handle) {
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
)
{
if let Some(entity) = self.document.get_entity_mut(handle) {
let reference = acadrust::types::Vector3::new(center[0], center[1], center[2]);
match entity {
EntityType::Solid3D(entity) => {
entity.point_of_reference = reference;
entity.wires = wires;
entity.silhouettes.clear();
}
EntityType::Surface(entity) => {
entity.point_of_reference = reference;
entity.wires = wires;
entity.silhouettes.clear();
}
_ => {}
}
}
let name = handle.value().to_string();
.apply_to_with_face_overrides(
&mut set,
&self.document,
self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut set,
&self.document,
entity,
);
}
Some((set, wires, center))
}
/// Commit a prepared display without tessellating the body a second time.
pub(crate) fn register_prepared_solid_model(
&mut self,
handle: Handle,
solid: cadkernel::brep::Body,
display: (MeshLodSet, Vec<acadrust::entities::Wire>, [f64; 3]),
) -> bool {
let (mut set, wires, center) = display;
// New command results can be prepared before their handle exists.
// Apply the committed entity's actual style without retessellating.
if let Some(entity) = self.document.get_entity(handle) {
let color = self.render_style(entity).0;
for mesh in &mut set.lods {
mesh.name = name.clone();
mesh.name = handle.value().to_string();
mesh.color = color;
}
if let Some(entity) = self.document.get_entity(handle) {
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
)
.apply_to_with_face_overrides(
&mut set,
&self.document,
self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut set,
&self.document,
entity,
);
crate::scene::model::material_model::resolve_material_with_base(
&self.document, entity, color, None, self.material_base_dir.as_deref(),
).apply_to_with_face_overrides(
&mut set, &self.document, self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut set, &self.document, entity,
);
}
if let Some(entity) = self.document.get_entity_mut(handle) {
let reference = acadrust::types::Vector3::new(center[0], center[1], center[2]);
match entity {
EntityType::Solid3D(entity) => {
entity.point_of_reference = reference;
entity.wires = wires;
entity.silhouettes.clear();
}
EntityType::Surface(entity) => {
entity.point_of_reference = reference;
entity.wires = wires;
entity.silhouettes.clear();
}
_ => {}
}
self.meshes.insert(handle, set);
true
} else {
self.meshes.remove(&handle);
false
};
}
self.meshes.insert(handle, set);
self.solid_models.insert(handle, solid);
self.sync_solid_reference_point(handle);
// Only this solid's mesh changed — report just its handle so the mesh /
// wire caches patch it in rather than rebuilding the whole drawing (and
// so a bulk register loop stays O(n), not O(n²)).
self.bump_entities(&[(handle, ChangeKind::Modified)]);
displayed
true
}
/// Cache and display a Model-tab kernel solid. A failed preparation leaves
/// the previous entity, body and mesh untouched.
pub fn register_solid_model(
&mut self,
handle: Handle,
solid: cadkernel::brep::Body,
) -> bool {
let Some(display) = self.prepare_solid_model_display(handle, &solid) else {
return false;
};
if !display.0.complete && matches!(
self.document.solid_history_operation(handle),
Some(acadrust::objects::SolidHistoryOperation::Loft(_))
) {
return false;
}
self.register_prepared_solid_model(handle, solid, display)
}
/// `BACKGROUND` change picks up the new `adapt_to_bg` result without
+95
View File
@@ -0,0 +1,95 @@
//! LOFT command/history integration. Geometry and validation live in the kernel.
use acadrust::entities::{EmbeddedEntity, Point, Surface, SurfaceData, SurfaceKind};
use acadrust::objects::{SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase};
use acadrust::{EntityType, Handle};
use cadkernel::brep::Body;
use crate::command::{ExtrudeMode, LoftOptions, LoftSectionSelection};
use super::sweep_model::embedded_path;
fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
match entity {
EntityType::Region(region) => Some(EmbeddedEntity::Region(region.clone())),
EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())),
_ => embedded_path(entity),
}
}
pub fn is_section(entity: &EntityType) -> bool {
embedded_section(entity).is_some_and(|entity|
cadkernel::acis::loft_section_geometry(&[entity]).is_ok())
}
pub fn is_guide_or_path(entity: &EntityType) -> bool {
embedded_path(entity).is_some_and(|entity| cadkernel::acis::loft_path_geometry(&entity).is_ok())
}
pub fn record(
sections: &[LoftSectionSelection], guides: &[Handle], path: Option<Handle>,
available: &[(Handle, EntityType)], mode: ExtrudeMode, options: LoftOptions,
) -> Result<SolidHistoryLoft, String> {
let find = |handle| available.iter().find(|(candidate, _)| *candidate == handle)
.map(|(_, entity)| entity).ok_or_else(|| "A selected loft source no longer exists.".to_string());
let mut cross_sections = Vec::new();
let mut section_counts = Vec::new();
let mut surface = mode == ExtrudeMode::Surface;
for section in sections {
let members = match section {
LoftSectionSelection::Entity(handle) => vec![embedded_section(find(*handle)?)
.ok_or("Unsupported loft section.")?],
LoftSectionSelection::Point(point) => vec![EmbeddedEntity::Point(Point::from_coords(point.x, point.y, point.z))],
LoftSectionSelection::Join(handles) => handles.iter().map(|handle| {
embedded_section(find(*handle)?).ok_or_else(|| "Unsupported joined loft edge.".to_string())
}).collect::<Result<Vec<_>, _>>()?,
};
if let cadkernel::brep::LoftSection::Profile { closed, .. } = cadkernel::acis::loft_section_geometry(&members)? {
surface |= !closed;
}
section_counts.push(members.len());
cross_sections.extend(members);
}
let guides = guides.iter().map(|handle| embedded_path(find(*handle)?)
.ok_or_else(|| "Unsupported loft guide.".to_string())).collect::<Result<Vec<_>, _>>()?;
let path_entity = path.map(|handle| embedded_path(find(handle)?)
.ok_or_else(|| "Unsupported loft path.".to_string())).transpose()?;
Ok(SolidHistoryLoft {
base: SolidHistoryNodeBase::new(1), operation_major: 1,
cross_sections, guides,
parameters: Some(SolidHistoryLoftParameters {
path_entity, normals: options.normals,
start_draft_angle: options.start_draft_angle, end_draft_angle: options.end_draft_angle,
start_magnitude: options.start_magnitude, end_magnitude: options.end_magnitude,
start_continuity: options.start_continuity, end_continuity: options.end_continuity,
start_bulge: options.start_bulge, end_bulge: options.end_bulge,
closed: options.closed, periodic: options.periodic, surface, align_direction: options.align_direction, section_counts,
}),
..SolidHistoryLoft::default()
})
}
pub fn build_body(
sections: &[LoftSectionSelection], guides: &[Handle], path: Option<Handle>,
available: &[(Handle, EntityType)], mode: ExtrudeMode, options: LoftOptions,
) -> Result<Body, String> {
cadkernel::acis::rebuild_loft_with_options(&record(sections, guides, path, available, mode, options)?)
}
pub fn surface_entity(record: &SolidHistoryLoft) -> EntityType {
let settings = record.parameters.clone().unwrap_or_else(|| SolidHistoryLoftParameters {
normals: 0, ..Default::default()
});
let mut surface = Surface::new(SurfaceKind::Lofted);
surface.surface_data = SurfaceData::Lofted {
loft_transform: record.base.transform,
cross_section_entities: record.cross_sections.clone(), guide_entities: record.guides.clone(),
path_entity: settings.path_entity,
plane_normal_lofting_type: settings.normals,
start_draft_angle: settings.start_draft_angle, end_draft_angle: settings.end_draft_angle,
start_draft_magnitude: settings.start_magnitude, end_draft_magnitude: settings.end_magnitude,
arc_length_parameterization: true, no_twist: true, align_direction: settings.align_direction,
simple_surfaces: true, closed_surfaces: settings.closed, solid: false,
ruled_surface: settings.normals == 0, virtual_guide: false,
cross_sections: Vec::new(), guide_curves: Vec::new(), path_curve: None,
};
EntityType::Surface(surface)
}
+3
View File
@@ -11,5 +11,8 @@ pub mod mesh_model;
pub mod solid_model;
pub mod solid_history;
pub mod sweep_model;
pub mod presspull_model;
pub mod loft_command_model;
mod sweep_command_model;
pub mod visual_style_model;
pub mod object;
+378
View File
@@ -0,0 +1,378 @@
//! PRESSPULL selection and curve-only previews. Geometry operations stay in the kernel.
use acadrust::entities::{AcisData, EmbeddedEntity, Region, Wire};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use cadkernel::brep::{self, Body, FaceKey};
use cadkernel::geom2d::{contains, ring_nesting_depths, signed_area, Curve, Line, Tolerance};
use cadkernel::space::Plane;
use glam::DVec3;
use rustc_hash::FxHashMap;
use crate::command::WorkingPlane;
use crate::scene::{boundary_faces, exact_hatch_paths, ring_source_handles, BoundarySource, Scene};
use super::{sweep_model, wire_model::WireModel};
const BOUNDARY_TOLERANCE: f64 = 1e-6;
#[derive(Clone)]
pub enum PresspullTargetKind {
Profile { entity: EntityType, source: Option<Handle>, owner: Option<Handle> },
Face { handle: Handle, face: FaceKey, body: Body, offset: bool },
}
#[derive(Clone)]
pub struct PresspullTarget {
pub kind: PresspullTargetKind,
pub anchor: DVec3,
pub direction: DVec3,
}
/// Extract exact embedded REGION loops before falling back to polygonal entities.
/// In particular, preview and final extrusion must not approximate REGION arcs
/// from their display wires or discard holes.
pub fn profile_geometry(entity: &EntityType) -> Option<(Plane, Vec<Vec<Curve>>, bool)> {
if matches!(entity, EntityType::Ray(_) | EntityType::XLine(_)) {
return None;
}
let embedded = match entity {
EntityType::Region(region) => Some((
EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array(),
)),
_ => sweep_model::embedded_revolve_profile(entity),
};
if let Some((entity, transform)) = embedded {
if let Ok(geometry) = cadkernel::acis::sweep_profile_geometry(&entity, transform) {
return Some(geometry);
}
// Exact modeler data is authoritative, not a hint to replace by chords.
if matches!(entity, EmbeddedEntity::Region(ref region) if region.acis_data.has_data()) {
return None;
}
}
let (profile, closed) = sweep_model::extrusion_profile_of(entity)?;
Some((profile.plane, vec![profile.pieces], closed))
}
/// Builds the final solid or surface without discarding exact REGION loops.
pub fn extrusion_body(entity: &EntityType, direction: [f64; 3]) -> Option<Body> {
let (plane, loops, closed) = profile_geometry(entity)?;
if closed {
brep::extrude_region(plane, &loops, direction)
} else if loops.len() == 1 {
brep::extrude_surface(plane, &loops[0], direction)
} else {
None
}
}
fn eligible(scene: &Scene, entity: &EntityType) -> bool {
let common = entity.common();
!common.invisible
&& !scene.entity_temporarily_hidden(common.handle)
&& !scene.layer_hidden(&common.layer)
&& !scene.interaction_layer_frozen(&common.layer)
&& !scene.is_layer_locked(common.handle)
&& scene.belongs_to_visible_block(
common.handle, common.owner_handle, scene.interaction_block_handle(),
)
&& !crate::scene::annotative::annotative_offscale_for(
&scene.document, common, scene.displayed_annotation_scale_handle(),
scene.annotation_all_visible(),
)
}
fn working_plane(plane: Plane) -> WorkingPlane {
WorkingPlane::new(
DVec3::from_array(plane.origin), DVec3::from_array(plane.x_axis),
DVec3::from_array(plane.y_axis),
)
}
fn curves_on_plane(source: Plane, curves: &[Curve], plane: WorkingPlane) -> Vec<Curve> {
let origin = plane.to_local(DVec3::from_array(source.origin));
let x_axis = plane.vector_to_local(DVec3::from_array(source.x_axis));
let y_axis = plane.vector_to_local(DVec3::from_array(source.y_axis));
let transform = cadkernel::geom2d::Transform {
origin: [origin.x, origin.y].into(),
x_axis: [x_axis.x, x_axis.y].into(),
y_axis: [y_axis.x, y_axis.y].into(),
};
curves.iter().filter_map(|curve| {
// A line belongs to more than one plane. Its arbitrary source axes
// must not disqualify an edge which lies in the requested work plane.
if let Curve::Line(line) = curve {
let start = plane.to_local(DVec3::from_array(source.point_at(line.start)));
let end = plane.to_local(DVec3::from_array(source.point_at(line.end)));
return (start.z.abs() <= BOUNDARY_TOLERANCE && end.z.abs() <= BOUNDARY_TOLERANCE)
.then_some(Curve::Line(Line { start: [start.x, start.y], end: [end.x, end.y] }));
}
if origin.z.abs() > BOUNDARY_TOLERANCE || x_axis.z.abs() > 1e-9 || y_axis.z.abs() > 1e-9 {
return None;
}
curve.transformed(&transform)
}).collect()
}
fn boundary_source(curves: Vec<Curve>) -> Option<BoundarySource> {
let curves = curves.into_iter().filter(|curve| {
let length = curve.length();
length.is_finite() && length > BOUNDARY_TOLERANCE
}).collect::<Vec<_>>();
let segments = curves.iter().flat_map(|curve| {
curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
.windows(2).filter_map(|pair| {
let start = pair[0];
let end = pair[1];
(start.iter().chain(&end).all(|value| value.is_finite()) && start != end)
.then_some(Line { start, end })
}).collect::<Vec<_>>()
}).collect::<Vec<_>>();
(!segments.is_empty()).then_some(BoundarySource { segments, curves })
}
fn boundary_sources(scene: &Scene, plane: WorkingPlane) -> FxHashMap<Handle, BoundarySource> {
// Read native curve geometry, not shaded meshes, face isolines, temporary
// construction-history overlays or camera-clipped display segments.
scene.document.entities().filter(|entity| eligible(scene, entity)).filter_map(|entity| {
let (source, loops, _) = profile_geometry(entity)?;
let curves = loops.iter().flat_map(|ring| curves_on_plane(source, ring, plane)).collect();
boundary_source(curves).map(|source| (entity.common().handle, source))
}).collect()
}
fn selected_rings(sources: &FxHashMap<Handle, BoundarySource>, point: [f64; 2]) -> Option<Vec<Vec<[f64; 2]>>> {
let rings = boundary_faces(sources, BOUNDARY_TOLERANCE);
let loops = boundary_loops(sources, &rings)?;
let tolerance = Tolerance::new(BOUNDARY_TOLERANCE);
let outer = rings.iter().enumerate()
.filter(|(index, _)| contains(&loops[*index], point, tolerance))
.min_by(|(_, left), (_, right)| signed_area(left).abs().total_cmp(&signed_area(right).abs()))?
.0;
let depths = ring_nesting_depths(&rings);
let boundary = &loops[outer];
let mut result = vec![rings[outer].clone()];
for (index, ring) in rings.iter().enumerate() {
if index != outer && depths.get(index) == depths.get(outer).map(|depth| depth + 1).as_ref()
&& ring.first().is_some_and(|point| contains(boundary, *point, tolerance))
{
result.push(ring.clone());
}
}
Some(result)
}
fn boundary_loops(
sources: &FxHashMap<Handle, BoundarySource>, rings: &[Vec<[f64; 2]>],
) -> Option<Vec<Vec<Curve>>> {
let exterior = (0..rings.len()).map(|index| index == 0).collect::<Vec<_>>();
let paths = exact_hatch_paths(rings, &exterior, sources, BOUNDARY_TOLERANCE);
if paths.len() != rings.len() {
return None;
}
paths.iter().map(|path| {
path.edges.iter().map(crate::entities::hatch::edge_curve).collect::<Option<Vec<_>>>()
}).collect()
}
fn boundary_region(
sources: &FxHashMap<Handle, BoundarySource>, rings: &[Vec<[f64; 2]>], plane: WorkingPlane,
) -> Option<EntityType> {
let loops = boundary_loops(sources, rings)?;
let kernel_plane = Plane::from_axes(plane.origin.to_array(), plane.x.to_array(), plane.y.to_array());
let sheet = brep::planar_region(kernel_plane, &loops)?;
let document = crate::scene::convert::acis_export::solid_to_sat(&sheet)?;
let mut region = Region::new();
region.acis_data = AcisData::from_sat(&document.to_sat_string());
region.point_of_reference = Vector3::new(plane.origin.x, plane.origin.y, plane.origin.z);
region.wires = loops.iter().map(|ring| {
let mut points = Vec::new();
for curve in ring {
let skip = usize::from(!points.is_empty());
points.extend(curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
.into_iter().skip(skip).map(|point| {
let world = kernel_plane.point_at(point);
Vector3::new(world[0], world[1], world[2])
}));
}
Wire::from_points(points)
}).collect();
Some(EntityType::Region(region))
}
fn face_at_point(scene: &Scene, point: DVec3, preferred: Option<Handle>) -> Option<(Handle, FaceKey, &Body)> {
let mut handles = scene.solid_models.keys().copied().collect::<Vec<_>>();
handles.sort_by_key(|handle| handle.value());
for handle in handles {
if preferred.is_some_and(|preferred| preferred != handle) {
continue;
}
let Some(entity) = scene.document.get_entity(handle) else { continue; };
if !matches!(entity, EntityType::Solid3D(_)) || !eligible(scene, entity) {
continue;
}
let body = &scene.solid_models[&handle];
if let Some(face) = brep::planar_face_at_point(body, point.to_array(), BOUNDARY_TOLERANCE) {
return Some((handle, face, body));
}
}
None
}
fn profile_owner(scene: &Scene, plane: WorkingPlane, anchor: DVec3) -> Option<(Handle, DVec3)> {
let mut handles = scene.solid_models.keys().copied().collect::<Vec<_>>();
handles.sort_by_key(|handle| handle.value());
for handle in handles {
let Some(entity) = scene.document.get_entity(handle) else { continue; };
if !matches!(entity, EntityType::Solid3D(_)) || !eligible(scene, entity) {
continue;
}
let body = &scene.solid_models[&handle];
for face in body.face_keys() {
let Some(profile) = brep::planar_face_profile(body, face) else { continue; };
let outward = DVec3::from_array(profile.outward);
if outward.dot(plane.z).abs() < 1.0 - 1e-9
|| profile.plane.distance_to(anchor.to_array())?.abs() > BOUNDARY_TOLERANCE
{
continue;
}
let local = profile.plane.project(anchor.to_array())?;
let boundary = profile.loops.into_iter().flatten().collect::<Vec<_>>();
if contains(&boundary, local, Tolerance::new(BOUNDARY_TOLERANCE)) {
return Some((handle, outward));
}
}
}
None
}
/// A supplied curve handle means explicit edge/preselection. An absent handle
/// means a bounded-area pick; closed profiles therefore retain their distinct
/// edge and interior behaviors. A solid handle prefers its exact trimmed face.
pub fn resolve_target(
scene: &Scene, handle: Option<Handle>, point: DVec3, plane: WorkingPlane, offset: bool,
) -> Result<PresspullTarget, String> {
if !point.is_finite() {
return Err("PRESSPULL: the selected point is not finite.".to_owned());
}
if let Some(handle) = handle {
let entity = scene.document.get_entity(handle)
.ok_or("PRESSPULL: the selected object no longer exists.")?;
if !eligible(scene, entity) {
return Err("PRESSPULL: select a visible object on an unlocked layer.".to_owned());
}
if let Some((source_plane, _, closed)) = profile_geometry(entity) {
let profile_plane = working_plane(source_plane);
let local = profile_plane.to_local(point);
let anchor = profile_plane.to_world(DVec3::new(local.x, local.y, 0.0));
let attached = closed.then(|| profile_owner(scene, profile_plane, anchor)).flatten();
let owner = attached.map(|(owner, _)| owner);
let direction = attached.map(|(_, outward)| outward).unwrap_or(profile_plane.z);
return Ok(PresspullTarget {
kind: PresspullTargetKind::Profile { entity: entity.clone(), source: Some(handle), owner },
anchor, direction,
});
}
if !matches!(entity, EntityType::Solid3D(_)) {
return Err("PRESSPULL: select a planar curve, bounded area or planar solid face.".to_owned());
}
}
if let Some((owner, face, body)) = face_at_point(scene, point, handle) {
let profile = brep::planar_face_profile(body, face)
.ok_or("PRESSPULL: the selected solid face has no valid planar boundary.")?;
let face_plane = working_plane(profile.plane);
let local = face_plane.to_local(point);
let anchor = face_plane.to_world(DVec3::new(local.x, local.y, 0.0));
let direction = DVec3::from_array(profile.outward);
if !offset {
let mut sources = boundary_sources(scene, face_plane);
if let Some(source) = boundary_source(profile.loops.into_iter().flatten().collect()) {
sources.insert(owner, source);
}
if let Some(rings) = selected_rings(&sources, [local.x, local.y]) {
let bounded_by_drawing = rings.iter().any(|ring| {
ring_source_handles(ring, &sources).into_iter().any(|source| source != owner)
});
if bounded_by_drawing {
let entity = boundary_region(&sources, &rings, face_plane)
.ok_or("PRESSPULL: the selected boundary could not be reconstructed exactly.")?;
return Ok(PresspullTarget {
kind: PresspullTargetKind::Profile { entity, source: None, owner: Some(owner) },
anchor, direction,
});
}
}
}
return Ok(PresspullTarget {
kind: PresspullTargetKind::Face { handle: owner, face, body: body.clone(), offset },
anchor, direction,
});
}
if handle.is_some() {
return Err("PRESSPULL: select inside a trimmed planar face, outside its holes.".to_owned());
}
let local = plane.to_local(point);
if local.z.abs() > BOUNDARY_TOLERANCE {
return Err("PRESSPULL: the bounded area is not on the current working plane.".to_owned());
}
let sources = boundary_sources(scene, plane);
let rings = selected_rings(&sources, [local.x, local.y])
.ok_or("PRESSPULL: no closed boundary contains the selected point.")?;
let entity = boundary_region(&sources, &rings, plane)
.ok_or("PRESSPULL: the selected boundary could not be reconstructed exactly.")?;
Ok(PresspullTarget {
kind: PresspullTargetKind::Profile { entity, source: None, owner: None },
anchor: point, direction: plane.z,
})
}
/// Curves only: mouse movement never triangulates a preview body.
pub fn preview_wires(target: &PresspullTarget, distance: f64, color: [f32; 4], isolines: usize) -> Vec<WireModel> {
if !distance.is_finite() || distance.abs() <= 1e-9 {
return Vec::new();
}
if let PresspullTargetKind::Face { body, face, offset: true, .. } = &target.kind {
// Offset changes the intersections with neighbouring surfaces, so a
// constant-section prism would misrepresent sloped walls and caps.
// The kernel's analytic offset performs no Boolean or triangulation;
// the command memoizes this candidate by selected targets and distance.
// Only return an overlay: failure never hides or edits the resident body.
return brep::presspull_face(body, *face, distance, brep::PresspullMode::Offset)
.map(|candidate| preview_body_wires(&candidate, color, isolines))
.unwrap_or_default();
}
let geometry = match &target.kind {
PresspullTargetKind::Profile { entity, .. } => profile_geometry(entity)
.map(|(plane, loops, _)| (plane, loops, (target.direction * distance).to_array())),
PresspullTargetKind::Face { body, face, .. } => {
// The resident source stays visible. Show only the moving face's
// exact extrusion cage; its Boolean merge happens only on commit,
// never inside pointer movement. Unrelated faces and lumps need
// no rebuilding for a normal face-extrusion preview.
brep::planar_face_profile(body, *face).map(|profile| (profile.plane, profile.loops,
(DVec3::from_array(profile.outward) * distance).to_array()))
}
};
let Some((plane, loops, direction)) = geometry else { return Vec::new(); };
let mut wires = Vec::new();
// Each loop is a lateral sheet. This wire-only overlay does not need the
// planar caps or complete solid topology of extrude_region.
for ring in loops {
let ring = brep::extrusion_profile_pieces(&ring);
let Some(body) = brep::extrude_surface(plane, &ring, direction) else { continue; };
wires.extend(preview_body_wires(&body, color, isolines));
}
wires
}
fn preview_body_wires(body: &Body, color: [f32; 4], isolines: usize) -> Vec<WireModel> {
let wireframe = brep::mesh::tessellate_wireframe(body,
brep::mesh::TessellationTolerance::new(cadkernel::tessellation::DEFAULT_ANGLE, 1e-9)
.with_isolines(isolines));
wireframe.edges.into_iter().map(|edge| edge.positions)
.chain(wireframe.isolines.into_iter().map(|line| line.positions))
.filter(|points| points.len() >= 2)
.map(|points| WireModel::solid_f64("PRESSPULL-PREVIEW".to_owned(), points, color, false))
.collect()
}
+414 -65
View File
@@ -1,7 +1,7 @@
use acadrust::entities::{EmbeddedEntity, Solid3D};
use acadrust::objects::{
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryNodeBase, SolidHistoryOperation,
SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation,
SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep,
SolidHistoryTorus,
};
@@ -67,6 +67,9 @@ pub const PROP_TWIST_ALONG_PATH: &str = "solid_history_twist_along_path";
pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path";
pub const PROP_SWEEP_LENGTH: &str = "solid_history_sweep_length";
pub const PROP_EXTRUSION_HEIGHT: &str = "solid_history_extrusion_height";
pub const PROP_EXTRUSION_DIRECTION_X: &str = "solid_history_extrusion_direction_x";
pub const PROP_EXTRUSION_DIRECTION_Y: &str = "solid_history_extrusion_direction_y";
pub const PROP_EXTRUSION_DIRECTION_Z: &str = "solid_history_extrusion_direction_z";
pub const PROP_TAPER_ANGLE: &str = "solid_history_taper_angle";
pub const PROP_REVOLVE_ANGLE: &str = "solid_history_revolve_angle";
pub const PROP_AXIS_POSITION_X: &str = "solid_history_axis_position_x";
@@ -76,6 +79,15 @@ pub const PROP_AXIS_DIRECTION_X: &str = "solid_history_axis_direction_x";
pub const PROP_AXIS_DIRECTION_Y: &str = "solid_history_axis_direction_y";
pub const PROP_AXIS_DIRECTION_Z: &str = "solid_history_axis_direction_z";
pub const PROP_PYRAMID_TYPE: &str = "solid_history_pyramid_type";
pub const PROP_LOFT_TYPE: &str = "solid_history_loft_type";
pub const PROP_LOFT_SECTION_COUNT: &str = "solid_history_loft_section_count";
pub const PROP_LOFT_NORMALS: &str = "solid_history_loft_normals";
pub const PROP_LOFT_START_DRAFT_ANGLE: &str = "solid_history_loft_start_draft_angle";
pub const PROP_LOFT_END_DRAFT_ANGLE: &str = "solid_history_loft_end_draft_angle";
pub const PROP_LOFT_START_MAGNITUDE: &str = "solid_history_loft_start_magnitude";
pub const PROP_LOFT_END_MAGNITUDE: &str = "solid_history_loft_end_magnitude";
pub const PROP_LOFT_CLOSED: &str = "solid_history_loft_closed";
pub const PROP_LOFT_PERIODIC: &str = "solid_history_loft_periodic";
pub const PROP_HISTORY: &str = "solid_history_record";
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
@@ -195,6 +207,7 @@ pub fn has_specialized_primitive_properties(
| SolidHistoryOperation::Torus(_)
| SolidHistoryOperation::Pyramid(_)
| SolidHistoryOperation::Sweep(_)
| SolidHistoryOperation::Loft(_)
| SolidHistoryOperation::Extrusion(_)
| SolidHistoryOperation::Revolve(_)
)
@@ -212,7 +225,9 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
}
SolidHistoryOperation::Cone(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Cylinder(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Sweep(value) | SolidHistoryOperation::Extrusion(value) => world_point(
SolidHistoryOperation::Sweep(value) => cadkernel::acis::sweep_history_reference_point(value)
.ok().map(glam::DVec3::from_array),
SolidHistoryOperation::Extrusion(value) => world_point(
value.base.transform,
[
value.reference_point.x,
@@ -345,15 +360,19 @@ fn extrusion_properties(
handle: acadrust::Handle,
value: &SolidHistorySweep,
) -> Vec<PropSection> {
let position = world_point(
value.base.transform,
[
value.reference_point.x,
value.reference_point.y,
value.reference_point.z,
],
)
.unwrap_or(glam::DVec3::ZERO);
let direction = matrix(value.base.transform).map(|transform| {
transform.transform_vector3(glam::DVec3::new(
value.direction.x, value.direction.y, value.direction.z,
))
}).filter(|direction| direction.is_finite());
let direction_property = |label: &str, field: &'static str, axis: usize| Property {
label: label.to_string(),
field,
value: PropValue::ReadOnly(direction.map_or_else(
|| t!("Unavailable").into_owned(),
|direction| crate::entities::common::format_length(direction[axis]),
)),
};
let (record_history, object_show_history, show_history_mode) =
history_flags(document, handle).unwrap_or((false, false, 1));
let (show_history, show_history_editable) =
@@ -379,21 +398,6 @@ fn extrusion_properties(
field: "solid_history_type",
value: PropValue::ReadOnly(t!("Extrusion").into_owned()),
},
crate::entities::common::edit_prop(
t!("Position X").as_ref(),
PROP_POSITION_X,
position.x,
),
crate::entities::common::edit_prop(
t!("Position Y").as_ref(),
PROP_POSITION_Y,
position.y,
),
crate::entities::common::edit_prop(
t!("Position Z").as_ref(),
PROP_POSITION_Z,
position.z,
),
Property {
label: t!("Height").into_owned(),
field: PROP_EXTRUSION_HEIGHT,
@@ -404,6 +408,9 @@ fn extrusion_properties(
field: PROP_TAPER_ANGLE,
value: taper,
},
direction_property(t!("Direction X").as_ref(), PROP_EXTRUSION_DIRECTION_X, 0),
direction_property(t!("Direction Y").as_ref(), PROP_EXTRUSION_DIRECTION_Y, 1),
direction_property(t!("Direction Z").as_ref(), PROP_EXTRUSION_DIRECTION_Z, 2),
],
},
PropSection {
@@ -414,7 +421,7 @@ fn extrusion_properties(
field: PROP_HISTORY,
value: PropValue::Choice {
selected: if record_history { "Record" } else { "None" }.to_string(),
options: vec!["None".to_string(), "Record".to_string()],
options: vec!["Record".to_string(), "None".to_string()],
},
},
Property {
@@ -423,7 +430,7 @@ fn extrusion_properties(
value: if show_history_editable {
PropValue::Choice {
selected: if show_history { "Yes" } else { "No" }.to_string(),
options: vec!["No".to_string(), "Yes".to_string()],
options: vec!["Yes".to_string(), "No".to_string()],
}
} else {
PropValue::ReadOnly(if show_history { "Yes" } else { "No" }.to_string())
@@ -434,6 +441,43 @@ fn extrusion_properties(
]
}
/// Native surface construction data mirrors the same local profile, placement,
/// direction and taper used by the extrusion history rebuild.
pub fn extrusion_surface_data(
value: &SolidHistorySweep,
) -> Option<acadrust::entities::SurfaceData> {
use acadrust::entities::{SurfaceData, SurfaceSweepOptions};
if value.path_entity.is_some() {
return None;
}
let profile = value.sweep_entity.clone()?;
matrix(value.base.transform)?;
matrix(value.sweep_entity_transform)?;
Some(SurfaceData::Extruded {
sweep_entity: Some(profile),
options: SurfaceSweepOptions {
draft_angle: value.draft_angle,
draft_start_distance: value.start_draft_distance,
draft_end_distance: value.end_draft_distance,
twist_angle: value.twist_angle,
scale_factor: value.scale_factor,
align_angle: value.align_angle,
sweep_entity_transform: value.sweep_entity_transform,
path_entity_transform: value.path_entity_transform,
is_solid: false,
sweep_alignment_flags: value.align_option as i16,
align_start: value.has_align_start,
bank: value.bank,
base_point_set: true,
reference_vector: value.reference_point,
..SurfaceSweepOptions::default()
},
sweep_vector: value.direction,
sweep_transform: value.base.transform,
})
}
fn sweep_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
@@ -444,7 +488,7 @@ fn sweep_properties(
let (show_history, show_history_editable) =
displayed_history_state(object_show_history, show_history_mode);
let show_value = if show_history { "Yes" } else { "No" };
let length = sweep_path_length(value)
let length = cadkernel::acis::sweep_history_path_length(value).ok()
.map(crate::entities::common::format_length)
.unwrap_or_default();
vec![
@@ -497,7 +541,7 @@ fn sweep_properties(
field: PROP_HISTORY,
value: PropValue::Choice {
selected: if record_history { "Record" } else { "None" }.to_string(),
options: vec!["None".to_string(), "Record".to_string()],
options: vec!["Record".to_string(), "None".to_string()],
},
},
Property {
@@ -506,7 +550,7 @@ fn sweep_properties(
value: if show_history_editable {
PropValue::Choice {
selected: show_value.to_string(),
options: vec!["No".to_string(), "Yes".to_string()],
options: vec!["Yes".to_string(), "No".to_string()],
}
} else {
PropValue::ReadOnly(show_value.to_string())
@@ -517,6 +561,161 @@ fn sweep_properties(
]
}
const LOFT_NORMAL_CHOICES: [&str; 7] = [
"Ruled",
"Smooth",
"First normal",
"Last normal",
"Ends normal",
"All normal",
"Use draft angles",
];
fn loft_parameters(value: &SolidHistoryLoft) -> SolidHistoryLoftParameters {
value.parameters.clone().unwrap_or_else(|| SolidHistoryLoftParameters {
// Older history predates the options extension and rebuilds as ruled.
// Displaying or editing it must not silently change it to smooth.
normals: 0,
..SolidHistoryLoftParameters::default()
})
}
pub fn loft_section_count(value: &SolidHistoryLoft) -> usize {
value.parameters.as_ref()
.filter(|parameters| !parameters.section_counts.is_empty())
.map_or(value.cross_sections.len(), |parameters| parameters.section_counts.len())
}
pub fn loft_closed_editable(value: &SolidHistoryLoft) -> bool {
loft_section_count(value) >= 3
&& !value.cross_sections.iter().any(|section| matches!(section, EmbeddedEntity::Point(_)))
}
fn loft_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistoryLoft,
) -> Vec<PropSection> {
let parameters = loft_parameters(value);
let kind = if parameters.path_entity.is_some() {
t!("Loft with path").into_owned()
} else if !value.guides.is_empty() {
t!("Loft with guides").into_owned()
} else {
t!("Loft with cross sections only").into_owned()
};
let mut geometry = vec![
Property {
label: t!("Solid type").into_owned(),
field: PROP_LOFT_TYPE,
value: PropValue::ReadOnly(kind),
},
Property {
label: t!("Number of Cross sections").into_owned(),
field: PROP_LOFT_SECTION_COUNT,
value: PropValue::ReadOnly(loft_section_count(value).to_string()),
},
Property {
label: t!("Surface Normals").into_owned(),
field: PROP_LOFT_NORMALS,
value: PropValue::Choice {
selected: LOFT_NORMAL_CHOICES.get(parameters.normals as usize)
.map_or_else(|| parameters.normals.to_string(), |name| (*name).to_string()),
options: LOFT_NORMAL_CHOICES.iter().map(|name| (*name).to_string()).collect(),
},
},
];
if parameters.normals == 6 {
geometry.extend([
Property {
label: t!("Start draft angle").into_owned(),
field: PROP_LOFT_START_DRAFT_ANGLE,
value: PropValue::EditText(crate::entities::common::format_angle(parameters.start_draft_angle)),
},
Property {
label: t!("End draft angle").into_owned(),
field: PROP_LOFT_END_DRAFT_ANGLE,
value: PropValue::EditText(crate::entities::common::format_angle(parameters.end_draft_angle)),
},
Property {
label: t!("Start magnitude").into_owned(),
field: PROP_LOFT_START_MAGNITUDE,
value: PropValue::EditText(parameters.start_magnitude.to_string()),
},
Property {
label: t!("End magnitude").into_owned(),
field: PROP_LOFT_END_MAGNITUDE,
value: PropValue::EditText(parameters.end_magnitude.to_string()),
},
]);
}
let (record_history, object_show_history, show_history_mode) =
history_flags(document, handle).unwrap_or((false, false, 1));
let (show_history, show_history_editable) =
displayed_history_state(object_show_history, show_history_mode);
let show_value = if show_history { "Yes" } else { "No" }.to_string();
let closed_value = if parameters.closed { "Yes" } else { "No" }.to_string();
let mut sections = vec![
PropSection {
title: t!("Geometry").into_owned(),
props: geometry,
},
PropSection {
title: t!("Solid History").into_owned(),
props: vec![
Property {
label: t!("History").into_owned(),
field: PROP_HISTORY,
value: PropValue::Choice {
selected: if record_history { "Record" } else { "None" }.to_string(),
options: vec!["Record".to_string(), "None".to_string()],
},
},
Property {
label: t!("Show History").into_owned(),
field: PROP_SHOW_HISTORY,
value: if show_history_editable {
PropValue::Choice {
selected: show_value,
options: vec!["Yes".to_string(), "No".to_string()],
}
} else {
PropValue::ReadOnly(show_value)
},
},
],
},
PropSection {
title: t!("Misc").into_owned(),
props: vec![
Property {
label: t!("Closed").into_owned(),
field: PROP_LOFT_CLOSED,
value: if loft_closed_editable(value) {
PropValue::Choice {
selected: closed_value,
options: vec!["Yes".to_string(), "No".to_string()],
}
} else {
PropValue::ReadOnly(closed_value)
},
},
],
},
];
if parameters.closed {
sections[2].props.push(Property {
label: t!("Periodic").into_owned(),
field: PROP_LOFT_PERIODIC,
value: PropValue::Choice {
selected: if parameters.periodic { "Yes" } else { "No" }.to_string(),
options: vec!["Yes".to_string(), "No".to_string()],
},
});
}
sections
}
fn sweep_path_length(value: &SolidHistorySweep) -> Option<f64> {
use acadrust::entities::EmbeddedEntity;
use acadrust::EntityType;
@@ -1207,6 +1406,7 @@ pub fn primitive_properties(
SolidHistoryOperation::Pyramid(value) => pyramid_properties(document, handle, value),
SolidHistoryOperation::Torus(value) => torus_properties(document, handle, value),
SolidHistoryOperation::Sweep(value) => sweep_properties(document, handle, value),
SolidHistoryOperation::Loft(value) => loft_properties(document, handle, value),
SolidHistoryOperation::Extrusion(value) => {
extrusion_properties(document, handle, value)
}
@@ -1251,6 +1451,13 @@ pub fn is_primitive_property(field: &str) -> bool {
| PROP_AXIS_DIRECTION_Y
| PROP_AXIS_DIRECTION_Z
| PROP_PYRAMID_TYPE
| PROP_LOFT_NORMALS
| PROP_LOFT_START_DRAFT_ANGLE
| PROP_LOFT_END_DRAFT_ANGLE
| PROP_LOFT_START_MAGNITUDE
| PROP_LOFT_END_MAGNITUDE
| PROP_LOFT_CLOSED
| PROP_LOFT_PERIODIC
)
}
@@ -1258,8 +1465,14 @@ pub fn is_history_choice(field: &str) -> bool {
matches!(field, PROP_HISTORY | PROP_SHOW_HISTORY)
}
pub fn is_loft_geometry_choice(field: &str) -> bool {
matches!(field, PROP_LOFT_NORMALS | PROP_LOFT_CLOSED | PROP_LOFT_PERIODIC)
}
pub fn is_specialized_property(field: &str) -> bool {
matches!(field, "solid_history_type" | PROP_BANK | PROP_SWEEP_LENGTH)
matches!(field, "solid_history_type" | PROP_BANK | PROP_SWEEP_LENGTH
| PROP_LOFT_TYPE | PROP_LOFT_SECTION_COUNT
| PROP_EXTRUSION_DIRECTION_X | PROP_EXTRUSION_DIRECTION_Y | PROP_EXTRUSION_DIRECTION_Z)
|| is_primitive_property(field)
|| is_history_choice(field)
}
@@ -1686,6 +1899,85 @@ fn apply_sweep_geometry_property(
}
}
fn apply_loft_geometry_property(
value: &mut SolidHistoryLoft,
field: &str,
text: &str,
) -> Option<bool> {
let current = loft_parameters(value);
let mut parameters = current.clone();
match field {
PROP_LOFT_NORMALS => {
let Some(index) = LOFT_NORMAL_CHOICES.iter()
.position(|name| name.eq_ignore_ascii_case(text.trim()))
else {
return Some(false);
};
parameters.normals = index as i32;
}
PROP_LOFT_START_DRAFT_ANGLE | PROP_LOFT_END_DRAFT_ANGLE => {
if parameters.normals != 6 {
return Some(false);
}
let Some(angle) = crate::entities::common::parse_angle(text)
.filter(|angle| angle.is_finite() && (0.0..=std::f64::consts::PI).contains(angle))
else {
return Some(false);
};
if field == PROP_LOFT_START_DRAFT_ANGLE {
parameters.start_draft_angle = angle;
} else {
parameters.end_draft_angle = angle;
}
}
PROP_LOFT_START_MAGNITUDE | PROP_LOFT_END_MAGNITUDE => {
if parameters.normals != 6 {
return Some(false);
}
let Some(magnitude) = text.trim().replace(',', ".").parse::<f64>().ok()
.filter(|magnitude| magnitude.is_finite() && *magnitude >= 0.0)
else {
return Some(false);
};
if field == PROP_LOFT_START_MAGNITUDE {
parameters.start_magnitude = magnitude;
} else {
parameters.end_magnitude = magnitude;
}
}
PROP_LOFT_CLOSED => {
if !loft_closed_editable(value) {
return Some(false);
}
parameters.closed = if text.trim().eq_ignore_ascii_case("Yes") {
true
} else if text.trim().eq_ignore_ascii_case("No") {
false
} else {
return Some(false);
};
}
PROP_LOFT_PERIODIC => {
if !parameters.closed {
return Some(false);
}
parameters.periodic = if text.trim().eq_ignore_ascii_case("Yes") {
true
} else if text.trim().eq_ignore_ascii_case("No") {
false
} else {
return Some(false);
};
}
_ => return None,
}
if parameters == current {
return Some(false);
}
value.parameters = Some(parameters);
Some(true)
}
fn apply_torus_position_property(
value: &mut SolidHistoryTorus,
field: &str,
@@ -1879,6 +2171,11 @@ pub fn apply_primitive_property(
return applied;
}
}
if let SolidHistoryOperation::Loft(loft_value) = operation {
if let Some(applied) = apply_loft_geometry_property(loft_value, field, value) {
return applied;
}
}
if let SolidHistoryOperation::Extrusion(extrusion_value) = operation {
if let Some(applied) =
apply_extrusion_geometry_property(extrusion_value, field, value)
@@ -2179,12 +2476,58 @@ fn embedded_entity(value: &EmbeddedEntity) -> Option<EntityType> {
EmbeddedEntity::Ellipse(value) => EntityType::Ellipse(value.clone()),
EmbeddedEntity::Spline(value) => EntityType::Spline(value.clone()),
EmbeddedEntity::LwPolyline(value) => EntityType::LwPolyline(value.clone()),
EmbeddedEntity::Region(value) => EntityType::Region(value.clone()),
EmbeddedEntity::Ray(value) => EntityType::Ray(value.clone()),
EmbeddedEntity::XLine(value) => EntityType::XLine(value.clone()),
EmbeddedEntity::Unknown { .. } => return None,
})
}
/// Visible LOFT construction curves, owned by the parent display entity.
/// Copies are transformed to WCS and never inserted into the document.
pub fn loft_visible_history_entities(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
) -> Vec<EntityType> {
let Some((_, object_show_history, show_history_mode)) = history_flags(document, handle) else {
return Vec::new();
};
if !displayed_history_state(object_show_history, show_history_mode).0 {
return Vec::new();
}
let Some(SolidHistoryOperation::Loft(value)) = document.solid_history_operation(handle) else {
return Vec::new();
};
let Some(parent) = document.get_entity(handle) else {
return Vec::new();
};
let Some(transform) = matrix(value.base.transform) else {
return Vec::new();
};
let columns = transform.to_cols_array_2d();
let affine = acadrust::types::Transform::from_matrix(
acadrust::types::Matrix4 {
m: std::array::from_fn(|row| std::array::from_fn(|column| columns[column][row])),
},
);
value.cross_sections.iter()
.chain(&value.guides)
.chain(value.parameters.as_ref().and_then(|parameters| parameters.path_entity.as_ref()))
.filter_map(|source| {
let mut entity = embedded_entity(source)?;
entity.apply_transform(&affine);
// Construction sources belong to the visible parent's layer and
// selection identity, not a deleted source or hidden source layer.
let common = entity.common_mut();
common.handle = handle;
common.owner_handle = parent.common().owner_handle;
common.layer = parent.common().layer.clone();
common.invisible = parent.common().invisible;
Some(entity)
})
.collect()
}
fn into_embedded_entity(value: EntityType) -> Option<EmbeddedEntity> {
Some(match value {
EntityType::Point(value) => EmbeddedEntity::Point(value),
@@ -2194,6 +2537,7 @@ fn into_embedded_entity(value: EntityType) -> Option<EmbeddedEntity> {
EntityType::Ellipse(value) => EmbeddedEntity::Ellipse(value),
EntityType::Spline(value) => EmbeddedEntity::Spline(value),
EntityType::LwPolyline(value) => EmbeddedEntity::LwPolyline(value),
EntityType::Region(value) => EmbeddedEntity::Region(value),
EntityType::Ray(value) => EmbeddedEntity::Ray(value),
EntityType::XLine(value) => EmbeddedEntity::XLine(value),
_ => return None,
@@ -2288,21 +2632,27 @@ fn apply_extrusion_profile_grip(
true
}
fn sweep_placement_matrices(value: &SolidHistorySweep) -> Option<(glam::DMat4, glam::DMat4)> {
let (profile, path) = cadkernel::acis::sweep_history_placements(value).ok()?;
let matrix = |place: cadkernel::brep::Placement| {
glam::DMat4::from_cols(
glam::DVec3::from_array(place.x_axis).extend(0.0),
glam::DVec3::from_array(place.y_axis).extend(0.0),
glam::DVec3::from_array(place.z_axis).extend(0.0),
glam::DVec3::from_array(place.origin).extend(1.0),
)
};
Some((matrix(profile), matrix(path)))
}
fn sweep_embedded_grips(
value: Option<&EmbeddedEntity>,
base_transform: [f64; 16],
entity_transform: [f64; 16],
transform: glam::DMat4,
first_id: usize,
) -> Vec<GripDef> {
let Some(entity) = value.and_then(embedded_entity) else {
return Vec::new();
};
let (Some(base), Some(entity_placement)) =
(matrix(base_transform), matrix(entity_transform))
else {
return Vec::new();
};
let transform = base * entity_placement;
entity
.grips()
.into_iter()
@@ -2330,8 +2680,7 @@ fn sweep_embedded_grips(
fn apply_sweep_embedded_grip(
value: &mut Option<EmbeddedEntity>,
base_transform: [f64; 16],
entity_transform: [f64; 16],
transform: glam::DMat4,
index: usize,
apply: GripApply,
) -> bool {
@@ -2341,12 +2690,7 @@ fn apply_sweep_embedded_grip(
let Some(source_grip) = entity.grips().get(index).cloned() else {
return false;
};
let (Some(base), Some(entity_placement)) =
(matrix(base_transform), matrix(entity_transform))
else {
return false;
};
let inverse = (base * entity_placement).inverse();
let inverse = transform.inverse();
if !inverse.is_finite() {
return false;
}
@@ -2666,7 +3010,15 @@ fn apply_extrusion_draft_grip(value: &mut SolidHistorySweep, apply: GripApply) -
fn extrusion_draft_rebuilds(value: &SolidHistorySweep, angle: f64) -> bool {
let mut candidate = value.clone();
candidate.draft_angle = angle;
cadkernel::acis::rebuild_body(&SolidHistoryOperation::Extrusion(candidate)).is_ok()
let Some(profile) = candidate.sweep_entity.as_ref() else {
return false;
};
let Ok((_, _, closed)) = cadkernel::acis::sweep_profile_geometry(
profile, candidate.sweep_entity_transform,
) else {
return false;
};
cadkernel::acis::rebuild_extrusion_with_mode(&candidate, !closed).is_ok()
}
fn grip(
@@ -2891,18 +3243,14 @@ pub fn primitive_grips(
);
}
SolidHistoryOperation::Sweep(value) => {
grips.extend(sweep_embedded_grips(
value.sweep_entity.as_ref(),
value.base.transform,
value.sweep_entity_transform,
GRIP_SWEEP_PROFILE_FIRST,
));
grips.extend(sweep_embedded_grips(
value.path_entity.as_ref(),
value.base.transform,
value.path_entity_transform,
GRIP_SWEEP_PATH_FIRST,
));
if let Some((profile, path)) = sweep_placement_matrices(value) {
grips.extend(sweep_embedded_grips(
value.sweep_entity.as_ref(), profile, GRIP_SWEEP_PROFILE_FIRST,
));
grips.extend(sweep_embedded_grips(
value.path_entity.as_ref(), path, GRIP_SWEEP_PATH_FIRST,
));
}
}
SolidHistoryOperation::Loft(value) => grips.extend(loft_section_grips(value)),
SolidHistoryOperation::Revolve(value) => {
@@ -2962,11 +3310,13 @@ pub fn apply_primitive_grip(
}
}
if let SolidHistoryOperation::Sweep(value) = operation {
let Some((profile, path)) = sweep_placement_matrices(value) else {
return false;
};
if let Some(index) = grip_id.checked_sub(GRIP_SWEEP_PATH_FIRST) {
return apply_sweep_embedded_grip(
&mut value.path_entity,
value.base.transform,
value.path_entity_transform,
path,
index,
apply,
);
@@ -2977,8 +3327,7 @@ pub fn apply_primitive_grip(
{
return apply_sweep_embedded_grip(
&mut value.sweep_entity,
value.base.transform,
value.sweep_entity_transform,
profile,
index,
apply,
);
+113
View File
@@ -0,0 +1,113 @@
//! SWEEP command data mapped to the shared kernel history reconstruction.
use acadrust::entities::{Surface, SurfaceData, SurfaceKind, SurfaceSweepOptions};
use acadrust::objects::{SolidHistoryNodeBase, SolidHistorySweep};
use acadrust::types::Vector3;
use acadrust::EntityType;
use cadkernel::brep::Body;
use crate::command::{ExtrudeMode, SweepOptions};
use super::sweep_model::{embedded_path, embedded_revolve_profile};
fn embedded_sweep_profile(entity: &EntityType) -> Option<(acadrust::entities::EmbeddedEntity, [f64; 16])> {
if let EntityType::Region(region) = entity {
Some((acadrust::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array()))
} else {
embedded_revolve_profile(entity)
}
}
pub fn is_sweep_profile(entity: &EntityType) -> bool {
embedded_sweep_profile(entity).is_some_and(|(profile, transform)| {
cadkernel::acis::sweep_profile_geometry(&profile, transform).is_ok()
})
}
pub fn is_sweep_path(entity: &EntityType) -> bool {
match embedded_path(entity) {
Some(acadrust::entities::EmbeddedEntity::Spline(value)) => {
value.degree > 0 && (value.control_points.len() > value.degree as usize
|| value.fit_points.len() >= 2)
}
Some(_) => crate::entities::curve::entity_curve(entity)
.is_some_and(|curve| curve.curve.length().is_finite() && curve.curve.length() > 1e-9),
None => false,
}
}
/// All selected profiles use one base point, preserving their relative offsets.
pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOptions) -> Option<SweepOptions> {
if options.base_point.is_some() {
return Some(options);
}
let geometry = profiles.iter().map(|profile| {
let (entity, transform) = embedded_sweep_profile(profile)?;
let (plane, wires, _) = cadkernel::acis::sweep_profile_geometry(&entity, transform).ok()?;
Some((plane, wires))
}).collect::<Option<Vec<_>>>()?;
options.base_point = Some(glam::DVec3::from_array(
cadkernel::brep::sweep_profile_group_base(&geometry)?,
));
Some(options)
}
pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Option<SolidHistorySweep> {
let (sweep_entity, sweep_entity_transform) = embedded_sweep_profile(profile)?;
let (plane, wires, _) = cadkernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?;
let base_point = match options.base_point {
Some(point) => point.to_array(),
None => cadkernel::brep::sweep_profile_base(plane, &wires)?,
};
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
Some(SolidHistorySweep {
base,
operation_major: 1,
sweep_entity: Some(sweep_entity),
path_entity: Some(embedded_path(path)?),
scale_factor: options.scale,
twist_angle: options.twist_angle,
align_option: u8::from(options.align),
has_align_start: true,
bank: options.bank,
sweep_entity_transform,
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: Vector3::new(base_point[0], base_point[1], base_point[2]),
..SolidHistorySweep::default()
})
}
pub fn swept_with_options(profile: &EntityType, path: &EntityType, mode: ExtrudeMode, options: SweepOptions) -> Option<Body> {
let record = sweep_record(profile, path, options)?;
cadkernel::acis::rebuild_sweep_with_mode(&record, mode == ExtrudeMode::Surface).ok()
}
/// Preserve native construction parameters alongside the sheet's saved B-rep.
pub fn swept_surface_entity(record: &SolidHistorySweep) -> EntityType {
let mut surface = Surface::new(SurfaceKind::Swept);
if let Ok(point) = cadkernel::acis::sweep_history_reference_point(record) {
surface.point_of_reference = Vector3::new(point[0], point[1], point[2]);
}
surface.surface_data = SurfaceData::Swept {
class_version: 0,
sweep_entity: record.sweep_entity.clone(),
path_entity: record.path_entity.clone(),
sweep_transform: glam::DMat4::IDENTITY.to_cols_array(),
path_transform: glam::DMat4::IDENTITY.to_cols_array(),
options: SurfaceSweepOptions {
draft_angle: record.draft_angle,
twist_angle: record.twist_angle,
scale_factor: record.scale_factor,
align_angle: record.align_angle,
sweep_entity_transform: record.sweep_entity_transform,
path_entity_transform: record.path_entity_transform,
sweep_alignment_flags: record.align_option as i16,
align_start: record.has_align_start,
bank: record.bank,
base_point_set: true,
reference_vector: record.reference_point,
..SurfaceSweepOptions::default()
},
};
EntityType::Surface(surface)
}
+23 -6
View File
@@ -12,6 +12,9 @@ use acadrust::types::{Vector2, Vector3};
use acadrust::EntityType;
use crate::entities::curve::entity_curve;
pub use super::sweep_command_model::{
is_sweep_path, is_sweep_profile, sweep_record, sweep_selection_options, swept_surface_entity, swept_with_options,
};
/// A drawn profile, as the kernel wants it: the plane it lies in and the
/// chain of pieces closing a loop in that plane.
@@ -591,18 +594,32 @@ pub fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
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::Polyline3D(value) if !value.is_closed() && value.vertices.len() >= 2 => {
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,
value
.vertices
.iter()
.map(|vertex| vertex.position)
.collect(),
points,
);
spline.flags.linear = true;
spline.flags.planar = false;
spline.flags.closed = value.is_closed();
Some(EmbeddedEntity::Spline(spline))
}
_ => None,
+128 -23
View File
@@ -810,14 +810,16 @@ impl Scene {
let Some(reference) = reference else {
return;
};
let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else {
return;
};
entity.point_of_reference = acadrust::types::Vector3::new(
let point = acadrust::types::Vector3::new(
reference.x,
reference.y,
reference.z,
);
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.point_of_reference = point,
Some(EntityType::Surface(entity)) => entity.point_of_reference = point,
_ => {}
}
}
fn copy_solid_history(&mut self, source: Handle, target: Handle) -> bool {
@@ -832,22 +834,78 @@ impl Scene {
}
pub(crate) fn delete_solid_history(&mut self, handle: Handle) {
if self.is_recording_undo() {
// The codec also clears the entity's history handle. Capture that
// link before deletion so undo reconnects the restored graph.
let before = self.document.get_entity_arc(handle);
self.record_undo_before(handle, before);
}
self.record_solid_history_before(handle);
self.document.delete_solid_history(handle);
}
fn rebuild_history_body(
&self,
handle: Handle,
operation: &acadrust::objects::SolidHistoryOperation,
) -> Option<cadkernel::brep::Body> {
use acadrust::objects::SolidHistoryOperation;
match (self.document.get_entity(handle)?, operation) {
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok()
}
(EntityType::Surface(_), SolidHistoryOperation::Loft(_))
| (EntityType::Solid3D(_), _) => cadkernel::acis::rebuild_body(operation).ok(),
_ => None,
}
}
fn history_surface_data(
&self,
handle: Handle,
operation: &acadrust::objects::SolidHistoryOperation,
) -> Option<Option<acadrust::entities::SurfaceData>> {
use acadrust::objects::SolidHistoryOperation;
match (self.document.get_entity(handle)?, operation) {
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
crate::scene::model::solid_history::extrusion_surface_data(value).map(Some)
}
(EntityType::Surface(_), SolidHistoryOperation::Loft(value)) => {
let EntityType::Surface(surface) =
crate::scene::model::loft_command_model::surface_entity(value)
else { return None; };
Some(Some(surface.surface_data))
}
(EntityType::Solid3D(_), _) => Some(None),
_ => None,
}
}
pub fn rebuild_solid_history(
&mut self,
handle: Handle,
operation: acadrust::objects::SolidHistoryOperation,
) -> bool {
self.solid_history_preview_wires.remove(&handle);
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
let Some(body) = self.rebuild_history_body(handle, &operation) else {
return false;
};
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else {
return false;
};
let Some(surface_data) = self.history_surface_data(handle, &operation) else {
return false;
};
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
return false;
};
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
{
return false;
}
self.record_solid_history_before(handle);
if self
.document
@@ -856,12 +914,21 @@ impl Scene {
{
return false;
}
let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else {
return false;
};
entity.set_sat_document(&document);
self.sync_solid_reference_point(handle);
self.register_solid_model(handle, body);
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document),
Some(EntityType::Surface(entity)) => {
entity.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string());
if let Some(data) = surface_data {
if matches!(&data, acadrust::entities::SurfaceData::Extruded { .. }) {
entity.kind = acadrust::entities::SurfaceKind::Extruded;
}
entity.surface_data = data;
}
}
_ => return false,
}
self.register_prepared_solid_model(handle, body, display);
self.solid_history_preview_wires.remove(&handle);
true
}
@@ -869,19 +936,39 @@ impl Scene {
let Some(operation) = self.document.solid_history_operation(handle).cloned() else {
return false;
};
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
let Some(body) = self.rebuild_history_body(handle, &operation) else {
return false;
};
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else {
return false;
};
let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else {
let Some(surface_data) = self.history_surface_data(handle, &operation) else {
return false;
};
entity.set_sat_document(&document);
self.sync_solid_reference_point(handle);
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
return false;
};
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
{
return false;
}
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document),
Some(EntityType::Surface(entity)) => {
entity.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string());
if let Some(data) = surface_data {
if matches!(&data, acadrust::entities::SurfaceData::Extruded { .. }) {
entity.kind = acadrust::entities::SurfaceKind::Extruded;
}
entity.surface_data = data;
}
}
_ => return false,
}
self.register_prepared_solid_model(handle, body, display);
self.solid_history_preview_wires.remove(&handle);
self.register_solid_model(handle, body);
true
}
@@ -890,9 +977,12 @@ impl Scene {
handle: Handle,
operation: acadrust::objects::SolidHistoryOperation,
) -> bool {
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
let Some(body) = self.rebuild_history_body(handle, &operation) else {
return false;
};
if self.history_surface_data(handle, &operation).is_none() {
return false;
}
if self
.document
.update_solid_history(handle, operation)
@@ -900,9 +990,6 @@ impl Scene {
{
return false;
}
let Some(EntityType::Solid3D(_)) = self.document.get_entity(handle) else {
return false;
};
self.solid_history_preview_wires.insert(
handle,
crate::scene::model::solid_model::grip_preview_wires(&body, handle),
@@ -925,6 +1012,15 @@ impl Scene {
return false;
}
if let EntityTransform::Translate(delta) = transform {
let surface_data = match (&operation, self.document.get_entity(handle)) {
(acadrust::objects::SolidHistoryOperation::Extrusion(value), Some(EntityType::Surface(_))) => {
let Some(data) = crate::scene::model::solid_history::extrusion_surface_data(value) else {
return false;
};
Some(data)
}
_ => None,
};
if self
.document
.update_solid_history(handle, operation)
@@ -932,6 +1028,11 @@ impl Scene {
{
return false;
}
if let (Some(data), Some(EntityType::Surface(entity))) =
(surface_data, self.document.get_entity_mut(handle))
{
entity.surface_data = data;
}
self.translate_solid_geometry(handle, delta.to_array());
return true;
}
@@ -945,12 +1046,16 @@ impl Scene {
value: &str,
) -> bool {
self.record_solid_history_before(handle);
crate::scene::model::solid_history::apply_history_choice(
let applied = crate::scene::model::solid_history::apply_history_choice(
&mut self.document,
handle,
field,
value,
)
);
if applied {
self.bump_entities(&[(handle, ChangeKind::Modified)]);
}
applied
}
fn apply_solid_history_grip(