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