forked from 1b-agent/opencadstudio-upstream
Complete LOFT commands, surfaces, previews and editable history
This commit is contained in:
1 parent
8e3d851133
commit
479bf87988
15 files changed
+1505
-161
No files matched your search
Generated
+3
-3
@@ -71,8 +71,8 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.5.3"
|
||||
source = "git+https://[email protected]/HakanSeven12/cadcodec.git?rev=305e93d4cf8ff70ce8449015edb201c2765fcc04#305e93d4cf8ff70ce8449015edb201c2765fcc04"
|
||||
version = "0.5.4"
|
||||
source = "git+https://[email protected]/HakanSeven12/cadcodec.git?rev=da65c079396eb6e48d0f5d411eb0f87477479ee2#da65c079396eb6e48d0f5d411eb0f87477479ee2"
|
||||
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=209df414c9c0434af00edabc5529c6c774091f0e#209df414c9c0434af00edabc5529c6c774091f0e"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=5dbd3dfb9a29af3112c082a8de2d0587ec1b7b16#5dbd3dfb9a29af3112c082a8de2d0587ec1b7b16"
|
||||
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 = "305e93d4cf8ff70ce8449015edb201c2765fcc04", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "209df414c9c0434af00edabc5529c6c774091f0e", features = ["acis", "offset"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "da65c079396eb6e48d0f5d411eb0f87477479ee2", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "5dbd3dfb9a29af3112c082a8de2d0587ec1b7b16", 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://[email protected]/HakanSeven12/cadcodec.git", rev = "305e93d4cf8ff70ce8449015edb201c2765fcc04" }
|
||||
acadrust = { git = "https://[email protected]/HakanSeven12/cadcodec.git", rev = "da65c079396eb6e48d0f5d411eb0f87477479ee2" }
|
||||
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
||||
+60
-40
@@ -4082,53 +4082,73 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── 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 {
|
||||
|
||||
@@ -832,7 +832,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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3152,6 +3152,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.
|
||||
@@ -3160,9 +3173,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,
|
||||
|
||||
+55
-2
@@ -1165,6 +1165,55 @@ impl Default for SweepOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -1484,9 +1533,13 @@ pub enum CmdResult {
|
||||
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.
|
||||
|
||||
@@ -6,8 +6,8 @@ use acadrust::{entities::Solid3D, EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{
|
||||
CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, SweepOptions,
|
||||
WorkingPlane,
|
||||
CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, LoftOptions,
|
||||
LoftSectionSelection, SelectionEntity, SweepOptions, WorkingPlane,
|
||||
};
|
||||
use crate::scene::WireModel;
|
||||
use crate::t;
|
||||
@@ -1606,15 +1606,400 @@ impl CadCommand for SweepCommand {
|
||||
// ── LOFT command ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct LoftCommand {
|
||||
profiles: Vec<acadrust::Handle>,
|
||||
state: LoftState,
|
||||
undo: Vec<LoftState>,
|
||||
available: Vec<(Handle, EntityType)>,
|
||||
injected_entity: Option<EntityType>,
|
||||
entity_revision: u64,
|
||||
preview_key: Option<LoftPreviewKey>,
|
||||
preview_cache: Vec<WireModel>,
|
||||
preview_error: Option<String>,
|
||||
notice: Option<String>,
|
||||
isolines: usize,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum LoftStep {
|
||||
Sections,
|
||||
Join,
|
||||
Point,
|
||||
Mode,
|
||||
Options,
|
||||
Guides,
|
||||
Path,
|
||||
Settings,
|
||||
Normals,
|
||||
StartDraft,
|
||||
EndDraft,
|
||||
StartMagnitude,
|
||||
EndMagnitude,
|
||||
StartContinuity,
|
||||
EndContinuity,
|
||||
StartBulge,
|
||||
EndBulge,
|
||||
Closed,
|
||||
AlignDirection,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LoftState {
|
||||
step: LoftStep,
|
||||
return_step: LoftStep,
|
||||
sections: Vec<LoftSectionSelection>,
|
||||
join: Vec<Handle>,
|
||||
guides: Vec<Handle>,
|
||||
path: Option<Handle>,
|
||||
mode: ExtrudeMode,
|
||||
options: LoftOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct LoftPreviewKey {
|
||||
sections: Vec<LoftSectionSelection>,
|
||||
guides: Vec<Handle>,
|
||||
path: Option<Handle>,
|
||||
mode: ExtrudeMode,
|
||||
options: LoftOptions,
|
||||
entity_revision: u64,
|
||||
}
|
||||
|
||||
impl LoftCommand {
|
||||
pub fn new(color: [f32; 4]) -> Self {
|
||||
Self {
|
||||
profiles: Vec::new(),
|
||||
pub fn new(
|
||||
color: [f32; 4],
|
||||
isolines: usize,
|
||||
mode: ExtrudeMode,
|
||||
seed: Vec<(Handle, EntityType)>,
|
||||
available: Vec<(Handle, EntityType)>,
|
||||
) -> Self {
|
||||
let mut command = Self {
|
||||
state: LoftState {
|
||||
step: LoftStep::Sections,
|
||||
return_step: LoftStep::Sections,
|
||||
sections: Vec::new(),
|
||||
join: Vec::new(),
|
||||
guides: Vec::new(),
|
||||
path: None,
|
||||
mode,
|
||||
options: LoftOptions::default(),
|
||||
},
|
||||
undo: Vec::new(),
|
||||
available,
|
||||
injected_entity: None,
|
||||
entity_revision: 0,
|
||||
preview_key: None,
|
||||
preview_cache: Vec::new(),
|
||||
preview_error: None,
|
||||
notice: None,
|
||||
isolines,
|
||||
color,
|
||||
};
|
||||
// Preserve supplied selection order; a loft is not an unordered set.
|
||||
for (handle, entity) in seed {
|
||||
if !handle.is_null()
|
||||
&& !command.contains_section(handle)
|
||||
&& crate::scene::model::loft_command_model::is_section(&entity)
|
||||
{
|
||||
command.store_entity(handle, entity);
|
||||
command.state.sections.push(LoftSectionSelection::Entity(handle));
|
||||
}
|
||||
}
|
||||
let invalid_point = command.state.sections.iter().enumerate().find_map(|(index, section)| {
|
||||
(command.is_point_section(section)
|
||||
&& ((index > 0 && index + 1 < command.state.sections.len())
|
||||
|| (index > 0 && command.is_point_section(&command.state.sections[index - 1]))))
|
||||
.then_some(index)
|
||||
});
|
||||
if let Some(index) = invalid_point {
|
||||
command.state.sections.truncate(index);
|
||||
command.notice = Some(t!("Point cross-sections are allowed only at the first or last end, with a curve between point ends.").into_owned());
|
||||
} else if command.state.sections.len() >= 2 {
|
||||
command.state.step = LoftStep::Options;
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
fn store_entity(&mut self, handle: Handle, entity: EntityType) {
|
||||
if let Some((_, stored)) = self.available.iter_mut().find(|(id, _)| *id == handle) {
|
||||
*stored = entity;
|
||||
} else {
|
||||
self.available.push((handle, entity));
|
||||
}
|
||||
self.entity_revision = self.entity_revision.wrapping_add(1);
|
||||
}
|
||||
|
||||
fn contains_section(&self, handle: Handle) -> bool {
|
||||
self.state.sections.iter().any(|section| match section {
|
||||
LoftSectionSelection::Entity(id) => *id == handle,
|
||||
LoftSectionSelection::Join(handles) => handles.contains(&handle),
|
||||
LoftSectionSelection::Point(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_point_section(&self, section: &LoftSectionSelection) -> bool {
|
||||
match section {
|
||||
LoftSectionSelection::Point(_) => true,
|
||||
LoftSectionSelection::Entity(handle) => self.available.iter().any(|(id, entity)| {
|
||||
id == handle && matches!(entity, EntityType::Point(_))
|
||||
}),
|
||||
LoftSectionSelection::Join(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_ends(&self) -> (bool, bool) {
|
||||
(
|
||||
self.state.sections.first().is_some_and(|section| self.is_point_section(section)),
|
||||
self.state.sections.last().is_some_and(|section| self.is_point_section(section)),
|
||||
)
|
||||
}
|
||||
|
||||
fn point_setting_next(&self, step: LoftStep) -> LoftStep {
|
||||
match step {
|
||||
LoftStep::StartContinuity if self.point_ends().1 => LoftStep::EndContinuity,
|
||||
LoftStep::StartBulge if self.point_ends().1 => LoftStep::EndBulge,
|
||||
_ => LoftStep::Options,
|
||||
}
|
||||
}
|
||||
|
||||
fn remember(&mut self) {
|
||||
self.undo.push(self.state.clone());
|
||||
self.notice = None;
|
||||
}
|
||||
|
||||
fn enter_step(&mut self, step: LoftStep) {
|
||||
self.remember();
|
||||
self.state.step = step;
|
||||
}
|
||||
|
||||
fn invalid(&mut self, message: String) -> CmdResult {
|
||||
self.notice = Some(message);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn can_close(&self) -> bool {
|
||||
self.state.sections.len() >= 3
|
||||
&& !self.state.sections.iter().any(|section| self.is_point_section(section))
|
||||
}
|
||||
|
||||
fn key(&self) -> LoftPreviewKey {
|
||||
LoftPreviewKey {
|
||||
sections: self.state.sections.clone(),
|
||||
guides: self.state.guides.clone(),
|
||||
path: self.state.path,
|
||||
mode: self.state.mode,
|
||||
options: self.state.options,
|
||||
entity_revision: self.entity_revision,
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_preview(&mut self, key: LoftPreviewKey) -> Vec<WireModel> {
|
||||
if self.preview_key.as_ref() == Some(&key) {
|
||||
return self.preview_cache.clone();
|
||||
}
|
||||
self.preview_cache.clear();
|
||||
self.preview_error = None;
|
||||
if key.sections.len() >= 2 {
|
||||
match crate::scene::model::loft_command_model::build_body(
|
||||
&key.sections,
|
||||
&key.guides,
|
||||
key.path,
|
||||
&self.available,
|
||||
key.mode,
|
||||
key.options,
|
||||
) {
|
||||
Ok(body) => {
|
||||
// Only edges/isolines: do not triangulate faces on mouse movement.
|
||||
self.preview_cache = preview_body_wires(&body, self.color, self.isolines);
|
||||
}
|
||||
Err(error) => self.preview_error = Some(error),
|
||||
}
|
||||
}
|
||||
self.preview_key = Some(key);
|
||||
self.preview_cache.clone()
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> CmdResult {
|
||||
if self.state.sections.len() < 2 {
|
||||
return self.invalid(t!("LOFT requires at least two cross-sections.").into_owned());
|
||||
}
|
||||
if self.state.sections.iter().enumerate().any(|(index, section)| {
|
||||
self.is_point_section(section)
|
||||
&& ((index > 0 && index + 1 < self.state.sections.len())
|
||||
|| (index > 0 && self.is_point_section(&self.state.sections[index - 1])))
|
||||
}) {
|
||||
return self.invalid(t!("Point cross-sections are allowed only at the first or last end, with a curve between point ends.").into_owned());
|
||||
}
|
||||
if self.state.options.closed && !self.can_close() {
|
||||
return self.invalid(t!("A closed loft requires at least three curve cross-sections and no point ends.").into_owned());
|
||||
}
|
||||
let key = self.key();
|
||||
self.cached_preview(key);
|
||||
if let Some(error) = self.preview_error.clone() {
|
||||
return self.invalid(error);
|
||||
}
|
||||
CmdResult::LoftEntities {
|
||||
sections: self.state.sections.clone(),
|
||||
guides: self.state.guides.clone(),
|
||||
path: self.state.path,
|
||||
mode: self.state.mode,
|
||||
options: self.state.options,
|
||||
color: self.color,
|
||||
}
|
||||
}
|
||||
|
||||
fn undo_selection(&mut self) -> CmdResult {
|
||||
if let Some(state) = self.undo.pop() {
|
||||
self.state = state;
|
||||
} else if !self.state.sections.is_empty() {
|
||||
self.state.sections.pop();
|
||||
self.state.step = LoftStep::Sections;
|
||||
self.state.options.closed = false;
|
||||
}
|
||||
self.notice = None;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn add_section(&mut self, handle: Handle) -> CmdResult {
|
||||
if self.state.sections.len() >= 2 && self.point_ends().1 {
|
||||
return self.invalid(t!("A point must remain the last cross-section; use Undo to change the end.").into_owned());
|
||||
}
|
||||
if self.contains_section(handle) {
|
||||
return self.invalid(t!("That cross-section is already selected.").into_owned());
|
||||
}
|
||||
if self.available.iter().find(|(id, _)| *id == handle)
|
||||
.is_none_or(|(_, entity)| !crate::scene::model::loft_command_model::is_section(entity))
|
||||
{
|
||||
return self.invalid(t!("Select a supported planar curve or region cross-section.").into_owned());
|
||||
}
|
||||
let section = LoftSectionSelection::Entity(handle);
|
||||
let point = self.is_point_section(§ion);
|
||||
if point && self.point_ends().1 {
|
||||
return self.invalid(t!("Add a curve cross-section between point ends.").into_owned());
|
||||
}
|
||||
self.remember();
|
||||
self.state.sections.push(section);
|
||||
if point {
|
||||
self.state.options.closed = false;
|
||||
if self.state.sections.len() >= 2 {
|
||||
self.state.step = LoftStep::Options;
|
||||
}
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn set_numeric_setting(&mut self, text: &str) -> bool {
|
||||
let angle = matches!(self.state.step, LoftStep::StartDraft | LoftStep::EndDraft);
|
||||
let bulge = matches!(self.state.step, LoftStep::StartBulge | LoftStep::EndBulge);
|
||||
let number = if angle {
|
||||
crate::entities::common::parse_angle(text)
|
||||
} else {
|
||||
text.trim().replace(',', ".").parse::<f64>().ok()
|
||||
};
|
||||
let Some(number) = number.filter(|number| number.is_finite()) else {
|
||||
return false;
|
||||
};
|
||||
if (angle && !(0.0..=std::f64::consts::PI).contains(&number))
|
||||
|| (!angle && number < 0.0)
|
||||
|| (bulge && number < 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.remember();
|
||||
self.state.step = match self.state.step {
|
||||
LoftStep::StartDraft => {
|
||||
self.state.options.start_draft_angle = number;
|
||||
LoftStep::EndDraft
|
||||
}
|
||||
LoftStep::EndDraft => {
|
||||
self.state.options.end_draft_angle = number;
|
||||
LoftStep::Settings
|
||||
}
|
||||
LoftStep::StartMagnitude => {
|
||||
self.state.options.start_magnitude = number;
|
||||
LoftStep::EndMagnitude
|
||||
}
|
||||
LoftStep::EndMagnitude => {
|
||||
self.state.options.end_magnitude = number;
|
||||
LoftStep::Settings
|
||||
}
|
||||
LoftStep::StartBulge => {
|
||||
self.state.options.start_bulge = number;
|
||||
self.point_setting_next(LoftStep::StartBulge)
|
||||
}
|
||||
LoftStep::EndBulge => {
|
||||
self.state.options.end_bulge = number;
|
||||
LoftStep::Options
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
fn keyword(text: &str, choices: &[&str]) -> Option<usize> {
|
||||
let keyword = text.trim().trim_start_matches('_').replace([' ', '-'], "").to_ascii_uppercase();
|
||||
let keyword = match keyword.as_str() {
|
||||
"CROSSSECTIONSONLY" => "CROSSSECTIONS",
|
||||
"BULGEMAGNITUDE" => "BULGE",
|
||||
_ => keyword.as_str(),
|
||||
};
|
||||
if keyword.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(index) = choices.iter().position(|choice| *choice == keyword) {
|
||||
return Some(index);
|
||||
}
|
||||
let mut matches = choices.iter().enumerate().filter(|(_, choice)| choice.starts_with(keyword));
|
||||
let index = matches.next()?.0;
|
||||
matches.next().is_none().then_some(index)
|
||||
}
|
||||
|
||||
fn option_prompt(&self) -> String {
|
||||
match self.state.step {
|
||||
LoftStep::Sections => format!(
|
||||
"{} ({}):", t!("LOFT Select cross-sections in order or [Point/Join/Mode/Undo] (Enter to finish)"),
|
||||
self.state.sections.len(),
|
||||
),
|
||||
LoftStep::Join => format!(
|
||||
"{} ({}):", t!("LOFT Select connected edges for one cross-section or [Undo] (Enter to finish)"),
|
||||
self.state.join.len(),
|
||||
),
|
||||
LoftStep::Point => t!("LOFT Specify point end:").into_owned(),
|
||||
LoftStep::Mode => format!("{} <{}>:", t!("LOFT Creation mode [Solid/Surface]"),
|
||||
if self.state.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" }),
|
||||
LoftStep::Options => {
|
||||
let (start, end) = self.point_ends();
|
||||
if start || end {
|
||||
t!("LOFT Enter an option [Guides/Path/Cross sections only/Settings/Continuity/Bulge magnitude/Mode/Undo] <Cross sections only>:").into_owned()
|
||||
} else {
|
||||
t!("LOFT Enter an option [Guides/Path/Cross sections only/Settings/Mode/Undo] <Cross sections only>:").into_owned()
|
||||
}
|
||||
}
|
||||
LoftStep::Guides => format!("{} ({}):", t!("LOFT Select guide curves or [Undo] (Enter to finish)"), self.state.guides.len()),
|
||||
LoftStep::Path => t!("LOFT Select path or [Undo]:").into_owned(),
|
||||
LoftStep::Settings => {
|
||||
let mut choices = vec![t!("Normals").into_owned()];
|
||||
if self.state.options.normals == 6 {
|
||||
choices.push(t!("Draft angles").into_owned());
|
||||
choices.push(t!("Magnitudes").into_owned());
|
||||
}
|
||||
if self.can_close() {
|
||||
choices.push(t!("Closed").into_owned());
|
||||
}
|
||||
choices.push(t!("Align direction").into_owned());
|
||||
choices.push(t!("Done").into_owned());
|
||||
format!("{} [{}]:", t!("LOFT Settings"), choices.join("/"))
|
||||
}
|
||||
LoftStep::Normals => t!("LOFT Surface normals [Ruled/Smooth/First normal/Last normal/Ends normal/All normal/Use draft angles]:").into_owned(),
|
||||
LoftStep::StartDraft => format!("{} <{}>:", t!("LOFT Start draft angle"), crate::entities::common::format_angle(self.state.options.start_draft_angle)),
|
||||
LoftStep::EndDraft => format!("{} <{}>:", t!("LOFT End draft angle"), crate::entities::common::format_angle(self.state.options.end_draft_angle)),
|
||||
LoftStep::StartMagnitude => format!("{} <{}>:", t!("LOFT Start magnitude"), self.state.options.start_magnitude),
|
||||
LoftStep::EndMagnitude => format!("{} <{}>:", t!("LOFT End magnitude"), self.state.options.end_magnitude),
|
||||
LoftStep::StartContinuity => format!("{} <G{}>:", t!("LOFT Start point continuity [G0/G1]"), self.state.options.start_continuity),
|
||||
LoftStep::EndContinuity => format!("{} <G{}>:", t!("LOFT End point continuity [G0/G1]"), self.state.options.end_continuity),
|
||||
LoftStep::StartBulge => format!("{} <{:.4}>:", t!("LOFT Start point bulge magnitude"), self.state.options.start_bulge),
|
||||
LoftStep::EndBulge => format!("{} <{:.4}>:", t!("LOFT End point bulge magnitude"), self.state.options.end_bulge),
|
||||
LoftStep::Closed => format!("{} <{}>:", t!("LOFT Closed [Yes/No]"), if self.state.options.closed { "Yes" } else { "No" }),
|
||||
LoftStep::AlignDirection => format!("{} <{}>:", t!("LOFT Align cross-section directions [Yes/No]"), if self.state.options.align_direction { "Yes" } else { "No" }),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1624,47 +2009,331 @@ impl CadCommand for LoftCommand {
|
||||
"LOFT"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
if self.profiles.is_empty() {
|
||||
t!("LOFT Select first cross-section:").into_owned()
|
||||
} else {
|
||||
t!(
|
||||
"LOFT Select next cross-section (%{count} selected, Enter to finish):",
|
||||
count = self.profiles.len()
|
||||
)
|
||||
.into_owned()
|
||||
let prompt = self.option_prompt();
|
||||
match &self.notice {
|
||||
Some(notice) => format!("{notice}\n{prompt}"),
|
||||
None => prompt,
|
||||
}
|
||||
}
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
let mut options = match self.state.step {
|
||||
LoftStep::Sections => vec![CmdOption::new("Point", "POINT"), CmdOption::new("Join", "JOIN"), CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")],
|
||||
LoftStep::Join | LoftStep::Guides => vec![CmdOption::enter("Done")],
|
||||
LoftStep::Mode => vec![CmdOption::new("Solid", "SOLID"), CmdOption::new("Surface", "SURFACE")],
|
||||
LoftStep::Options => {
|
||||
let mut choices = vec![CmdOption::new("Guides", "GUIDES"), CmdOption::new("Path", "PATH"), CmdOption::new("Cross sections only", "CROSSSECTIONS"), CmdOption::new("Settings", "SETTINGS")];
|
||||
let (start, end) = self.point_ends();
|
||||
if start || end {
|
||||
choices.push(CmdOption::new("Continuity", "CONTINUITY"));
|
||||
choices.push(CmdOption::new("Bulge magnitude", "BULGE"));
|
||||
}
|
||||
choices.push(CmdOption::new("Mode", "MODE"));
|
||||
choices
|
||||
}
|
||||
LoftStep::Settings => {
|
||||
let mut choices = vec![CmdOption::new("Normals", "NORMALS")];
|
||||
if self.state.options.normals == 6 {
|
||||
choices.push(CmdOption::new("Draft angles", "DRAFTANGLES"));
|
||||
choices.push(CmdOption::new("Magnitudes", "MAGNITUDES"));
|
||||
}
|
||||
if self.can_close() {
|
||||
choices.push(CmdOption::new("Closed", "CLOSED"));
|
||||
}
|
||||
choices.push(CmdOption::new("Align direction", "ALIGNDIRECTION"));
|
||||
choices.push(CmdOption::enter("Done"));
|
||||
choices
|
||||
}
|
||||
LoftStep::Normals => vec![
|
||||
CmdOption::new("Ruled", "RULED"), CmdOption::new("Smooth", "SMOOTH"),
|
||||
CmdOption::new("First normal", "FIRSTNORMAL"), CmdOption::new("Last normal", "LASTNORMAL"),
|
||||
CmdOption::new("Ends normal", "ENDSNORMAL"), CmdOption::new("All normal", "ALLNORMAL"),
|
||||
CmdOption::new("Use draft angles", "USEDRAFTANGLES"),
|
||||
],
|
||||
LoftStep::Closed | LoftStep::AlignDirection => vec![CmdOption::new("Yes", "YES"), CmdOption::new("No", "NO")],
|
||||
LoftStep::StartContinuity | LoftStep::EndContinuity => vec![CmdOption::new("G0", "G0"), CmdOption::new("G1", "G1")],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
options.push(CmdOption::new("Undo", "UNDO"));
|
||||
options
|
||||
}
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
matches!(self.state.step, LoftStep::Sections | LoftStep::Join | LoftStep::Guides | LoftStep::Path)
|
||||
}
|
||||
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
self.needs_entity_pick()
|
||||
}
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
self.needs_entity_pick()
|
||||
}
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.injected_entity = Some(entity);
|
||||
}
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
if !self.needs_entity_pick() || handle.is_null() {
|
||||
self.injected_entity = None;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
// Avoid duplicate picks.
|
||||
if !self.profiles.contains(&handle) {
|
||||
self.profiles.push(handle);
|
||||
if let Some(entity) = self.injected_entity.take() {
|
||||
self.store_entity(handle, entity);
|
||||
}
|
||||
if self.state.step == LoftStep::Sections {
|
||||
return self.add_section(handle);
|
||||
}
|
||||
if self.contains_section(handle) {
|
||||
return self.invalid(t!("A cross-section cannot also be a guide, path, or joined edge.").into_owned());
|
||||
}
|
||||
if self.available.iter().find(|(id, _)| *id == handle)
|
||||
.is_none_or(|(_, entity)| !crate::scene::model::loft_command_model::is_guide_or_path(entity))
|
||||
{
|
||||
return self.invalid(t!("Select a supported guide or path curve.").into_owned());
|
||||
}
|
||||
match self.state.step {
|
||||
LoftStep::Join => {
|
||||
if self.state.join.contains(&handle) {
|
||||
return self.invalid(t!("That edge is already selected.").into_owned());
|
||||
}
|
||||
self.remember();
|
||||
self.state.join.push(handle);
|
||||
}
|
||||
LoftStep::Guides => {
|
||||
if self.state.guides.contains(&handle) {
|
||||
return self.invalid(t!("That guide is already selected.").into_owned());
|
||||
}
|
||||
self.remember();
|
||||
self.state.guides.push(handle);
|
||||
}
|
||||
LoftStep::Path => {
|
||||
self.remember();
|
||||
self.state.path = Some(handle);
|
||||
self.state.guides.clear();
|
||||
return self.finish();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.profiles.len() >= 2
|
||||
true
|
||||
}
|
||||
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> {
|
||||
None
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.state.step == LoftStep::Point
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.profiles.len() < 2 {
|
||||
CmdResult::Cancel
|
||||
} else {
|
||||
CmdResult::LoftEntities {
|
||||
handles: self.profiles.clone(),
|
||||
color: self.color,
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if self.state.step != LoftStep::Point || !point.is_finite() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.point_ends().1 {
|
||||
return self.invalid(t!("Add a curve cross-section between point ends.").into_owned());
|
||||
}
|
||||
self.remember();
|
||||
self.state.sections.push(LoftSectionSelection::Point(point));
|
||||
self.state.options.closed = false;
|
||||
self.state.step = if self.state.sections.len() >= 2 { LoftStep::Options } else { LoftStep::Sections };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if text.trim().is_empty() {
|
||||
return Some(self.on_enter());
|
||||
}
|
||||
if Self::keyword(text, &["UNDO"]).is_some() {
|
||||
return Some(self.undo_selection());
|
||||
}
|
||||
match self.state.step {
|
||||
LoftStep::Sections => match Self::keyword(text, &["POINT", "JOIN", "MODE"]) {
|
||||
Some(0) => {
|
||||
if self.point_ends().1 {
|
||||
return Some(self.invalid(t!("Select a curve cross-section before another point end.").into_owned()));
|
||||
}
|
||||
self.enter_step(LoftStep::Point);
|
||||
}
|
||||
Some(1) => {
|
||||
if self.state.sections.len() >= 2 && self.point_ends().1 {
|
||||
return Some(self.invalid(t!("A point must remain the last cross-section; use Undo to change the end.").into_owned()));
|
||||
}
|
||||
self.enter_step(LoftStep::Join);
|
||||
self.state.join.clear();
|
||||
}
|
||||
Some(2) => {
|
||||
self.enter_step(LoftStep::Mode);
|
||||
self.state.return_step = LoftStep::Sections;
|
||||
}
|
||||
_ => return Some(self.invalid(t!("Select a cross-section or enter Point, Join, Mode, or Undo.").into_owned())),
|
||||
},
|
||||
LoftStep::Mode => match Self::keyword(text, &["SOLID", "SURFACE"]) {
|
||||
Some(mode) => {
|
||||
self.remember();
|
||||
self.state.mode = if mode == 0 { ExtrudeMode::Solid } else { ExtrudeMode::Surface };
|
||||
self.state.step = self.state.return_step;
|
||||
}
|
||||
_ => return Some(self.invalid(t!("Enter Solid or Surface.").into_owned())),
|
||||
},
|
||||
LoftStep::Options => match Self::keyword(text, &["GUIDES", "PATH", "CROSSSECTIONS", "SETTINGS", "MODE", "CONTINUITY", "BULGE"]) {
|
||||
Some(0) => {
|
||||
self.enter_step(LoftStep::Guides);
|
||||
self.state.path = None;
|
||||
}
|
||||
Some(1) => {
|
||||
self.enter_step(LoftStep::Path);
|
||||
self.state.guides.clear();
|
||||
}
|
||||
Some(2) => {
|
||||
self.remember();
|
||||
self.state.guides.clear();
|
||||
self.state.path = None;
|
||||
return Some(self.finish());
|
||||
}
|
||||
Some(3) => self.enter_step(LoftStep::Settings),
|
||||
Some(4) => {
|
||||
self.enter_step(LoftStep::Mode);
|
||||
self.state.return_step = LoftStep::Options;
|
||||
}
|
||||
Some(5) | Some(6) if !self.point_ends().0 && !self.point_ends().1 => {
|
||||
return Some(self.invalid(t!("Continuity and bulge magnitude require a point cross-section at an end.").into_owned()));
|
||||
}
|
||||
Some(5) => self.enter_step(if self.point_ends().0 { LoftStep::StartContinuity } else { LoftStep::EndContinuity }),
|
||||
Some(6) => self.enter_step(if self.point_ends().0 { LoftStep::StartBulge } else { LoftStep::EndBulge }),
|
||||
_ => return Some(self.invalid(t!("Enter one of the listed loft options.").into_owned())),
|
||||
},
|
||||
LoftStep::Settings => match Self::keyword(text, &["NORMALS", "DRAFTANGLES", "MAGNITUDES", "CLOSED", "ALIGNDIRECTION", "DONE"]) {
|
||||
Some(0) => self.enter_step(LoftStep::Normals),
|
||||
Some(1) | Some(2) if self.state.options.normals != 6 => {
|
||||
return Some(self.invalid(t!("Choose Use draft angles under Normals before editing draft angles or magnitudes.").into_owned()));
|
||||
}
|
||||
Some(1) => self.enter_step(LoftStep::StartDraft),
|
||||
Some(2) => self.enter_step(LoftStep::StartMagnitude),
|
||||
Some(3) if self.can_close() => self.enter_step(LoftStep::Closed),
|
||||
Some(3) => return Some(self.invalid(t!("Closed is available for at least three curve cross-sections without point ends.").into_owned())),
|
||||
Some(4) => self.enter_step(LoftStep::AlignDirection),
|
||||
Some(5) => self.enter_step(LoftStep::Options),
|
||||
_ => return Some(self.invalid(t!("Enter a listed loft setting.").into_owned())),
|
||||
},
|
||||
LoftStep::Normals => {
|
||||
let selected = Self::keyword(text, &["RULED", "SMOOTH", "FIRSTNORMAL", "LASTNORMAL", "ENDSNORMAL", "ALLNORMAL", "USEDRAFTANGLES"])
|
||||
.or_else(|| text.trim().parse::<usize>().ok().filter(|value| *value <= 6));
|
||||
let Some(selected) = selected else {
|
||||
return Some(self.invalid(t!("Select one of the seven surface normal options.").into_owned()));
|
||||
};
|
||||
self.remember();
|
||||
self.state.options.normals = selected as i32;
|
||||
self.state.step = LoftStep::Settings;
|
||||
}
|
||||
LoftStep::StartDraft | LoftStep::EndDraft | LoftStep::StartMagnitude | LoftStep::EndMagnitude => {
|
||||
if !self.set_numeric_setting(text) {
|
||||
return Some(self.invalid(t!("Enter a finite draft angle from 0 to 180 degrees or a non-negative magnitude.").into_owned()));
|
||||
}
|
||||
}
|
||||
LoftStep::StartContinuity | LoftStep::EndContinuity => {
|
||||
let continuity = match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() {
|
||||
"G0" => 0,
|
||||
"G1" => 1,
|
||||
_ => return Some(self.invalid(t!("Enter G0 or G1.").into_owned())),
|
||||
};
|
||||
let step = self.state.step;
|
||||
self.remember();
|
||||
if step == LoftStep::StartContinuity {
|
||||
self.state.options.start_continuity = continuity;
|
||||
} else {
|
||||
self.state.options.end_continuity = continuity;
|
||||
}
|
||||
self.state.step = self.point_setting_next(step);
|
||||
}
|
||||
LoftStep::StartBulge | LoftStep::EndBulge => {
|
||||
if !self.set_numeric_setting(text) {
|
||||
return Some(self.invalid(t!("Enter a finite bulge magnitude greater than zero.").into_owned()));
|
||||
}
|
||||
}
|
||||
LoftStep::Closed | LoftStep::AlignDirection => {
|
||||
let Some(selected) = Self::keyword(text, &["YES", "NO"]) else {
|
||||
return Some(self.invalid(t!("Enter Yes or No.").into_owned()));
|
||||
};
|
||||
if self.state.step == LoftStep::Closed && selected == 0 && !self.can_close() {
|
||||
return Some(self.invalid(t!("A closed loft requires at least three curve cross-sections and no point ends.").into_owned()));
|
||||
}
|
||||
self.remember();
|
||||
if self.state.step == LoftStep::Closed {
|
||||
self.state.options.closed = selected == 0;
|
||||
} else {
|
||||
self.state.options.align_direction = selected == 0;
|
||||
}
|
||||
self.state.step = LoftStep::Settings;
|
||||
}
|
||||
LoftStep::Point => return None,
|
||||
LoftStep::Join | LoftStep::Guides | LoftStep::Path => {
|
||||
return Some(self.invalid(t!("Select a curve or use Undo.").into_owned()));
|
||||
}
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.state.step {
|
||||
LoftStep::Sections if self.state.sections.len() >= 2 => self.enter_step(LoftStep::Options),
|
||||
LoftStep::Sections => return self.invalid(t!("LOFT requires at least two cross-sections; select another section or a point end.").into_owned()),
|
||||
LoftStep::Join if !self.state.join.is_empty() => {
|
||||
self.remember();
|
||||
self.state.sections.push(LoftSectionSelection::Join(std::mem::take(&mut self.state.join)));
|
||||
self.state.step = LoftStep::Sections;
|
||||
}
|
||||
LoftStep::Join => return self.invalid(t!("Select connected edges for the cross-section.").into_owned()),
|
||||
LoftStep::Options => return self.finish(),
|
||||
LoftStep::Guides if !self.state.guides.is_empty() => return self.finish(),
|
||||
LoftStep::Guides => return self.invalid(t!("Select at least one guide curve.").into_owned()),
|
||||
LoftStep::Mode => self.enter_step(self.state.return_step),
|
||||
LoftStep::Settings => self.enter_step(LoftStep::Options),
|
||||
LoftStep::Normals | LoftStep::Closed | LoftStep::AlignDirection | LoftStep::EndDraft | LoftStep::EndMagnitude => self.enter_step(LoftStep::Settings),
|
||||
LoftStep::StartDraft => self.enter_step(LoftStep::EndDraft),
|
||||
LoftStep::StartMagnitude => self.enter_step(LoftStep::EndMagnitude),
|
||||
LoftStep::StartContinuity | LoftStep::EndContinuity | LoftStep::StartBulge | LoftStep::EndBulge => self.enter_step(self.point_setting_next(self.state.step)),
|
||||
LoftStep::Path | LoftStep::Point => return self.invalid(t!("Specify the requested path or point, or use Undo.").into_owned()),
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
Some(self.undo_selection())
|
||||
}
|
||||
fn wants_hover_entity(&self, handle: Handle) -> bool {
|
||||
self.needs_entity_pick() && !handle.is_null()
|
||||
&& !self.available.iter().any(|(id, _)| *id == handle)
|
||||
}
|
||||
fn inject_hover_entity(&mut self, handle: Handle, entity: EntityType) {
|
||||
self.store_entity(handle, entity);
|
||||
}
|
||||
fn on_hover_entity(&mut self, handle: Handle, _point: DVec3) -> Vec<WireModel> {
|
||||
let mut key = self.key();
|
||||
if !handle.is_null() && !self.contains_section(handle) {
|
||||
if let Some((_, entity)) = self.available.iter().find(|(id, _)| *id == handle) {
|
||||
match self.state.step {
|
||||
LoftStep::Sections if crate::scene::model::loft_command_model::is_section(entity) => {
|
||||
let section = LoftSectionSelection::Entity(handle);
|
||||
let point = self.is_point_section(§ion);
|
||||
if !(self.point_ends().1 && (point || self.state.sections.len() >= 2)) {
|
||||
key.sections.push(section);
|
||||
if point {
|
||||
key.options.closed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
LoftStep::Guides if !key.guides.contains(&handle)
|
||||
&& crate::scene::model::loft_command_model::is_guide_or_path(entity) => key.guides.push(handle),
|
||||
LoftStep::Path if crate::scene::model::loft_command_model::is_guide_or_path(entity) => {
|
||||
key.path = Some(handle);
|
||||
key.guides.clear();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cached_preview(key)
|
||||
}
|
||||
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
|
||||
let mut key = self.key();
|
||||
if self.state.step == LoftStep::Point && point.is_finite()
|
||||
&& !key.sections.is_empty()
|
||||
&& !self.point_ends().1
|
||||
{
|
||||
key.sections.push(LoftSectionSelection::Point(point));
|
||||
key.options.closed = false;
|
||||
}
|
||||
self.cached_preview(key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+151
-56
@@ -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,88 @@ 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();
|
||||
}
|
||||
_ => {}
|
||||
.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 (set, wires, center) = display;
|
||||
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();
|
||||
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(),
|
||||
)
|
||||
.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,
|
||||
);
|
||||
}
|
||||
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,6 +11,7 @@ pub mod mesh_model;
|
||||
pub mod solid_model;
|
||||
pub mod solid_history;
|
||||
pub mod sweep_model;
|
||||
pub mod loft_command_model;
|
||||
mod sweep_command_model;
|
||||
pub mod visual_style_model;
|
||||
pub mod object;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -76,6 +76,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 +204,7 @@ pub fn has_specialized_primitive_properties(
|
||||
| SolidHistoryOperation::Torus(_)
|
||||
| SolidHistoryOperation::Pyramid(_)
|
||||
| SolidHistoryOperation::Sweep(_)
|
||||
| SolidHistoryOperation::Loft(_)
|
||||
| SolidHistoryOperation::Extrusion(_)
|
||||
| SolidHistoryOperation::Revolve(_)
|
||||
)
|
||||
@@ -519,6 +529,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;
|
||||
@@ -1209,6 +1374,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)
|
||||
}
|
||||
@@ -1253,6 +1419,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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1260,8 +1433,13 @@ 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)
|
||||
|| is_primitive_property(field)
|
||||
|| is_history_choice(field)
|
||||
}
|
||||
@@ -1688,6 +1866,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,
|
||||
@@ -1881,6 +2138,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)
|
||||
@@ -2188,6 +2450,51 @@ fn embedded_entity(value: &EmbeddedEntity) -> Option<EntityType> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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),
|
||||
|
||||
+54
-15
@@ -841,13 +841,29 @@ impl Scene {
|
||||
handle: Handle,
|
||||
operation: acadrust::objects::SolidHistoryOperation,
|
||||
) -> bool {
|
||||
self.solid_history_preview_wires.remove(&handle);
|
||||
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
|
||||
return false;
|
||||
};
|
||||
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else {
|
||||
return false;
|
||||
};
|
||||
let loft_surface_data = match (&operation, self.document.get_entity(handle)) {
|
||||
(acadrust::objects::SolidHistoryOperation::Loft(record), Some(EntityType::Surface(_))) => {
|
||||
let EntityType::Surface(surface) = crate::scene::model::loft_command_model::surface_entity(record)
|
||||
else { return false; };
|
||||
Some(surface.surface_data)
|
||||
}
|
||||
(_, Some(EntityType::Solid3D(_))) => None,
|
||||
_ => return false,
|
||||
};
|
||||
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
|
||||
return false;
|
||||
};
|
||||
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_))
|
||||
&& !display.0.complete
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.record_solid_history_before(handle);
|
||||
if self
|
||||
.document
|
||||
@@ -856,12 +872,16 @@ 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) = loft_surface_data { entity.surface_data = data; }
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
self.register_prepared_solid_model(handle, body, display);
|
||||
self.solid_history_preview_wires.remove(&handle);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -875,13 +895,28 @@ impl Scene {
|
||||
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(display) = self.prepare_solid_model_display(handle, &body) else {
|
||||
return false;
|
||||
};
|
||||
entity.set_sat_document(&document);
|
||||
self.sync_solid_reference_point(handle);
|
||||
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_))
|
||||
&& !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 acadrust::objects::SolidHistoryOperation::Loft(record) = &operation {
|
||||
if let EntityType::Surface(updated) = crate::scene::model::loft_command_model::surface_entity(record) {
|
||||
entity.surface_data = updated.surface_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
self.register_prepared_solid_model(handle, body, display);
|
||||
self.solid_history_preview_wires.remove(&handle);
|
||||
self.register_solid_model(handle, body);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -900,9 +935,9 @@ impl Scene {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Some(EntityType::Solid3D(_)) = self.document.get_entity(handle) else {
|
||||
if !matches!(self.document.get_entity(handle), Some(EntityType::Solid3D(_) | EntityType::Surface(_))) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
self.solid_history_preview_wires.insert(
|
||||
handle,
|
||||
crate::scene::model::solid_model::grip_preview_wires(&body, handle),
|
||||
@@ -945,12 +980,16 @@ impl Scene {
|
||||
value: &str,
|
||||
) -> bool {
|
||||
self.record_solid_history_before(handle);
|
||||
crate::scene::model::solid_history::apply_history_choice(
|
||||
let applied = crate::scene::model::solid_history::apply_history_choice(
|
||||
&mut self.document,
|
||||
handle,
|
||||
field,
|
||||
value,
|
||||
)
|
||||
);
|
||||
if applied {
|
||||
self.bump_entities(&[(handle, ChangeKind::Modified)]);
|
||||
}
|
||||
applied
|
||||
}
|
||||
|
||||
fn apply_solid_history_grip(
|
||||
|
||||
Reference in new issue
Block a user