forked from 1b-agent/opencadstudio-upstream
Merge reviewed modeling PRs with Unicode and PRESSPULL fixes
This commit is contained in:
commit
90761904a9
24 files changed
+3528
-498
No files matched your search
+167
-55
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user