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
+152
-133
@@ -353,6 +353,17 @@ impl OpenCADStudio {
|
||||
self.reset_tracking_after_point();
|
||||
self.push_ucs_to_cmd(i);
|
||||
}
|
||||
if let StepInput::EntityPick(handle, _) = &input {
|
||||
if self.tabs[i].active_cmd.as_ref()
|
||||
.is_some_and(|command| command.inject_before_entity_pick())
|
||||
{
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() {
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
command.inject_picked_entity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let StepInput::SelectionComplete(handles) = &input {
|
||||
let entities = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
@@ -3781,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 ────────────────────────────────────────────────────
|
||||
@@ -3979,107 +3940,165 @@ impl OpenCADStudio {
|
||||
self.refresh_properties();
|
||||
}
|
||||
// ── SWEEP ─────────────────────────────────────────────────────
|
||||
CmdResult::SweepEntity {
|
||||
profile_handle,
|
||||
CmdResult::SweepEntities {
|
||||
handles,
|
||||
path_handle,
|
||||
mode,
|
||||
options,
|
||||
color: _,
|
||||
} => {
|
||||
if self.reject_locked_edit(i, profile_handle)
|
||||
|| self.reject_locked_edit(i, path_handle)
|
||||
{
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Task::none();
|
||||
}
|
||||
use crate::command::ExtrudeMode;
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
use crate::scene::model::{solid_history, sweep_model};
|
||||
|
||||
let profile_ent = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(profile_handle)
|
||||
.cloned();
|
||||
let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned();
|
||||
let result = profile_ent.zip(path_ent).and_then(|(profile, path)| {
|
||||
let reference_point = sweep_model::profile_of(&profile)?.plane.origin;
|
||||
let solid = sweep_model::swept(&profile, &path)?;
|
||||
let history = sweep_model::sweep_history(
|
||||
&profile,
|
||||
&path,
|
||||
0.0,
|
||||
reference_point,
|
||||
)
|
||||
.unwrap_or_else(|| solid_history::brep_op(&solid));
|
||||
Some((solid, history))
|
||||
});
|
||||
|
||||
if let Some((solid, history)) = result {
|
||||
let pending = self.begin_undo(i, "SWEEP", 1, true);
|
||||
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||
if !created.is_null() {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::t!("SWEEP: solid created.").as_ref());
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
use crate::scene::model::sweep_model;
|
||||
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
|
||||
let path = self.tabs[i].scene.document.get_entity(path_handle).cloned();
|
||||
let mut profiles = Vec::new();
|
||||
let mut failed = 0usize;
|
||||
for handle in handles {
|
||||
if handle == path_handle || self.reject_locked_edit(i, handle) {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() {
|
||||
profiles.push((handle, entity));
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
let selection = profiles.iter().map(|(_, entity)| entity.clone()).collect::<Vec<_>>();
|
||||
let options = sweep_model::sweep_selection_options(&selection, options);
|
||||
let pending = if profiles.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.begin_undo(i, "SWEEP", profiles.len(), true)
|
||||
};
|
||||
let mut created_handles = Vec::new();
|
||||
let mut consumed = Vec::new();
|
||||
for (handle, profile) in profiles {
|
||||
let result = path.as_ref().zip(options).and_then(|(path, options)| {
|
||||
let record = sweep_model::sweep_record(&profile, path, options)?;
|
||||
let (_, _, closed) = cadkernel::acis::sweep_profile_geometry(
|
||||
record.sweep_entity.as_ref()?, record.sweep_entity_transform,
|
||||
).ok()?;
|
||||
let surface = mode == ExtrudeMode::Surface || !closed;
|
||||
let body = cadkernel::acis::rebuild_sweep_with_mode(&record, surface).ok()?;
|
||||
Some((body, record, surface))
|
||||
});
|
||||
let Some((body, record, surface)) = result else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let created = if surface {
|
||||
self.add_surface_model(sweep_model::swept_surface_entity(&record), body)
|
||||
} else {
|
||||
self.add_solid_model(
|
||||
empty_solid3d(), body,
|
||||
acadrust::objects::SolidHistoryOperation::Sweep(record),
|
||||
)
|
||||
};
|
||||
if created.is_null() {
|
||||
failed += 1;
|
||||
} else {
|
||||
created_handles.push(created);
|
||||
if delete_sources {
|
||||
consumed.push(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !created_handles.is_empty() {
|
||||
consumed.sort_unstable_by_key(|handle| handle.value());
|
||||
consumed.dedup();
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for handle in &created_handles {
|
||||
self.tabs[i].scene.select_entity(*handle, true);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"SWEEP: created {created} object(s); {failed} source(s) could not be swept.",
|
||||
created = created_handles.len(), failed = failed
|
||||
).as_ref());
|
||||
} else {
|
||||
self.command_line.push_error(crate::t!("SWEEP: could not sweep the profile along the path.").as_ref());
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
// ── LOFT ──────────────────────────────────────────────────────
|
||||
CmdResult::LoftEntities { handles, color: _ } => {
|
||||
if let Some(handle) = handles
|
||||
.iter()
|
||||
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
|
||||
.copied()
|
||||
{
|
||||
self.reject_locked_edit(i, handle);
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Task::none();
|
||||
}
|
||||
CmdResult::LoftEntities { sections, guides, path, mode, options, color: _ } => {
|
||||
use crate::command::LoftSectionSelection;
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
use crate::scene::model::{solid_history, sweep_model};
|
||||
|
||||
let profiles: Vec<acadrust::EntityType> = handles
|
||||
.iter()
|
||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).cloned())
|
||||
.collect();
|
||||
let result = sweep_model::loft_history(&profiles)
|
||||
.and_then(|history| {
|
||||
cadkernel::acis::rebuild_body(&history)
|
||||
.ok()
|
||||
.map(|solid| (solid, history))
|
||||
})
|
||||
.or_else(|| {
|
||||
sweep_model::lofted(&profiles).map(|solid| {
|
||||
let history = solid_history::brep_op(&solid);
|
||||
(solid, history)
|
||||
})
|
||||
});
|
||||
if let Some((solid, history)) = result {
|
||||
let pending = self.begin_undo(i, "LOFT", 1, true);
|
||||
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||
if !created.is_null() {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::t!("LOFT: solid created.").as_ref());
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
use crate::scene::model::loft_command_model;
|
||||
let mut sources = sections.iter().flat_map(|section| match section {
|
||||
LoftSectionSelection::Entity(handle) => vec![*handle],
|
||||
LoftSectionSelection::Join(handles) => handles.clone(),
|
||||
LoftSectionSelection::Point(_) => Vec::new(),
|
||||
}).collect::<Vec<_>>();
|
||||
sources.sort_unstable_by_key(|handle| handle.value());
|
||||
sources.dedup();
|
||||
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
|
||||
let locked = delete_sources && sources.iter().any(|handle| self.tabs[i].scene.is_layer_locked(*handle));
|
||||
let available = sources.iter().chain(guides.iter()).copied().chain(path)
|
||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
|
||||
.cloned().map(|entity| (handle, entity))).collect::<Vec<_>>();
|
||||
let result = if locked {
|
||||
Err("LOFT: a source is on a locked layer; disable source deletion or unlock it.".to_string())
|
||||
} else {
|
||||
self.command_line.push_error(crate::t!("LOFT: select at least two closed profiles.").as_ref());
|
||||
loft_command_model::record(§ions, &guides, path, &available, mode, options)
|
||||
.and_then(|record| cadkernel::acis::rebuild_loft_with_options(&record).map(|body| (body, record)))
|
||||
};
|
||||
match result {
|
||||
Ok((body, record)) => {
|
||||
let surface = record.parameters.as_ref().is_some_and(|settings| settings.surface);
|
||||
let dirty_before = self.tabs[i].dirty;
|
||||
let pending = self.begin_undo(i, "LOFT", 1, true);
|
||||
let created = if surface {
|
||||
let handle = self.add_surface_model(loft_command_model::surface_entity(&record), body);
|
||||
if !handle.is_null() {
|
||||
self.tabs[i].scene.create_solid_history(handle,
|
||||
acadrust::objects::SolidHistoryOperation::Loft(record));
|
||||
}
|
||||
handle
|
||||
} else {
|
||||
self.add_solid_model(empty_solid3d(), body,
|
||||
acadrust::objects::SolidHistoryOperation::Loft(record))
|
||||
};
|
||||
if created.is_null() {
|
||||
// The creation helper already removed its provisional
|
||||
// result. Discard its empty undo recording as well.
|
||||
self.tabs[i].scene.take_undo_recording();
|
||||
self.tabs[i].dirty = dirty_before;
|
||||
self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref());
|
||||
return Task::none();
|
||||
} else {
|
||||
if delete_sources { self.tabs[i].scene.erase_entities(&sources); }
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entity(created, true);
|
||||
self.tabs[i].dirty = true;
|
||||
let message = if surface {
|
||||
crate::t!("LOFT: surface created.")
|
||||
} else { crate::t!("LOFT: solid created.") };
|
||||
self.command_line.push_output(message.as_ref());
|
||||
}
|
||||
if let Some(pending) = pending { self.commit_undo_delta(i, pending); }
|
||||
}
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
CmdResult::SolidEdgeBlend {
|
||||
|
||||
+19
-16
@@ -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));
|
||||
}
|
||||
@@ -814,7 +802,16 @@ impl OpenCADStudio {
|
||||
"SWEEP" => {
|
||||
use crate::modules::insert::solid3d_cmds::SweepCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let cmd = SweepCommand::new(color);
|
||||
let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize;
|
||||
let mut cmd = SweepCommand::new(color, isolines);
|
||||
let selected = self.tabs[i].scene.selected_handles_in_order()
|
||||
.into_iter()
|
||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
|
||||
.cloned().map(|entity| (handle, entity)))
|
||||
.collect::<Vec<_>>();
|
||||
if !selected.is_empty() {
|
||||
cmd.set_preselection(selected);
|
||||
}
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
@@ -823,7 +820,13 @@ impl OpenCADStudio {
|
||||
"LOFT" => {
|
||||
use crate::modules::insert::solid3d_cmds::LoftCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let cmd = LoftCommand::new(color);
|
||||
let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize;
|
||||
let selected = self.tabs[i].scene.selected_handles_in_order().into_iter()
|
||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
|
||||
.cloned().map(|entity| (handle, entity))).collect();
|
||||
let available = self.tabs[i].scene.document.entities()
|
||||
.map(|entity| (entity.common().handle, entity.clone())).collect();
|
||||
let cmd = LoftCommand::new(color, isolines, crate::command::ExtrudeMode::Solid, selected, available);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
@@ -830,6 +830,8 @@ impl OpenCADStudio {
|
||||
| "BLIPMODE"
|
||||
| "SPLFRAME"
|
||||
| "DELOBJ"
|
||||
| "SOLIDHIST"
|
||||
| "SHOWHIST"
|
||||
| "PLINEGEN"
|
||||
| "PSLTSCALE"
|
||||
| "DISPSILH"
|
||||
@@ -926,6 +928,37 @@ impl OpenCADStudio {
|
||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||
);
|
||||
} else {
|
||||
if matches!(name.as_str(), "SHOWHIST" | "SOLIDHIST") {
|
||||
let current = if name == "SHOWHIST" {
|
||||
self.tabs[i].scene.document.header.show_solid_history.clamp(0, 2)
|
||||
} else {
|
||||
i16::from(self.tabs[i].scene.document.header.record_solid_history)
|
||||
};
|
||||
if let Some(value) = &value {
|
||||
let maximum = if name == "SHOWHIST" { 2 } else { 1 };
|
||||
match value.parse::<i16>().ok().filter(|value| (0..=maximum).contains(value)) {
|
||||
Some(mode) => {
|
||||
if current != mode {
|
||||
self.push_undo_snapshot(i, &name);
|
||||
if name == "SHOWHIST" {
|
||||
self.tabs[i].scene.document.header.show_solid_history = mode;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
} else {
|
||||
self.tabs[i].scene.document.header.record_solid_history = mode != 0;
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
self.command_line.push_output(&format!("{name} = {mode}"));
|
||||
}
|
||||
None => self.command_line.push_error(&format!("{name}: expected an integer from 0 to {maximum}.")),
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
||||
if name == "FRAME" || frame_kind.is_some() {
|
||||
let current = frame_kind.map_or_else(
|
||||
|
||||
@@ -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;
|
||||
|
||||
+14
-56
@@ -35,8 +35,10 @@ impl super::OpenCADStudio {
|
||||
let Some(handle) = self.commit_entity_handle(entity) else {
|
||||
return Handle::NULL;
|
||||
};
|
||||
let require_complete = matches!(history, SolidHistoryOperation::Loft(_));
|
||||
self.tabs[i].scene.create_solid_history(handle, history);
|
||||
if !self.tabs[i].scene.register_solid_model(handle, solid) {
|
||||
if !self.tabs[i].scene.register_solid_model(handle, solid)
|
||||
|| (require_complete && self.tabs[i].scene.meshes.get(&handle).is_none_or(|mesh| !mesh.complete)) {
|
||||
// A command result is not successful until it has renderable
|
||||
// geometry. Roll the new entity back so DELOBJ cannot consume a
|
||||
// visible source in exchange for an invisible result.
|
||||
@@ -69,7 +71,10 @@ impl super::OpenCADStudio {
|
||||
let Some(handle) = self.commit_entity_handle(entity) else {
|
||||
return Handle::NULL;
|
||||
};
|
||||
if !self.tabs[i].scene.register_solid_model(handle, surface) {
|
||||
let require_complete = self.tabs[i].scene.document.get_entity(handle).is_some_and(|entity|
|
||||
matches!(entity, EntityType::Surface(value) if value.kind == acadrust::entities::SurfaceKind::Lofted));
|
||||
if !self.tabs[i].scene.register_solid_model(handle, surface)
|
||||
|| (require_complete && self.tabs[i].scene.meshes.get(&handle).is_none_or(|mesh| !mesh.complete)) {
|
||||
self.tabs[i].scene.rollback_new_entities(&[handle]);
|
||||
return Handle::NULL;
|
||||
}
|
||||
@@ -114,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]
|
||||
@@ -126,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;
|
||||
@@ -184,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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -2001,7 +2001,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
self.push_undo_snapshot(i, "CHPROP");
|
||||
|
||||
if crate::scene::model::solid_history::is_history_choice(field) {
|
||||
if crate::scene::model::solid_history::is_loft_geometry_choice(field) {
|
||||
// These choices change the generated body, not just history
|
||||
// flags. Use the same transactional rebuild as numeric edits.
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
continue;
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.apply_solid_history_property(handle, field, &value);
|
||||
}
|
||||
} else if crate::scene::model::solid_history::is_history_choice(field) {
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
continue;
|
||||
|
||||
+157
-16
@@ -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
|
||||
@@ -2298,6 +2333,15 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.set_hover_highlight(None);
|
||||
}
|
||||
let hover_handle = hovered.unwrap_or(acadrust::Handle::NULL);
|
||||
let wants_entity = self.tabs[i].active_cmd.as_ref()
|
||||
.is_some_and(|command| command.wants_hover_entity(hover_handle));
|
||||
if wants_entity {
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(hover_handle).cloned() {
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
command.inject_hover_entity(hover_handle, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
let shift = self.shift_down;
|
||||
let mut p = self.tabs[i]
|
||||
.active_cmd
|
||||
@@ -2450,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;
|
||||
@@ -2786,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.
|
||||
@@ -3065,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.
|
||||
@@ -3143,6 +3201,19 @@ impl OpenCADStudio {
|
||||
is_added_polyline_vertex(original, current, target.grip_id)
|
||||
.then_some(target.grip_id)
|
||||
});
|
||||
// Keep originals available until every history shape has a valid
|
||||
// final display. A rejected rebuild cancels the entire gesture.
|
||||
let history_handles: Vec<_> = self.grip_preview_handles.iter().copied()
|
||||
.filter(|handle| self.tabs[i].scene.document.solid_history_operation(*handle).is_some())
|
||||
.collect();
|
||||
for handle in history_handles {
|
||||
if !self.tabs[i].scene.finalize_solid_history(handle) {
|
||||
self.cancel_active_grip_edit();
|
||||
self.command_line.push_error(crate::t!("The edited shape could not be displayed; the original geometry was restored.").as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_grip = None;
|
||||
// Commit the grip drag as one undoable group, then put every
|
||||
// edited entity back into the resident tessellation.
|
||||
@@ -3151,9 +3222,6 @@ impl OpenCADStudio {
|
||||
let history_originals = std::mem::take(&mut self.grip_history_originals);
|
||||
let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty);
|
||||
if !handles.is_empty() {
|
||||
for &handle in &handles {
|
||||
let _ = self.tabs[i].scene.finalize_solid_history(handle);
|
||||
}
|
||||
if !originals.is_empty() {
|
||||
self.push_entity_group_history(
|
||||
i,
|
||||
@@ -3474,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;
|
||||
@@ -3482,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
|
||||
@@ -3501,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()
|
||||
@@ -3612,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(_))
|
||||
@@ -3730,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()
|
||||
@@ -4752,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) = {
|
||||
@@ -5043,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();
|
||||
}
|
||||
@@ -5139,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);
|
||||
|
||||
Reference in new issue
Block a user