Complete PRESSPULL selection, face editing and extrusion history

This commit is contained in:
ramox81 committed 2026-09-04 15:31:30 +03:00
1 parent 509e4b7e7f
commit 9279384e9a
16 files changed
+1284 -247

No files matched your search

Generated
+1 -1
View File
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=5565ec3f23f6629974d2a5fed5215d2b92738a58#5565ec3f23f6629974d2a5fed5215d2b92738a58"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=37a33babe7d64e05d9d4144981eeb9bc8ebf390e#37a33babe7d64e05d9d4144981eeb9bc8ebf390e"
dependencies = [
"acadrust",
"cavalier_contours",
+1 -1
View File
@@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "580678d28f11ccbefbce3f9d3390d28ef4bbde80", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "5565ec3f23f6629974d2a5fed5215d2b92738a58", features = ["acis", "offset"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "37a33babe7d64e05d9d4144981eeb9bc8ebf390e", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+5 -55
View File
@@ -3792,61 +3792,11 @@ impl OpenCADStudio {
self.refresh_properties();
}
CmdResult::PresspullEntity {
handle,
pick,
distance,
drag,
color,
} => {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_))
) {
let task = self.solid_face_presspull(handle, pick, distance, drag);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
return task;
}
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::{solid_history, sweep_model};
let result = self.tabs[i].scene.document.get_entity(handle).and_then(|entity| {
let distance = match drag {
Some(point) => sweep_model::projected_drag(entity, pick, point)?,
None => distance,
};
sweep_model::extruded(entity, distance)
});
if let Some(solid) = result {
let pending = self.begin_undo(i, "PRESSPULL", 1, true);
let history = solid_history::brep_op(&solid);
let created = self.add_solid_model(empty_solid3d(), solid, history);
let _ = color;
if !created.is_null() {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::t!("PRESSPULL: solid created.").as_ref());
if let Some(delta) = pending {
self.commit_undo_delta(i, delta);
}
}
} else {
self.command_line.push_error(
crate::t!("PRESSPULL: select a closed profile or planar solid face.")
.as_ref(),
);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
CmdResult::PresspullPick { handle, point, offset, multiple: _ } => {
self.presspull_pick(handle, point, offset);
}
CmdResult::PresspullApply { targets, distance, color: _ } => {
self.presspull_apply(targets, distance);
}
// ── REVOLVE ────────────────────────────────────────────────────
+2 -14
View File
@@ -771,22 +771,10 @@ impl OpenCADStudio {
"PRESSPULL" => {
use crate::modules::insert::solid3d_cmds::PresspullCommand;
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let mut command = PresspullCommand::new(color);
if selected.len() == 1
&& !matches!(selected[0].1, acadrust::EntityType::Solid3D(_))
{
if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
command.set_entity_pick_direction(
curve.plane.normal().map(glam::DVec3::from_array),
);
command.on_entity_pick(
selected[0].0,
glam::DVec3::from_array(curve.plane.origin),
);
}
}
command.set_isolines(self.tabs[i].scene.document.header.isolines.max(0) as usize);
command.set_preselection(self.presspull_preselection());
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
+1
View File
@@ -17,6 +17,7 @@ mod mtext_editor;
#[cfg(not(target_arch = "wasm32"))]
pub mod plugin_host;
mod properties;
mod presspull_ops;
mod recent;
pub(crate) mod settings;
mod shortcuts;
+7 -54
View File
@@ -119,6 +119,12 @@ impl super::OpenCADStudio {
entity.history_handle = None;
entity.set_sat_document(&document);
let Some(display) = self.tabs[i].scene.prepare_solid_model_display(handle, &result)
.filter(|display| display.0.complete)
else {
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original solid was retained.").as_ref());
return false;
};
self.push_undo_snapshot(i, label);
self.tabs[i].scene.delete_solid_history(handle);
if !self.tabs[i]
@@ -131,7 +137,7 @@ impl super::OpenCADStudio {
}
let history = solid_history::brep_op(&result);
self.tabs[i].scene.create_solid_history(handle, history);
self.tabs[i].scene.register_solid_model(handle, result);
self.tabs[i].scene.register_prepared_solid_model(handle, result, display);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].dirty = true;
@@ -189,59 +195,6 @@ impl super::OpenCADStudio {
Task::none()
}
pub(super) fn solid_face_presspull(
&mut self,
handle: Handle,
pick: glam::DVec3,
distance: f64,
drag: Option<glam::DVec3>,
) -> Task<Message> {
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return Task::none();
}
self.tabs[i].scene.restore_solid_models(&[handle]);
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
self.command_line
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
return Task::none();
};
let Some(face) = solid_model::nearest_planar_face(&body, pick.to_array()) else {
self.command_line
.push_error(crate::t!("Select a planar solid face.").as_ref());
return Task::none();
};
let distance = match drag {
Some(point) => {
let Some(normal) = solid_model::planar_face_normal(&body, face) else {
self.command_line
.push_error(crate::t!("Select a planar solid face.").as_ref());
return Task::none();
};
(point - pick).dot(glam::DVec3::from_array(normal))
}
None => distance,
};
if !distance.is_finite() || distance.abs() <= 1e-6 {
self.command_line.push_error(
crate::t!("PRESSPULL: drag along the selected face normal.").as_ref(),
);
return Task::none();
}
let Some(result) = cadkernel::brep::presspull(&body, face, distance) else {
self.command_line.push_error(
crate::t!("PRESSPULL: the face move would collapse or invalidate the solid.")
.as_ref(),
);
return Task::none();
};
if self.replace_solid_body(handle, result, "PRESSPULL") {
self.command_line
.push_output(crate::t!("PRESSPULL: solid updated.").as_ref());
}
Task::none()
}
/// Run a boolean over the selected solids in selection order.
pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task<Message> {
let i = self.active_tab;
+244
View File
@@ -0,0 +1,244 @@
//! PRESSPULL scene integration. Resolve first; build and validate the complete
//! batch before recording undo or changing any persistent entity.
use acadrust::entities::{AcisData, EmbeddedEntity, Solid3D, Surface, SurfaceKind, Wire};
use acadrust::objects::{DynamicBlockData, ObjectType, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistorySweep};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use cadkernel::brep::{self, Body, PlanarFaceProfile, PresspullMode};
use glam::DVec3;
use crate::scene::model::{solid_history, sweep_model};
use crate::scene::model::mesh_model::MeshLodSet;
use crate::scene::model::presspull_model::{self, PresspullTarget, PresspullTargetKind};
struct PreparedPresspull {
owner: Option<Handle>,
entity: EntityType,
body: Body,
history: SolidHistoryOperation,
display: (MeshLodSet, Vec<Wire>, [f64; 3]),
}
fn extrusion_history(entity: &EntityType, direction: DVec3, anchor: DVec3) -> Option<SolidHistoryOperation> {
if let EntityType::Region(region) = entity {
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
return Some(SolidHistoryOperation::Extrusion(SolidHistorySweep {
base,
operation_major: 1,
sweep_entity: Some(EmbeddedEntity::Region(region.clone())),
direction: Vector3::new(direction.x, direction.y, direction.z),
end_draft_distance: direction.length(),
scale_factor: 1.0,
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: Vector3::new(anchor.x, anchor.y, anchor.z),
..SolidHistorySweep::default()
}));
}
sweep_model::extrusion_history(entity, None, direction.to_array(), 0.0, anchor.to_array())
}
impl super::OpenCADStudio {
fn restore_presspull_bodies(&mut self) {
let scene = &mut self.tabs[self.active_tab].scene;
let handles = scene.document.entities().filter_map(|entity| {
matches!(entity, EntityType::Solid3D(_)).then_some(entity.common().handle)
}).collect::<Vec<_>>();
scene.restore_solid_models(&handles);
}
pub(super) fn presspull_preselection(&mut self) -> Vec<PresspullTarget> {
self.restore_presspull_bodies();
let tab = &self.tabs[self.active_tab];
let plane = tab.ucs_xform().working_plane();
tab.scene.selected_handles_in_order().into_iter().filter_map(|handle| {
let entity = tab.scene.document.get_entity(handle)?;
let (profile_plane, loops, _) = presspull_model::profile_geometry(entity)?;
let first = loops.first()?.first()?.point_at(0.0);
let anchor = DVec3::from_array(profile_plane.point_at(first));
presspull_model::resolve_target(&tab.scene, Some(handle), anchor, plane, false).ok()
}).collect()
}
pub(super) fn presspull_pick(&mut self, handle: Option<Handle>, point: DVec3, offset: bool) {
self.restore_presspull_bodies();
let i = self.active_tab;
let target = presspull_model::resolve_target(
&self.tabs[i].scene, handle, point, self.tabs[i].ucs_xform().working_plane(), offset,
);
match target {
Ok(target) => {
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.on_presspull_target(target);
}
self.tabs[i].scene.set_hover_highlight(None);
}
Err(error) => self.command_line.push_error(&error),
}
self.refresh_presspull_prompt();
}
fn refresh_presspull_prompt(&mut self) {
if let Some(command) = self.tabs[self.active_tab].active_cmd.as_ref() {
self.command_line.push_info(&command.prompt());
self.command_line.set_step_options(command.options());
}
self.sync_dyn_fields();
}
fn prepare_presspull(&self, targets: &[PresspullTarget], distance: f64) -> Result<Vec<PreparedPresspull>, String> {
if targets.is_empty() || !distance.is_finite() || distance.abs() <= 1e-9 {
return Err("PRESSPULL: enter a finite non-zero height.".into());
}
let scene = &self.tabs[self.active_tab].scene;
let mut results: Vec<(Option<Handle>, EntityType, Body, SolidHistoryOperation)> = Vec::new();
for target in targets {
let owner = match &target.kind {
PresspullTargetKind::Profile { source, entity, owner } => {
if let Some(source) = source {
if scene.is_layer_locked(*source) || scene.document.get_entity(*source) != Some(entity) {
return Err("PRESSPULL: a selected source changed. Select it again.".into());
}
}
*owner
}
PresspullTargetKind::Face { handle, .. } => Some(*handle),
};
if owner.is_some_and(|handle| scene.is_layer_locked(handle)) {
return Err("PRESSPULL: the solid is on a locked layer.".into());
}
let previous = owner.and_then(|handle| results.iter().position(|result| result.0 == Some(handle)));
let current = previous.map(|index| &results[index].2)
.or_else(|| owner.and_then(|handle| scene.solid_models.get(&handle)));
let (body, history, mut entity) = match &target.kind {
PresspullTargetKind::Profile { entity, .. } => {
let (plane, loops, closed) = presspull_model::profile_geometry(entity)
.ok_or("PRESSPULL: the profile could not be recovered exactly.")?;
if owner.is_some() {
let current = current.ok_or("PRESSPULL: the original solid could not be restored.")?;
if !closed {
return Err("PRESSPULL: an attached region must be closed.".into());
}
let profile = PlanarFaceProfile { plane, loops, outward: target.direction.to_array() };
let body = brep::presspull_region(current, &profile, distance)
.ok_or("PRESSPULL: the region could not be added to or cut from the solid.")?;
let history = solid_history::brep_op(&body);
(body, history, scene.document.get_entity(owner.unwrap()).cloned()
.ok_or("PRESSPULL: the original solid no longer exists.")?)
} else {
let direction = target.direction * distance;
let body = presspull_model::extrusion_body(entity, direction.to_array())
.ok_or("PRESSPULL: the profile could not be extruded with this height.")?;
let history = extrusion_history(entity, direction, target.anchor)
.ok_or("PRESSPULL: the editable extrusion profile could not be retained.")?;
let entity = if closed {
EntityType::Solid3D(Solid3D::new())
} else {
let mut surface = Surface::new(SurfaceKind::Extruded);
if let SolidHistoryOperation::Extrusion(record) = &history {
surface.surface_data = solid_history::extrusion_surface_data(record)
.ok_or("PRESSPULL: the surface parameters could not be retained.")?;
}
EntityType::Surface(surface)
};
(body, history, entity)
}
}
PresspullTargetKind::Face { face, offset, .. } => {
let current = current.ok_or("PRESSPULL: the original solid could not be restored.")?;
// Earlier targets in the same batch may have rebuilt face keys.
let face = if previous.is_some() {
brep::planar_face_at_point(current, target.anchor.to_array(), 1e-6)
.ok_or("PRESSPULL: an earlier selected region changed this face.")?
} else { *face };
let body = brep::presspull_face(current, face, distance,
if *offset { PresspullMode::Offset } else { PresspullMode::Extrude })
.ok_or("PRESSPULL: the requested face operation would invalidate the solid.")?;
let history = solid_history::brep_op(&body);
(body, history, scene.document.get_entity(owner.unwrap()).cloned()
.ok_or("PRESSPULL: the original solid no longer exists.")?)
}
};
let sat = crate::scene::convert::acis_export::solid_to_sat(&body)
.ok_or("PRESSPULL: the result could not be encoded. The original objects were retained.")?;
if owner.is_none() { entity.common_mut().plotstyle_flags = 2; }
match &mut entity {
EntityType::Solid3D(solid) => {
solid.history_handle = None;
solid.silhouettes.clear();
solid.set_sat_document(&sat);
}
EntityType::Surface(surface) => {
surface.history_handle = None;
surface.silhouettes.clear();
surface.acis_data = AcisData::from_sat(&sat.to_sat_string());
}
_ => return Err("PRESSPULL: unsupported result object.".into()),
}
let result = (owner, entity, body, history);
if let Some(index) = previous { results[index] = result; } else { results.push(result); }
}
results.into_iter().map(|(owner, entity, body, history)| {
let display = scene.prepare_solid_model_display(owner.unwrap_or(Handle::NULL), &body)
.filter(|display| display.0.complete && display.0.lods.iter().any(|mesh| !mesh.indices.is_empty()))
.ok_or("PRESSPULL: the complete result could not be displayed. The original objects were retained.")?;
Ok(PreparedPresspull { owner, entity, body, history, display })
}).collect()
}
pub(super) fn presspull_apply(&mut self, targets: Vec<PresspullTarget>, distance: f64) {
let i = self.active_tab;
let prepared = match self.prepare_presspull(&targets, distance) {
Ok(prepared) => prepared,
Err(error) => {
self.command_line.push_error(&error);
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.on_presspull_applied(false);
}
self.refresh_presspull_prompt();
return;
}
};
let pending = self.begin_undo(i, "PRESSPULL", prepared.len(), true);
let mut handles = Vec::new();
// All fallible geometry, persistence and display work finished above.
// No source curve is deleted by PRESSPULL.
for result in prepared {
let handle = if let Some(handle) = result.owner {
self.tabs[i].scene.delete_solid_history(handle);
self.tabs[i].scene.update_entity(result.entity);
handle
} else {
self.commit_entity_handle(result.entity).expect("validated solid/surface insertion")
};
self.tabs[i].scene.create_solid_history(handle, result.history);
// PRESSPULL keeps editable construction parameters without enabling
// recording of subsequent solid operations by default.
if result.owner.is_none() {
let document = &mut self.tabs[i].scene.document;
if let Some(graph) = document.solid_history_graph(handle) {
if let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&graph.root) {
if let DynamicBlockData::SolidHistory(history) = &mut object.data {
history.record_history = false;
}
}
}
}
self.tabs[i].scene.register_prepared_solid_model(handle, result.body, result.display);
handles.push(handle);
}
self.tabs[i].scene.deselect_all();
for handle in &handles { self.tabs[i].scene.select_entity(*handle, false); }
self.tabs[i].dirty = true;
if let Some(pending) = pending { self.commit_undo_delta(i, pending); }
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].snap_result = None;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.on_presspull_applied(true);
}
self.command_line.push_output(crate::tf!("PRESSPULL: %{count} object(s) updated.", count = handles.len()).as_ref());
self.refresh_properties();
self.refresh_presspull_prompt();
}
}
+1 -6
View File
@@ -448,12 +448,7 @@ impl OpenCADStudio {
let mut sections =
dispatch::properties_sectioned(handle, entity, &text_style_names);
if specialized_primitive {
sections.retain(|section| {
!section.props.iter().any(|property| {
property.field.starts_with("acis_")
|| property.field.starts_with("s3d_")
})
});
retain_specialized_primitive_sections(&mut sections);
}
sections.extend(
crate::scene::model::solid_history::primitive_properties(
+135 -13
View File
@@ -494,6 +494,25 @@ impl OpenCADStudio {
}
}
/// An acquired planar profile can lie away from the current UCS plane.
/// Reproject the cursor onto that profile before using it as a drag anchor
/// or looking for its supporting solid face.
fn profile_pick_point(
&self, i: usize, handle: Handle,
edit_cam: &Option<crate::scene::view::camera::Camera>,
cursor: iced::Point, bounds: iced::Rectangle,
) -> Option<glam::DVec3> {
let entity = self.tabs[i].scene.document.get_entity(handle)?;
let (plane, _, _) = crate::scene::model::presspull_model::profile_geometry(entity)?;
let normal = glam::DVec3::from_array(plane.normal()?).as_vec3();
let origin = glam::DVec3::from_array(plane.origin);
let point = match edit_cam {
Some(camera) => camera.pick_on_plane(cursor, bounds, normal, origin),
None => self.tabs[i].scene.camera.borrow().pick_on_plane(cursor, bounds, normal, origin),
};
point.is_finite().then_some(point)
}
/// Projection + hit-test wires for the active pane. Inside a floating
/// viewport (`edit_cam` Some) it returns the viewport camera and the live
/// **model** wires, so wire / hatch picking lands on the entity under the
@@ -922,6 +941,12 @@ impl OpenCADStudio {
return Task::none();
}
let i = self.active_tab;
// Modifier-driven selection must be known before cursor_plane/axis and
// drafting constraints are read, not merely before the final callback.
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
let perf_move = crate::perf::enabled();
let move_started = Instant::now();
@@ -1765,7 +1790,9 @@ impl OpenCADStudio {
// O(N) per frame and stalls the cursor on large drawings,
// so each move resets the dwell timer — `HoverDwellTick` runs the hit-test only
// once the cursor has been still for `HOVER_DWELL_MS`.
if !dragging && self.tabs[i].active_cmd.is_none() {
let deferred_command_hover = self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.needs_entity_pick() && command.entity_pick_deferred_hover());
if !dragging && (self.tabs[i].active_cmd.is_none() || deferred_command_hover) {
// On dense drawings, clearing a rollover immediately schedules a
// second full scene frame just as motion resumes. Keep the previous
// highlight until the next settled pick replaces it.
@@ -2090,21 +2117,25 @@ impl OpenCADStudio {
}
pt
};
let effective = self.tabs[i]
let effective = if needs_entity && self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
cursor_world
} else { self.tabs[i]
.active_cmd
.as_ref()
.and_then(|command| command.cursor_axis())
.and_then(|(origin, direction)| {
cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction)
})
.unwrap_or(effective);
.unwrap_or(effective) };
// Dynamic-input locked fields constrain the preview point
// (#356): a typed angle pins the direction, a typed
// distance pins the radius — the same resolution the Enter
// commit uses, so preview and commit agree. The lock wins
// over osnap/ortho/polar.
let effective = {
let locked = self.tabs[i].active_cmd.is_some()
let locked = !needs_entity && self.tabs[i].active_cmd.is_some()
&& self.tabs[i].dyn_fields.iter().any(|f| f.buffer.is_some());
if locked {
self.tabs[i].last_cursor_world = effective;
@@ -2224,6 +2255,10 @@ impl OpenCADStudio {
p.extend(cmd.on_preview_wires(effective));
}
p
} else if needs_entity && deferred_command_hover {
// The dwell callback resolves and outlines bounded areas once.
// Moving the cursor clears that outline without rebuilding it.
Vec::new()
} else if needs_entity {
let include_fills = self.tabs[i]
.active_cmd
@@ -2459,6 +2494,12 @@ impl OpenCADStudio {
pub(crate) fn on_viewport_exit(&mut self) -> Task<Message> {
let i = self.active_tab;
self.hover_dwell = None;
if self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_deferred_hover())
{
self.tabs[i].scene.clear_preview_wire();
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = false;
sel.left_press_pos = None;
@@ -2795,6 +2836,10 @@ impl OpenCADStudio {
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
let i = self.active_tab;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
// A left-click during a command resets the right-click cycle, so
// the next right-click acts as Enter again rather than opening
// the context menu.
@@ -3074,6 +3119,10 @@ impl OpenCADStudio {
pub(super) fn on_viewport_left_release(&mut self) -> Task<Message> {
let i = self.active_tab;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
// Navigation mode: end this drag but keep the tool armed for the next
// left drag (exit is Esc / another command). Mirror of the press.
@@ -3493,7 +3542,7 @@ impl OpenCADStudio {
// A click while dynamic-input fields hold typed values
// commits the CONSTRAINED point — the same resolution
// the preview shows and Enter would commit (#356).
if self.tabs[i].active_cmd.is_some()
if !needs_entity_click && self.tabs[i].active_cmd.is_some()
&& self.tabs[i].dyn_fields.iter().any(|f| f.buffer.is_some())
{
self.tabs[i].last_cursor_world = pt;
@@ -3501,7 +3550,13 @@ impl OpenCADStudio {
pt = r;
}
}
pt
if needs_entity_click && self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
raw
} else {
pt
}
};
// `world_pt` is in offset-relative (local) space, matching
@@ -3520,6 +3575,10 @@ impl OpenCADStudio {
world_pt + glam::DVec3::new(wo[0], wo[1], wo[2])
};
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
let result = if self.tabs[i]
.active_cmd
.as_ref()
@@ -3631,11 +3690,26 @@ impl OpenCADStudio {
self.tabs[i]
.scene
.solid_click_point_for(p, view_rot2, eye2, bounds, handle)
.unwrap_or(pick_wcs)
.or_else(|| self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_deferred_hover())
.then(|| self.profile_pick_point(i, handle, &edit_cam, p, bounds)).flatten())
.unwrap_or_else(|| {
let bounded_pick = self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_deferred_hover());
if bounded_pick && matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_)),
) {
// The aperture caught an edge outside its face.
// Do not reinterpret the working-plane point as
// a hit on another cap of the same body.
glam::DVec3::NAN
} else { pick_wcs }
})
} else {
pick_wcs
};
let entity_pick_direction = if uses_surface_point {
let entity_pick_direction = if uses_surface_point && entity_pick_point.is_finite() {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_))
@@ -3749,6 +3823,10 @@ impl OpenCADStudio {
}
}
result
} else if self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
self.tabs[i].active_cmd.as_mut().map(|command| command.on_point(pick_wcs))
} else if self.tabs[i]
.active_cmd
.as_ref()
@@ -4771,7 +4849,9 @@ impl OpenCADStudio {
/// ends. Repeated wheel events replace this timestamp, so the pick runs only
/// after the final zoom step settles.
pub(in crate::app) fn arm_hover_after_navigation(&mut self, i: usize) {
if i >= self.tabs.len() || self.tabs[i].active_cmd.is_some() {
if i >= self.tabs.len() || self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| !command.entity_pick_deferred_hover())
{
return;
}
let (cursor, canvas_size) = {
@@ -5062,12 +5142,19 @@ impl OpenCADStudio {
let i = dwell.tab;
// Re-check the gate — drag / command may have started
// between the move that armed the dwell and this tick.
if i >= self.tabs.len() {
if i >= self.tabs.len() || i != self.active_tab {
self.hover_dwell = None;
return Task::none();
}
self.push_ucs_to_cmd(i);
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
let deferred_command = self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.needs_entity_pick() && command.entity_pick_deferred_hover());
let navigating = self.tabs[i].scene.selection.borrow().middle_down;
if self.tabs[i].active_cmd.is_some() || navigating {
if (self.tabs[i].active_cmd.is_some() && !deferred_command) || navigating {
self.clear_navigation_hover(i);
return Task::none();
}
@@ -5158,16 +5245,51 @@ impl OpenCADStudio {
}
if hovered.is_none() {
let started = Instant::now();
hovered = self.tabs[i].scene.solid_edge_hover_hit(
hovered = if deferred_command { self.tabs[i].scene.solid_click_hit(
p, view_rot, eye, bounds, candidate_handles.as_ref(),
) } else { self.tabs[i].scene.solid_edge_hover_hit(
p,
view_rot,
eye,
bounds,
candidate_handles.as_ref(),
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
);
) };
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
}
if deferred_command {
let surface_point = hovered.and_then(|handle| {
self.tabs[i].scene.solid_click_point_for(p, view_rot, eye, bounds, handle)
.or_else(|| self.profile_pick_point(i, handle, &edit_cam, p, bounds))
});
let point = surface_point.unwrap_or(hover_world);
let is_solid = hovered.is_some_and(|handle| matches!(
self.tabs[i].scene.document.get_entity(handle), Some(acadrust::EntityType::Solid3D(_)),
));
// An aperture may catch an edge beside, rather than on, a face.
// Never send its XY-plane fallback as a 3D face pick.
if is_solid && surface_point.is_none() {
self.tabs[i].scene.set_hover_highlight(None);
self.tabs[i].scene.clear_preview_wire();
self.hover_dwell = None;
return Task::none();
}
let handles = self.tabs[i].scene.document.entities().filter_map(|entity| {
matches!(entity, acadrust::EntityType::Solid3D(_)).then_some(entity.common().handle)
}).collect::<Vec<_>>();
self.tabs[i].scene.restore_solid_models(&handles);
let show = (self.model_space.selection_preview & 2) != 0;
let previews = if show {
let tab = &mut self.tabs[i];
tab.active_cmd.as_mut().map(|command| {
command.on_deferred_entity_hover(&tab.scene, hovered, point)
}).unwrap_or_default()
} else { Vec::new() };
self.tabs[i].scene.set_hover_highlight(None);
self.tabs[i].scene.set_preview_wires(previews);
self.hover_dwell = None;
return Task::none();
}
let preview_idle = (self.model_space.selection_preview & 1) != 0;
if preview_idle {
self.tabs[i].scene.set_hover_highlight(hovered);
+34 -5
View File
@@ -1507,12 +1507,17 @@ pub enum CmdResult {
taper_angle: f64,
color: [f32; 4],
},
/// Pull a closed profile or a planar solid face by a signed distance.
PresspullEntity {
handle: Handle,
pick: DVec3,
/// Resolve a profile, bounded area, or solid face for PRESSPULL.
PresspullPick {
handle: Option<Handle>,
point: DVec3,
offset: bool,
multiple: bool,
},
/// Apply a signed distance to the resolved PRESSPULL selection.
PresspullApply {
targets: Vec<crate::scene::model::presspull_model::PresspullTarget>,
distance: f64,
drag: Option<DVec3>,
color: [f32; 4],
},
/// Revolve the profile entities around the given axis.
@@ -1937,6 +1942,19 @@ pub trait CadCommand: Send {
false
}
/// Expensive bounded-area acquisition runs once after cursor dwell, not
/// from every pointer event. The host supplies the same WCS surface pick
/// used for clicks, and clears the overlay when movement resumes.
fn entity_pick_deferred_hover(&self) -> bool {
false
}
fn on_deferred_entity_hover(
&mut self, _scene: &Scene, _handle: Option<Handle>, _point: DVec3,
) -> Vec<WireModel> {
Vec::new()
}
/// Called when the text editor closes, either because the user committed or cancelled the edit.
fn on_editor_closed(&mut self, _committed: bool) -> CmdResult {
CmdResult::Cancel
@@ -1953,6 +1971,17 @@ pub trait CadCommand: Send {
CmdResult::Cancel
}
/// Supply a resolved PRESSPULL target after an object or bounded-area pick.
fn on_presspull_target(
&mut self,
_target: crate::scene::model::presspull_model::PresspullTarget,
) {
}
/// Resume PRESSPULL after an atomic apply attempt. A failed operation keeps
/// the current targets available for another distance or selection undo.
fn on_presspull_applied(&mut self, _success: bool) {}
/// Host callback after `CmdResult::CommitLiveEntity`: records the handle the
/// new live entity was assigned so later `UpdateLiveEntity` results can
/// target it.
+299 -45
View File
@@ -10,6 +10,7 @@ use crate::command::{
LoftSectionSelection, SelectionEntity, SweepOptions, WorkingPlane,
};
use crate::scene::WireModel;
use crate::scene::model::presspull_model::{PresspullTarget, PresspullTargetKind};
use crate::t;
// ── EXTRUDE command ────────────────────────────────────────────────────────
@@ -515,19 +516,125 @@ impl CadCommand for ExtrudeCommand {
// ── PRESSPULL command ─────────────────────────────────────────────────────
pub struct PresspullCommand {
picked: Option<(acadrust::Handle, DVec3)>,
direction: Option<DVec3>,
step: PresspullStep,
targets: Vec<PresspullTarget>,
ctrl: bool,
shift: bool,
isolines: usize,
target_generation: u64,
preview_key: Option<(u64, u64)>,
preview_cache: Vec<WireModel>,
working_plane: WorkingPlane,
hover_key: Option<(u64, Option<Handle>, [u64; 3], bool)>,
hover_cache: Vec<WireModel>,
color: [f32; 4],
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PresspullStep {
Pick,
Multiple,
Height,
}
impl PresspullCommand {
pub fn new(color: [f32; 4]) -> Self {
Self {
picked: None,
direction: None,
step: PresspullStep::Pick,
targets: Vec::new(),
ctrl: false,
shift: false,
isolines: 0,
target_generation: 0,
preview_key: None,
preview_cache: Vec::new(),
working_plane: WorkingPlane::default(),
hover_key: None,
hover_cache: Vec::new(),
color,
}
}
pub fn set_isolines(&mut self, isolines: usize) {
if self.isolines != isolines {
self.isolines = isolines;
self.invalidate_preview();
}
}
pub fn set_preselection(&mut self, targets: Vec<PresspullTarget>) {
self.targets.clear();
self.step = PresspullStep::Pick;
self.invalidate_preview();
for target in targets {
self.on_presspull_target(target);
}
}
fn invalidate_preview(&mut self) {
self.target_generation = self.target_generation.wrapping_add(1);
self.preview_key = None;
self.preview_cache.clear();
self.hover_key = None;
self.hover_cache.clear();
}
fn has_target(&self, target: &PresspullTarget) -> bool {
self.targets.iter().any(|selected| match (&selected.kind, &target.kind) {
(
PresspullTargetKind::Profile { source: Some(first), .. },
PresspullTargetKind::Profile { source: Some(second), .. },
) => first == second,
(
PresspullTargetKind::Face { handle: first, face: first_face, .. },
PresspullTargetKind::Face { handle: second, face: second_face, .. },
) => first == second && first_face == second_face,
(
PresspullTargetKind::Profile { source: None, entity: first, owner: first_owner },
PresspullTargetKind::Profile { source: None, entity: second, owner: second_owner },
) => first_owner == second_owner && first == second,
_ => false,
})
}
fn pick(&self, handle: Option<Handle>, point: DVec3) -> CmdResult {
if !point.is_finite() {
return CmdResult::NeedPoint;
}
CmdResult::PresspullPick {
handle: handle.filter(|handle| !handle.is_null()),
point,
offset: self.ctrl,
multiple: self.step == PresspullStep::Multiple || self.shift,
}
}
fn height(&self, point: DVec3) -> Option<f64> {
let target = self.targets.last()?;
let distance = (point - target.anchor).dot(target.direction);
distance.is_finite().then_some(distance)
}
fn finish(&self, distance: f64) -> CmdResult {
if self.targets.is_empty() || !distance.is_finite() || distance.abs() <= 1e-6 {
return CmdResult::NeedPoint;
}
CmdResult::PresspullApply {
targets: self.targets.clone(),
distance,
color: self.color,
}
}
fn undo_selection(&mut self) -> CmdResult {
if self.targets.pop().is_some() {
self.invalidate_preview();
}
if self.targets.is_empty() {
self.step = PresspullStep::Pick;
}
CmdResult::NeedPoint
}
}
impl CadCommand for PresspullCommand {
@@ -536,15 +643,37 @@ impl CadCommand for PresspullCommand {
}
fn prompt(&self) -> String {
if self.picked.is_none() {
t!("PRESSPULL Select a closed profile or planar solid face:").into_owned()
} else {
t!("PRESSPULL Signed distance:").into_owned()
match self.step {
PresspullStep::Pick => {
t!("PRESSPULL Select object or bounded area (Enter to finish):").into_owned()
}
PresspullStep::Multiple => {
t!("PRESSPULL Select additional objects or bounded areas [Undo] (Enter for height):").into_owned()
}
PresspullStep::Height => {
t!("PRESSPULL Specify signed extrusion height or [Multiple/Undo]:").into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
PresspullStep::Pick => vec![CmdOption::enter("Done")],
PresspullStep::Multiple => {
vec![CmdOption::new("Undo", "UNDO"), CmdOption::enter("Height")]
}
PresspullStep::Height => {
vec![CmdOption::new("Multiple", "MULTIPLE"), CmdOption::new("Undo", "UNDO")]
}
}
}
fn needs_entity_pick(&self) -> bool {
self.picked.is_none()
self.step != PresspullStep::Height || self.shift
}
fn entity_pick_accepts_points(&self) -> bool {
true
}
fn entity_pick_includes_fills(&self) -> bool {
@@ -555,69 +684,166 @@ impl CadCommand for PresspullCommand {
true
}
fn set_entity_pick_direction(&mut self, direction: Option<DVec3>) {
self.direction = direction.and_then(|value| value.try_normalize());
fn set_ctrl(&mut self, ctrl: bool) {
self.ctrl = ctrl;
}
fn set_working_plane(&mut self, plane: WorkingPlane) {
if self.working_plane.origin != plane.origin || self.working_plane.x != plane.x
|| self.working_plane.y != plane.y
{
self.hover_key = None;
self.hover_cache.clear();
}
self.working_plane = plane;
}
fn set_shift(&mut self, shift: bool) {
self.shift = shift;
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn entity_pick_deferred_hover(&self) -> bool {
self.needs_entity_pick()
}
fn on_deferred_entity_hover(
&mut self, scene: &crate::scene::Scene, handle: Option<Handle>, point: DVec3,
) -> Vec<WireModel> {
if !self.needs_entity_pick() {
return Vec::new();
}
let key = (scene.geometry_epoch, handle, point.to_array().map(f64::to_bits), self.ctrl);
if self.hover_key == Some(key) {
return self.hover_cache.clone();
}
self.hover_key = Some(key);
self.hover_cache.clear();
let Ok(target) = crate::scene::model::presspull_model::resolve_target(
scene, handle, point, self.working_plane, self.ctrl,
) else { return Vec::new(); };
let geometry = match target.kind {
PresspullTargetKind::Profile { entity, .. } =>
crate::scene::model::presspull_model::profile_geometry(&entity)
.map(|(plane, loops, _)| (plane, loops)),
PresspullTargetKind::Face { body, face, .. } =>
cadkernel::brep::planar_face_profile(&body, face)
.map(|profile| (profile.plane, profile.loops)),
};
if let Some((plane, loops)) = geometry {
self.hover_cache = loops.into_iter().flatten().filter_map(|curve| {
let points = curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
.into_iter().map(|point| plane.point_at(point)).collect::<Vec<_>>();
(points.len() >= 2).then(|| {
let mut wire = WireModel::solid_f64(
"PRESSPULL-BOUNDARY-HOVER".to_owned(), points, [1.0, 0.65, 0.1, 1.0], false,
);
wire.line_weight_px = 2.0;
wire
})
}).collect();
}
self.hover_cache.clone()
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
if !self.needs_entity_pick() {
return CmdResult::NeedPoint;
}
self.picked = Some((handle, point));
CmdResult::NeedPoint
self.pick(Some(handle), point)
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
let Some((handle, pick)) = self.picked else {
return CmdResult::NeedPoint;
};
let distance = self
.direction
.map(|direction| (point - pick).dot(direction))
.unwrap_or_else(|| point.distance(pick));
if !distance.is_finite() || distance.abs() <= 1e-6 {
return CmdResult::NeedPoint;
if self.needs_entity_pick() {
return self.pick(None, point);
}
CmdResult::PresspullEntity {
handle,
pick,
distance,
drag: Some(point),
color: self.color,
self.height(point).map_or(CmdResult::NeedPoint, |height| self.finish(height))
}
fn on_presspull_target(&mut self, mut target: PresspullTarget) {
let Some(direction) = target.direction.try_normalize() else {
return;
};
if !target.anchor.is_finite() || self.has_target(&target) {
return;
}
target.direction = direction;
self.targets.push(target);
if self.step != PresspullStep::Multiple {
self.step = PresspullStep::Height;
}
self.invalidate_preview();
}
fn on_presspull_applied(&mut self, success: bool) {
if success {
self.targets.clear();
self.step = PresspullStep::Pick;
self.invalidate_preview();
} else if !self.targets.is_empty() {
self.step = PresspullStep::Height;
}
}
fn wants_text_input(&self) -> bool {
self.picked.is_some()
true
}
fn point_step_accepts_keywords(&self) -> bool {
self.needs_entity_pick()
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let (handle, pick) = self.picked?;
crate::entities::common::parse_typed_length(text)
.filter(|distance| distance.abs() > 1e-6)
.map(|distance| CmdResult::PresspullEntity {
handle,
pick,
distance,
drag: None,
color: self.color,
})
let text = text.trim();
if text.is_empty() {
return Some(self.on_enter());
}
match text.trim_start_matches('_').to_ascii_uppercase().as_str() {
"M" | "MULTIPLE" if !self.targets.is_empty() => {
self.step = PresspullStep::Multiple;
return Some(CmdResult::NeedPoint);
}
"U" | "UNDO" => return Some(self.undo_selection()),
_ => {}
}
// The shared picker resolves coordinates and hexadecimal handles.
// Do not consume either as a distance while selecting targets.
if self.needs_entity_pick() || text.starts_with("0x") || text.starts_with("0X") {
return None;
}
crate::entities::common::parse_typed_length(text).map(|height| self.finish(height))
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
if self.step == PresspullStep::Multiple && !self.targets.is_empty() {
self.step = PresspullStep::Height;
CmdResult::NeedPoint
} else {
CmdResult::Cancel
}
}
fn on_undo_step(&mut self) -> Option<CmdResult> {
(!self.targets.is_empty()).then(|| self.undo_selection())
}
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
Some((self.picked?.1, self.direction?))
if self.needs_entity_pick() {
return None;
}
let target = self.targets.last()?;
Some((target.anchor, target.direction))
}
fn resolved_anchor(&self) -> Option<DVec3> {
self.targets.last().map(|target| target.anchor)
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
let (_, anchor) = self.picked?;
let (anchor, _) = self.cursor_axis()?;
Some(crate::command::DynSpec {
anchor: crate::command::DynAnchor::Point(anchor),
fields: vec![crate::command::DynFieldSpec::new(
@@ -629,8 +855,36 @@ impl CadCommand for PresspullCommand {
}
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
let (_, anchor) = self.picked?;
Some((cursor - anchor).dot(self.direction?))
if self.needs_entity_pick() {
return None;
}
self.height(cursor)
}
fn dyn_commit_as_text(&self) -> bool {
!self.needs_entity_pick()
}
fn on_preview_wires(&mut self, cursor: DVec3) -> Vec<WireModel> {
if self.needs_entity_pick() {
return Vec::new();
}
let Some(height) = self.height(cursor).filter(|height| height.abs() > 1e-6) else {
return Vec::new();
};
let key = (self.target_generation, height.to_bits());
if self.preview_key != Some(key) {
self.preview_cache = self.targets.iter().flat_map(|target| {
crate::scene::model::presspull_model::preview_wires(
target,
height,
self.color,
self.isolines,
)
}).collect();
self.preview_key = Some(key);
}
self.preview_cache.clone()
}
}
+18 -1
View File
@@ -2861,7 +2861,24 @@ impl Scene {
solid: cadkernel::brep::Body,
display: (MeshLodSet, Vec<acadrust::entities::Wire>, [f64; 3]),
) -> bool {
let (set, wires, center) = display;
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 = handle.value().to_string();
mesh.color = color;
}
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 {
+1
View File
@@ -11,6 +11,7 @@ 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;
+376
View File
@@ -0,0 +1,376 @@
//! 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 ring_curves(ring: &[[f64; 2]]) -> Vec<Curve> {
ring.iter().copied().zip(ring.iter().copied().cycle().skip(1))
.take(ring.len()).map(|(start, end)| Curve::Line(Line { start, end })).collect()
}
fn selected_rings(sources: &FxHashMap<Handle, BoundarySource>, point: [f64; 2]) -> Option<Vec<Vec<[f64; 2]>>> {
let rings = boundary_faces(sources, BOUNDARY_TOLERANCE);
let tolerance = Tolerance::new(BOUNDARY_TOLERANCE);
let outer = rings.iter().enumerate()
.filter(|(_, ring)| contains(&ring_curves(ring), 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 = ring_curves(&rings[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_region(
sources: &FxHashMap<Handle, BoundarySource>, rings: &[Vec<[f64; 2]>], plane: WorkingPlane,
) -> Option<EntityType> {
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;
}
let loops = paths.iter().map(|path| {
path.edges.iter().map(crate::entities::hatch::edge_curve).collect::<Option<Vec<_>>>()
}).collect::<Option<Vec<_>>>()?;
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. Calling extrude_region here would perform
// Boolean differences for holes even though this overlay only needs wires.
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()
}
+69 -28
View File
@@ -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";
@@ -357,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) =
@@ -391,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,
@@ -416,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 {
@@ -426,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 {
@@ -435,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())
@@ -446,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,
@@ -1439,7 +1471,8 @@ pub fn is_loft_geometry_choice(field: &str) -> bool {
pub fn is_specialized_property(field: &str) -> bool {
matches!(field, "solid_history_type" | PROP_BANK | PROP_SWEEP_LENGTH
| PROP_LOFT_TYPE | PROP_LOFT_SECTION_COUNT)
| 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)
}
@@ -2977,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(
+90 -24
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,34 +834,74 @@ 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 {
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 loft_surface_data = match (&operation, self.document.get_entity(handle)) {
(acadrust::objects::SolidHistoryOperation::Loft(record), Some(EntityType::Surface(_))) => {
let EntityType::Surface(surface) = crate::scene::model::loft_command_model::surface_entity(record)
else { return false; };
Some(surface.surface_data)
}
(_, Some(EntityType::Solid3D(_))) => None,
_ => 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(_))
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
{
return false;
@@ -876,7 +918,12 @@ impl Scene {
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) = loft_surface_data { entity.surface_data = data; }
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,
}
@@ -889,16 +936,20 @@ 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(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(_))
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
{
return false;
@@ -907,10 +958,11 @@ impl Scene {
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 acadrust::objects::SolidHistoryOperation::Loft(record) = &operation {
if let EntityType::Surface(updated) = crate::scene::model::loft_command_model::surface_entity(record) {
entity.surface_data = updated.surface_data;
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,
@@ -925,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)
@@ -935,9 +990,6 @@ impl Scene {
{
return false;
}
if !matches!(self.document.get_entity(handle), Some(EntityType::Solid3D(_) | EntityType::Surface(_))) {
return false;
}
self.solid_history_preview_wires.insert(
handle,
crate::scene::model::solid_model::grip_preview_wires(&body, handle),
@@ -960,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)
@@ -967,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;
}