forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into codex/shell-parity
# Conflicts: # Cargo.lock # Cargo.toml # crates/ocs_doc_api/Cargo.toml # src/modules/model/mod.rs
This commit is contained in:
commit
7913f8e2da
68 files changed
+5496
-548
No files matched your search
@@ -1265,4 +1265,63 @@ mod tests {
|
||||
let _ = std::fs::remove_file(sidecar);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pline_line_then_arc() {
|
||||
use crate::app::Message;
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
{
|
||||
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
|
||||
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
|
||||
}
|
||||
|
||||
// Start PLINE
|
||||
let _ = app.update(Message::CommandInput("PLINE".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
|
||||
// Click first point (100, 100)
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 100.0)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
|
||||
// Move to (200, 100) and click second point (draw line)
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
|
||||
let wid = app.main_window.unwrap_or_else(iced::window::Id::unique);
|
||||
|
||||
// Switch to arc: Option 1 - CommandOptionPick("A")
|
||||
let _ = app.update(Message::CommandOptionPick("A".to_string()));
|
||||
let _ = app.view(wid);
|
||||
println!("ENTITIES: {}", app.tabs[0].scene.document.entities().count());
|
||||
println!("CMD: {:?}", app.tabs[0].active_cmd.as_ref().map(|c| c.name()));
|
||||
|
||||
// Move mouse!
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(201.0, 100.0)));
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 150.0)));
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 150.0)));
|
||||
let _ = app.view(wid);
|
||||
|
||||
// Click arc point
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
|
||||
// Move mouse again
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 160.0)));
|
||||
|
||||
// Switch to line
|
||||
let _ = app.update(Message::CommandOptionPick("L".to_string()));
|
||||
|
||||
// Move mouse again
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 150.0)));
|
||||
|
||||
// Finish
|
||||
let _ = app.update(Message::CommandOptionPick(String::new()));
|
||||
}
|
||||
}
|
||||
@@ -733,7 +733,10 @@ impl OpenCADStudio {
|
||||
&result,
|
||||
CmdResult::Relaunch(..)
|
||||
| CmdResult::Dispatch(..)
|
||||
| CmdResult::SolidEdgeBlend { .. }
|
||||
| CmdResult::SolidSubtract { .. }
|
||||
| CmdResult::SliceEntities { .. }
|
||||
| CmdResult::SliceSurfaceEntities { .. }
|
||||
);
|
||||
let task = self.apply_cmd_result_inner(result);
|
||||
let i = self.active_tab;
|
||||
@@ -4111,11 +4114,20 @@ impl OpenCADStudio {
|
||||
|
||||
CmdResult::SolidEdgeBlend {
|
||||
handle,
|
||||
pick,
|
||||
edges,
|
||||
base_face,
|
||||
value,
|
||||
other_value,
|
||||
fillet,
|
||||
} => {
|
||||
let task = self.solid_edge_blend(handle, pick, value, fillet);
|
||||
let task = self.solid_edge_blend(
|
||||
handle,
|
||||
&edges,
|
||||
base_face,
|
||||
value,
|
||||
other_value,
|
||||
fillet,
|
||||
);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
@@ -4149,6 +4161,32 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
|
||||
CmdResult::SliceEntities {
|
||||
targets,
|
||||
plane,
|
||||
keep_point,
|
||||
} => {
|
||||
let task = self.solid_slice(&targets, plane, keep_point);
|
||||
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;
|
||||
}
|
||||
|
||||
CmdResult::SliceSurfaceEntities {
|
||||
targets,
|
||||
cutter,
|
||||
keep_point,
|
||||
} => {
|
||||
let task = self.solid_slice_surface(&targets, *cutter, keep_point);
|
||||
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;
|
||||
}
|
||||
|
||||
CmdResult::HatcheditApply {
|
||||
handle,
|
||||
name,
|
||||
|
||||
+92
-12
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
||||
fn selected_edge_body(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
||||
let scene = &app.tabs.get(tab)?.scene;
|
||||
let selected = scene.selected_handles_in_order();
|
||||
let [handle] = selected.as_slice() else {
|
||||
@@ -9,7 +9,7 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
||||
|
||||
if matches!(
|
||||
scene.document.get_entity(*handle),
|
||||
Some(acadrust::EntityType::Solid3D(_))
|
||||
Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_))
|
||||
) {
|
||||
Some(*handle)
|
||||
} else {
|
||||
@@ -17,6 +17,36 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
||||
}
|
||||
}
|
||||
|
||||
fn solid_edge_sources(
|
||||
app: &mut OpenCADStudio,
|
||||
tab: usize,
|
||||
) -> Vec<(acadrust::Handle, cadkernel::brep::Body)> {
|
||||
let handles = app.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|entity| {
|
||||
matches!(
|
||||
entity,
|
||||
acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_)
|
||||
)
|
||||
.then_some(entity.common().handle)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
app.tabs[tab].scene.restore_solid_models(&handles);
|
||||
handles
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
app.tabs[tab]
|
||||
.scene
|
||||
.solid_models
|
||||
.get(&handle)
|
||||
.cloned()
|
||||
.map(|body| (handle, body))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
@@ -960,16 +990,30 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"SOLIDFILLET" => {
|
||||
"FILLETEDGE" | "SOLIDFILLET" => {
|
||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
|
||||
let target = selected_edge_body(self, i);
|
||||
let bodies = solid_edge_sources(self, i);
|
||||
let command = SolidEdgeCommand::new(
|
||||
EdgeOperation::Fillet,
|
||||
target,
|
||||
bodies,
|
||||
crate::scene::WireModel::SELECTED,
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"FILLET" if selected_solid(self, i).is_some() => {
|
||||
"FILLET" if selected_edge_body(self, i).is_some() => {
|
||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
|
||||
let target = selected_edge_body(self, i);
|
||||
let bodies = solid_edge_sources(self, i);
|
||||
let command = SolidEdgeCommand::new(
|
||||
EdgeOperation::Fillet,
|
||||
target,
|
||||
bodies,
|
||||
crate::scene::WireModel::SELECTED,
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
@@ -1107,16 +1151,52 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"SOLIDCHAMFER" => {
|
||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
||||
"CHAMFEREDGE" | "SOLIDCHAMFER" => {
|
||||
use crate::modules::model::edge_cmd::SolidEdgeCommand;
|
||||
let target = selected_edge_body(self, i);
|
||||
let header = &self.tabs[i].scene.document.header;
|
||||
let distances = (header.chamfer_distance_a, header.chamfer_distance_b);
|
||||
let bodies = solid_edge_sources(self, i);
|
||||
let command = SolidEdgeCommand::new_chamfer(
|
||||
target,
|
||||
bodies,
|
||||
crate::scene::WireModel::SELECTED,
|
||||
distances,
|
||||
);
|
||||
let distances = command.chamfer_distances();
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"Distance1 = {:.4}, Distance2 = {:.4}",
|
||||
distances.0,
|
||||
distances.1
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"CHAMFER" if selected_solid(self, i).is_some() => {
|
||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
||||
"CHAMFER" if selected_edge_body(self, i).is_some() => {
|
||||
use crate::modules::model::edge_cmd::SolidEdgeCommand;
|
||||
let target = selected_edge_body(self, i);
|
||||
let header = &self.tabs[i].scene.document.header;
|
||||
let distances = (header.chamfer_distance_a, header.chamfer_distance_b);
|
||||
let bodies = solid_edge_sources(self, i);
|
||||
let command = SolidEdgeCommand::new_chamfer(
|
||||
target,
|
||||
bodies,
|
||||
crate::scene::WireModel::SELECTED,
|
||||
distances,
|
||||
);
|
||||
let distances = command.chamfer_distances();
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"Distance1 = {:.4}, Distance2 = {:.4}",
|
||||
distances.0,
|
||||
distances.1
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
+56
-15
@@ -1307,21 +1307,51 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// SLICE [X|Y|Z] <value> [TOP|BOTTOM] — cut the selected solid with an
|
||||
// axis-aligned plane, keeping the lower half by default.
|
||||
"SLICE" | "SL" => {
|
||||
use crate::command::SelectThenKeywordCommand;
|
||||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||||
let c = SelectThenKeywordCommand::new(
|
||||
"SLICE",
|
||||
"SLICE cutting-plane axis [X / Y / Z] (add TOP/BOTTOM by typing):",
|
||||
vec![
|
||||
("X", "X", Some("SLICE offset along X:")),
|
||||
("Y", "Y", Some("SLICE offset along Y:")),
|
||||
("Z", "Z", Some("SLICE offset along Z:")),
|
||||
],
|
||||
has_sel,
|
||||
);
|
||||
use crate::modules::model::slice_cmd::SliceCommand;
|
||||
let (targets, centre, radius, view_normal) = {
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
let mut targets = scene
|
||||
.selected_handles_in_order()
|
||||
.into_iter()
|
||||
.filter(|handle| !scene.is_layer_locked(*handle))
|
||||
.filter(|handle| {
|
||||
matches!(
|
||||
scene.document.get_entity(*handle),
|
||||
Some(
|
||||
acadrust::EntityType::Solid3D(_)
|
||||
| acadrust::EntityType::Surface(_)
|
||||
)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
scene.restore_solid_models(&targets);
|
||||
targets.retain(|handle| scene.solid_models.contains_key(handle));
|
||||
let bounds = targets
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
crate::scene::model::solid_model::extent(&scene.solid_models[handle])
|
||||
})
|
||||
.fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| {
|
||||
Some(match bounds {
|
||||
None => (low, high),
|
||||
Some((mut min, mut max)) => {
|
||||
for axis in 0..3 {
|
||||
min[axis] = min[axis].min(low[axis]);
|
||||
max[axis] = max[axis].max(high[axis]);
|
||||
}
|
||||
(min, max)
|
||||
}
|
||||
})
|
||||
});
|
||||
let (centre, radius) = bounds.map_or((glam::DVec3::ZERO, 10.0), |(min, max)| {
|
||||
let min = glam::DVec3::from_array(min);
|
||||
let max = glam::DVec3::from_array(max);
|
||||
((min + max) * 0.5, (max - min).length().max(2.0) * 0.65)
|
||||
});
|
||||
(targets, centre, radius, scene.active_gaze_dir().as_dvec3())
|
||||
};
|
||||
let c = SliceCommand::new(targets, view_normal, centre, radius);
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
}
|
||||
@@ -1340,7 +1370,18 @@ impl OpenCADStudio {
|
||||
let value: Option<f64> = parts.get(val_idx).and_then(|s| s.parse().ok());
|
||||
let keep_low = !parts.iter().any(|s| s == "TOP");
|
||||
match value {
|
||||
Some(v) => return Some(self.solid_slice(axis, v, keep_low)),
|
||||
Some(v) => {
|
||||
let (origin, x, normal) = match axis {
|
||||
0 => ([v, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
|
||||
1 => ([0.0, v, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
|
||||
_ => ([0.0, 0.0, v], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
|
||||
};
|
||||
let plane = cadkernel::space::Plane::orthonormal(origin, x, normal)
|
||||
.expect("fixed world axes define a plane");
|
||||
let side = glam::DVec3::from_array(origin)
|
||||
+ glam::DVec3::from_array(normal) * if keep_low { -1.0 } else { 1.0 };
|
||||
return Some(self.slice_selected(plane, Some(side)));
|
||||
}
|
||||
None => self.command_line.push_info(
|
||||
crate::t!("Usage: SLICE [X|Y|Z] <value> [TOP|BOTTOM] (cuts the selected solid)").as_ref(),
|
||||
),
|
||||
|
||||
@@ -335,9 +335,9 @@ impl OpenCADStudio {
|
||||
"PERSP" => return Some(Task::done(Message::SetProjection(false))),
|
||||
"LAYERS" => return Some(Task::done(Message::ToggleLayers)),
|
||||
|
||||
// SCRIPT <path> — run a command script: each non-blank, non-comment
|
||||
// SCRIPT <path> — run a command script: each non-comment
|
||||
// line is fed through the same command path the `--script` startup
|
||||
// flag uses, so the behaviour matches headless automation exactly.
|
||||
// flag uses. Blank rows submit Enter to the active command.
|
||||
"SCRIPT" | "SCR" => {
|
||||
use crate::command::ValuePromptCommand;
|
||||
let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:");
|
||||
@@ -352,10 +352,8 @@ impl OpenCADStudio {
|
||||
let cmds: Vec<Task<Message>> = text
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|l| {
|
||||
!l.is_empty() && !l.starts_with('#') && !l.starts_with(';')
|
||||
})
|
||||
.map(|l| Task::done(Message::Command(l.to_string())))
|
||||
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
|
||||
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
|
||||
.collect();
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"SCRIPT: running {} command(s) from {p}.",
|
||||
|
||||
+5
-1
@@ -2117,6 +2117,10 @@ pub enum Message {
|
||||
CommandInput(String),
|
||||
CommandSubmit,
|
||||
Command(String),
|
||||
/// Execute one complete line read from a command script. Unlike UI/ribbon
|
||||
/// dispatch, this accepts an interactive verb and all of its arguments on
|
||||
/// the same line (`BOX 0,0,0 10,10,0 10`).
|
||||
ScriptLine(String),
|
||||
/// Append one typed character to the command-line input from the
|
||||
/// global key-press subscription. Used when the text-input widget
|
||||
/// itself isn't focused (focus parked on viewport / button / etc.)
|
||||
@@ -3832,7 +3836,7 @@ impl OpenCADStudio {
|
||||
Task::batch(
|
||||
cfg.script_lines
|
||||
.into_iter()
|
||||
.map(|line| Task::done(Message::Command(line))),
|
||||
.map(|line| Task::done(Message::ScriptLine(line))),
|
||||
)
|
||||
};
|
||||
s.queue_startup_prompts();
|
||||
|
||||
+523
-105
@@ -7,7 +7,7 @@ use acadrust::{
|
||||
objects::SolidHistoryOperation,
|
||||
EntityType, Handle,
|
||||
};
|
||||
use cadkernel::brep::Body;
|
||||
use cadkernel::brep::{Body, EdgeKey, FaceKey};
|
||||
use iced::Task;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
@@ -76,6 +76,35 @@ struct PreparedSubtract {
|
||||
result: Option<(Handle, EntityType, Body, PreparedSolidDisplay)>,
|
||||
}
|
||||
|
||||
struct PreparedSlice {
|
||||
retained: Handle,
|
||||
entity: EntityType,
|
||||
body: Body,
|
||||
display: PreparedSolidDisplay,
|
||||
extra: Option<(EntityType, Body)>,
|
||||
}
|
||||
|
||||
enum ModelSliceTool {
|
||||
Plane(cadkernel::space::Plane),
|
||||
Surface(Body),
|
||||
}
|
||||
|
||||
impl ModelSliceTool {
|
||||
fn side(&self, point: [f64; 3]) -> Option<f64> {
|
||||
match self {
|
||||
Self::Plane(plane) => plane.distance_to(point),
|
||||
Self::Surface(surface) => cadkernel::brep::surface_side(surface, point),
|
||||
}
|
||||
}
|
||||
|
||||
fn split(&self, body: &Body) -> Result<Option<cadkernel::brep::PlaneSlice>, cadkernel::brep::Snag> {
|
||||
match self {
|
||||
Self::Plane(plane) => cadkernel::brep::slice_by_plane(body, *plane),
|
||||
Self::Surface(surface) => cadkernel::brep::slice_by_surface(body, surface),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IntersectGroup {
|
||||
kind: UnionEntityKind,
|
||||
plane: Option<cadkernel::space::Plane>,
|
||||
@@ -244,6 +273,29 @@ fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option<Entit
|
||||
Some(source)
|
||||
}
|
||||
|
||||
fn entity_with_history_body(mut source: EntityType, body: &Body) -> Option<EntityType> {
|
||||
let document = crate::scene::convert::acis_export::solid_to_sat(body)?;
|
||||
let wires = solid_model::edge_wires(body);
|
||||
match &mut source {
|
||||
EntityType::Solid3D(entity) => {
|
||||
entity.wires = wires;
|
||||
entity.silhouettes.clear();
|
||||
entity.set_sat_document(&document);
|
||||
}
|
||||
EntityType::Surface(entity) => {
|
||||
entity.wires = wires;
|
||||
entity.silhouettes.clear();
|
||||
if entity.kind != SurfaceKind::Plane {
|
||||
entity.kind = SurfaceKind::Generic;
|
||||
entity.surface_data = SurfaceData::Generic;
|
||||
}
|
||||
entity.acis_data = AcisData::from_sat(&document.to_sat_string());
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
Some(source)
|
||||
}
|
||||
|
||||
enum IntersectBodyOutcome {
|
||||
Area(Body),
|
||||
Touching,
|
||||
@@ -551,44 +603,169 @@ impl super::OpenCADStudio {
|
||||
groups
|
||||
}
|
||||
|
||||
fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool {
|
||||
fn replace_solid_body_with_fillet(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
source: &Body,
|
||||
result: Body,
|
||||
edges: &[EdgeKey],
|
||||
radius: f64,
|
||||
) -> bool {
|
||||
let i = self.active_tab;
|
||||
let Some(document) =
|
||||
crate::scene::convert::acis_export::solid_to_sat(&result)
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The result could not be encoded as ACIS.").as_ref());
|
||||
return false;
|
||||
};
|
||||
let Some(EntityType::Solid3D(mut entity)) =
|
||||
self.tabs[i].scene.document.get_entity(handle).cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
entity.wires = solid_model::edge_wires(&result);
|
||||
entity.silhouettes.clear();
|
||||
entity.history_handle = None;
|
||||
entity.set_sat_document(&document);
|
||||
|
||||
let Some(display) = self.tabs[i].scene.prepare_solid_model_display(handle, &result)
|
||||
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());
|
||||
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original edge body was retained.").as_ref());
|
||||
return false;
|
||||
};
|
||||
self.push_undo_snapshot(i, label);
|
||||
self.tabs[i].scene.delete_solid_history(handle);
|
||||
if !self.tabs[i]
|
||||
.scene
|
||||
.update_entity(EntityType::Solid3D(entity))
|
||||
let ordered = source.edge_keys().collect::<Vec<_>>();
|
||||
let Some(history_edges) = edges
|
||||
.iter()
|
||||
.map(|edge| {
|
||||
ordered
|
||||
.iter()
|
||||
.position(|candidate| candidate == edge)
|
||||
.and_then(|ordinal| i32::try_from(ordinal).ok())
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref());
|
||||
return false;
|
||||
};
|
||||
|
||||
self.push_undo_snapshot(i, "FILLETEDGE");
|
||||
if self.tabs[i].scene.document.solid_history_graph(handle).is_none()
|
||||
&& !self.tabs[i]
|
||||
.scene
|
||||
.create_solid_history(handle, solid_history::brep_op(source))
|
||||
{
|
||||
self.command_line
|
||||
.push_error(crate::t!("The solid could not be updated.").as_ref());
|
||||
.push_error(crate::t!("The edge body history could not be created.").as_ref());
|
||||
return false;
|
||||
}
|
||||
let history = solid_history::brep_op(&result);
|
||||
self.tabs[i].scene.create_solid_history(handle, history);
|
||||
self.tabs[i].scene.register_prepared_solid_model(handle, result, display);
|
||||
if !self.tabs[i].scene.append_solid_history(
|
||||
handle,
|
||||
solid_history::fillet_op(history_edges, radius),
|
||||
) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The fillet history could not be recorded.").as_ref());
|
||||
return false;
|
||||
}
|
||||
let _ = self.tabs[i].scene.apply_solid_history_choice(
|
||||
handle,
|
||||
solid_history::PROP_HISTORY,
|
||||
"Record",
|
||||
);
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let Some(entity) = entity_with_history_body(entity, &result) else {
|
||||
return false;
|
||||
};
|
||||
if !self.tabs[i].scene.update_entity(entity) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The edge body could not be updated.").as_ref());
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
self.refresh_properties();
|
||||
true
|
||||
}
|
||||
|
||||
fn replace_edge_body_with_chamfer(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
source: &Body,
|
||||
result: Body,
|
||||
edges: &[EdgeKey],
|
||||
base_face: FaceKey,
|
||||
base_distance: f64,
|
||||
other_distance: f64,
|
||||
) -> bool {
|
||||
let i = self.active_tab;
|
||||
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 edge body was retained.").as_ref());
|
||||
return false;
|
||||
};
|
||||
let ordered_edges = source.edge_keys().collect::<Vec<_>>();
|
||||
let Some(history_edges) = edges
|
||||
.iter()
|
||||
.map(|edge| {
|
||||
ordered_edges
|
||||
.iter()
|
||||
.position(|candidate| candidate == edge)
|
||||
.and_then(|ordinal| i32::try_from(ordinal).ok())
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref());
|
||||
return false;
|
||||
};
|
||||
let Some(history_face) = source
|
||||
.face_keys()
|
||||
.position(|candidate| candidate == base_face)
|
||||
.and_then(|ordinal| i32::try_from(ordinal).ok())
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The selected base face no longer belongs to the body.").as_ref());
|
||||
return false;
|
||||
};
|
||||
|
||||
self.push_undo_snapshot(i, "CHAMFEREDGE");
|
||||
if self.tabs[i].scene.document.solid_history_graph(handle).is_none()
|
||||
&& !self.tabs[i]
|
||||
.scene
|
||||
.create_solid_history(handle, solid_history::brep_op(source))
|
||||
{
|
||||
self.command_line
|
||||
.push_error(crate::t!("The edge body history could not be created.").as_ref());
|
||||
return false;
|
||||
}
|
||||
if !self.tabs[i].scene.append_solid_history(
|
||||
handle,
|
||||
solid_history::chamfer_op(
|
||||
history_edges,
|
||||
history_face,
|
||||
base_distance,
|
||||
other_distance,
|
||||
),
|
||||
) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The chamfer history could not be recorded.").as_ref());
|
||||
return false;
|
||||
}
|
||||
let _ = self.tabs[i].scene.apply_solid_history_choice(
|
||||
handle,
|
||||
solid_history::PROP_HISTORY,
|
||||
"Record",
|
||||
);
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let Some(entity) = entity_with_history_body(entity, &result) else {
|
||||
return false;
|
||||
};
|
||||
if !self.tabs[i].scene.update_entity(entity) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The edge body could not be updated.").as_ref());
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
@@ -599,20 +776,21 @@ impl super::OpenCADStudio {
|
||||
pub(super) fn solid_edge_blend(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
pick: glam::DVec3,
|
||||
edges: &[EdgeKey],
|
||||
base_face: Option<FaceKey>,
|
||||
value: f64,
|
||||
other_value: f64,
|
||||
fillet: bool,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
if self.reject_locked_edit(i, handle) {
|
||||
return Task::none();
|
||||
}
|
||||
if !matches!(
|
||||
self.tabs[i].scene.document.get_entity(handle),
|
||||
Some(EntityType::Solid3D(_))
|
||||
) {
|
||||
if !matches!(self.tabs[i].scene.document.get_entity(handle), Some(
|
||||
EntityType::Solid3D(_) | EntityType::Surface(_)
|
||||
)) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Select a 3D solid edge.").as_ref());
|
||||
.push_error(crate::t!("Select a 3D solid or surface edge.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].scene.restore_solid_models(&[handle]);
|
||||
@@ -621,15 +799,35 @@ impl super::OpenCADStudio {
|
||||
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
let Some(edge) = solid_model::nearest_edge(&body, pick.to_array()) else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Select a solid edge.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
let result = if fillet {
|
||||
cadkernel::brep::fillet(&body, edge, value)
|
||||
match cadkernel::brep::fillet_edges(&body, edges, value) {
|
||||
Ok(result) => Some(result),
|
||||
Err(error) => {
|
||||
self.command_line
|
||||
.push_error(&format!("FILLETEDGE: {error}"));
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cadkernel::brep::chamfer(&body, edge, value)
|
||||
let Some(base_face) = base_face else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("CHAMFEREDGE: select a base face edge.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
match cadkernel::brep::chamfer_edges(
|
||||
&body,
|
||||
edges,
|
||||
base_face,
|
||||
value,
|
||||
other_value,
|
||||
) {
|
||||
Ok(result) => Some(result),
|
||||
Err(error) => {
|
||||
self.command_line
|
||||
.push_error(&format!("CHAMFEREDGE: {error}"));
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
};
|
||||
let Some(result) = result else {
|
||||
self.command_line.push_error(
|
||||
@@ -638,8 +836,25 @@ impl super::OpenCADStudio {
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let label = if fillet { "SOLIDFILLET" } else { "SOLIDCHAMFER" };
|
||||
if self.replace_solid_body(handle, result, label) {
|
||||
let label = if fillet { "FILLETEDGE" } else { "CHAMFEREDGE" };
|
||||
let updated = if fillet {
|
||||
self.replace_solid_body_with_fillet(handle, &body, result, edges, value)
|
||||
} else {
|
||||
self.replace_edge_body_with_chamfer(
|
||||
handle,
|
||||
&body,
|
||||
result,
|
||||
edges,
|
||||
base_face.expect("checked chamfer face"),
|
||||
value,
|
||||
other_value,
|
||||
)
|
||||
};
|
||||
if updated {
|
||||
if !fillet {
|
||||
self.tabs[i].scene.document.header.chamfer_distance_a = value;
|
||||
self.tabs[i].scene.document.header.chamfer_distance_b = other_value;
|
||||
}
|
||||
self.command_line
|
||||
.push_output(crate::tf!("{label}: solid updated.").as_ref());
|
||||
}
|
||||
@@ -740,10 +955,42 @@ impl super::OpenCADStudio {
|
||||
}
|
||||
};
|
||||
|
||||
if self.replace_solid_body(handle, result, "SHELL") {
|
||||
let Some(source) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(entity) = entity_with_boolean_body(source, &result) else {
|
||||
self.command_line
|
||||
.push_output(crate::t!("SHELL: solid updated.").as_ref());
|
||||
.push_error(crate::t!("The result could not be encoded as ACIS.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
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 Task::none();
|
||||
};
|
||||
|
||||
self.push_undo_snapshot(i, "SHELL");
|
||||
self.tabs[i].scene.delete_solid_history(handle);
|
||||
if !self.tabs[i].scene.update_entity(entity) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The solid could not be updated.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.create_solid_history(handle, solid_history::brep_op(&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;
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(crate::t!("SHELL: solid updated.").as_ref());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
@@ -1374,77 +1621,248 @@ impl super::OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Slice the one selected solid with an axis-aligned plane (axis 0/1/2 =
|
||||
/// X/Y/Z at `value`), keeping the lower side when `keep_low` is true. The
|
||||
/// kept half is the intersection of the solid with a half-space box, reusing
|
||||
/// the same boolean path as the modelling tools.
|
||||
pub(super) fn solid_slice(&mut self, axis: usize, value: f64, keep_low: bool) -> Task<Message> {
|
||||
/// Split every selected solid or surface by an arbitrary plane. All
|
||||
/// geometry, persistence records, and displays are prepared before the
|
||||
/// first source entity is changed.
|
||||
pub(super) fn solid_slice(
|
||||
&mut self,
|
||||
requested: &[Handle],
|
||||
plane: cadkernel::space::Plane,
|
||||
keep_point: Option<glam::DVec3>,
|
||||
) -> Task<Message> {
|
||||
self.solid_slice_with_tool(requested, ModelSliceTool::Plane(plane), keep_point)
|
||||
}
|
||||
|
||||
pub(super) fn solid_slice_surface(
|
||||
&mut self,
|
||||
requested: &[Handle],
|
||||
cutter: Body,
|
||||
keep_point: Option<glam::DVec3>,
|
||||
) -> Task<Message> {
|
||||
self.solid_slice_with_tool(requested, ModelSliceTool::Surface(cutter), keep_point)
|
||||
}
|
||||
|
||||
fn solid_slice_with_tool(
|
||||
&mut self,
|
||||
requested: &[Handle],
|
||||
tool: ModelSliceTool,
|
||||
keep_point: Option<glam::DVec3>,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let handles = self.selected_solid_handles();
|
||||
if handles.len() != 1 {
|
||||
self.command_line
|
||||
.push_error(crate::t!("SLICE: select exactly one solid created this session.").as_ref());
|
||||
let mut handles = requested
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.filter(|handle| {
|
||||
matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(EntityType::Solid3D(_) | EntityType::Surface(_))
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
handles.sort_unstable_by_key(|handle| handle.value());
|
||||
handles.dedup();
|
||||
self.tabs[i].scene.restore_solid_models(&handles);
|
||||
handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle));
|
||||
if handles.is_empty() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: select at least one solid or surface.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let solid = self.tabs[i].scene.solid_models[&handles[0]].clone();
|
||||
// Bounding box from the solid's edge wires.
|
||||
let Some((min, max)) = solid_model::extent(&solid) else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("SLICE: could not determine the solid's extent.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
// Generous margin so the box fully spans the solid in the free axes.
|
||||
let m = [
|
||||
(max[0] - min[0]).max(1.0),
|
||||
(max[1] - min[1]).max(1.0),
|
||||
(max[2] - min[2]).max(1.0),
|
||||
];
|
||||
let mut lo = [min[0] - m[0], min[1] - m[1], min[2] - m[2]];
|
||||
let mut hi = [max[0] + m[0], max[1] + m[1], max[2] + m[2]];
|
||||
if keep_low {
|
||||
hi[axis] = value;
|
||||
|
||||
let keep_positive = if let Some(point) = keep_point {
|
||||
let tolerance = cadkernel::brep::operation_tolerance(
|
||||
&handles
|
||||
.iter()
|
||||
.map(|handle| &self.tabs[i].scene.solid_models[handle])
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let Some(distance) = tool.side(point.to_array()) else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("SLICE: the cutting plane is invalid.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
if distance.abs() <= tolerance {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: choose a point away from the cutting plane.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
Some(distance > 0.0)
|
||||
} else {
|
||||
lo[axis] = value;
|
||||
}
|
||||
if hi[axis] <= lo[axis] {
|
||||
self.command_line
|
||||
.push_error(crate::t!("SLICE: the plane does not cross the solid on the kept side.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let center = [
|
||||
(lo[0] + hi[0]) / 2.0,
|
||||
(lo[1] + hi[1]) / 2.0,
|
||||
(lo[2] + hi[2]) / 2.0,
|
||||
];
|
||||
let halfspace = solid_model::box_solid(center, hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]);
|
||||
let Some(result) = halfspace
|
||||
.as_ref()
|
||||
.and_then(|half| solid_model::boolean(Bool::Intersect, &solid, half))
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("SLICE failed — the plane may not cross the solid.").as_ref());
|
||||
return Task::none();
|
||||
None
|
||||
};
|
||||
|
||||
let mut prepared = Vec::with_capacity(handles.len());
|
||||
for handle in &handles {
|
||||
let body = &self.tabs[i].scene.solid_models[handle];
|
||||
let result = match tool.split(body) {
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: the cutting plane does not cross every selected object; no object was changed.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
Err(cadkernel::brep::Snag::NoClosedForm) => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: the selected geometry has an unsupported surface intersection; no object was changed.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
Err(cadkernel::brep::Snag::Coincident) => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: the cutting plane is coincident with source geometry; no object was changed.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
Err(cadkernel::brep::Snag::CutRefused) => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: the selected topology could not be split safely; no object was changed.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let (body, extra_body) = match keep_positive {
|
||||
Some(true) => (result.positive, None),
|
||||
Some(false) => (result.negative, None),
|
||||
None => (result.negative, Some(result.positive)),
|
||||
};
|
||||
let Some(source) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(entity) = entity_with_boolean_body(source.clone(), &body) else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: a result could not be encoded; no object was changed.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let Some(display) = self.tabs[i]
|
||||
.scene
|
||||
.prepare_solid_model_display(*handle, &body)
|
||||
.filter(|display| display.0.complete)
|
||||
else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: a result could not be displayed completely; no object was changed.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let extra = match extra_body {
|
||||
Some(extra_body) => {
|
||||
let mut extra_entity = match entity_with_boolean_body(source, &extra_body) {
|
||||
Some(entity) => entity,
|
||||
None => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: a result could not be encoded; no object was changed.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
extra_entity.common_mut().handle = Handle::NULL;
|
||||
Some((extra_entity, extra_body))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
prepared.push(PreparedSlice {
|
||||
retained: *handle,
|
||||
entity,
|
||||
body,
|
||||
display,
|
||||
extra,
|
||||
});
|
||||
}
|
||||
|
||||
self.push_undo_snapshot(i, "SLICE");
|
||||
self.tabs[i].scene.erase_entities(&handles);
|
||||
let mut s3d = Solid3D::new();
|
||||
s3d.wires = solid_model::edge_wires(&result);
|
||||
let history = solid_history::brep_op(&result);
|
||||
let handle = self.add_solid_model(EntityType::Solid3D(s3d), result, history);
|
||||
let mut created = Vec::new();
|
||||
for item in &prepared {
|
||||
let Some((entity, body)) = &item.extra else {
|
||||
continue;
|
||||
};
|
||||
let handle = match entity {
|
||||
EntityType::Solid3D(_) => self.add_solid_model_preserving_style(
|
||||
entity.clone(),
|
||||
body.clone(),
|
||||
solid_history::brep_op(body),
|
||||
),
|
||||
EntityType::Surface(_) => {
|
||||
self.add_surface_model_preserving_style(entity.clone(), body.clone())
|
||||
}
|
||||
_ => Handle::NULL,
|
||||
};
|
||||
if handle.is_null() {
|
||||
self.tabs[i].scene.rollback_new_entities(&created);
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: a result could not be committed; all source objects were retained.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
created.push(handle);
|
||||
}
|
||||
|
||||
let record_history = self.tabs[i].scene.document.header.record_solid_history;
|
||||
let mut retained = Vec::with_capacity(prepared.len());
|
||||
for item in prepared {
|
||||
self.tabs[i].scene.delete_solid_history(item.retained);
|
||||
if !self.tabs[i].scene.update_entity(item.entity) {
|
||||
self.command_line.push_error(
|
||||
crate::t!("SLICE: a retained result could not be updated.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if record_history
|
||||
&& matches!(
|
||||
self.tabs[i].scene.document.get_entity(item.retained),
|
||||
Some(EntityType::Solid3D(_))
|
||||
)
|
||||
{
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.create_solid_history(item.retained, solid_history::brep_op(&item.body));
|
||||
}
|
||||
self.tabs[i].scene.register_prepared_solid_model(
|
||||
item.retained,
|
||||
item.body,
|
||||
item.display,
|
||||
);
|
||||
retained.push(item.retained);
|
||||
}
|
||||
|
||||
retained.extend(created);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
if !handle.is_null() {
|
||||
self.tabs[i].scene.select_entity(handle, false);
|
||||
for handle in &retained {
|
||||
self.tabs[i].scene.select_entity(*handle, false);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
let ax = ["X", "Y", "Z"][axis];
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"SLICE: cut at {ax}={value}, kept the {} half.",
|
||||
if keep_low { "lower" } else { "upper" }
|
||||
).as_ref());
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"SLICE: created %{count} result object(s).",
|
||||
count = retained.len()
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn slice_selected(
|
||||
&mut self,
|
||||
plane: cadkernel::space::Plane,
|
||||
keep_point: Option<glam::DVec3>,
|
||||
) -> Task<Message> {
|
||||
let handles = self.tabs[self.active_tab].scene.selected_handles_in_order();
|
||||
self.solid_slice(&handles, plane, keep_point)
|
||||
}
|
||||
|
||||
/// INTERFERE — create a solid from the overlap of the two selected solids,
|
||||
/// leaving the originals in place (a non-destructive boolean intersect).
|
||||
pub(super) fn solid_interfere(&mut self) -> Task<Message> {
|
||||
|
||||
@@ -740,6 +740,14 @@ impl OpenCADStudio {
|
||||
.as_mut()
|
||||
.map(|c| c.on_preview_wires(cur))
|
||||
.unwrap_or_default();
|
||||
let preview_hidden = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|command| command.preview_hidden_handles().to_vec())
|
||||
.unwrap_or_default();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.set_command_preview_hidden(&preview_hidden);
|
||||
self.tabs[i].scene.set_preview_wires(previews);
|
||||
}
|
||||
}
|
||||
@@ -265,6 +265,11 @@ impl OpenCADStudio {
|
||||
self.control_observe_user_message(&msg);
|
||||
let perf_started = crate::perf::enabled().then(Instant::now);
|
||||
let perf_label = perf_message_label(&msg);
|
||||
// Keep the stable tab id because dispatch may switch or close tabs.
|
||||
let perf_edit_before = perf_started.map(|started| {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
(started, tab.id, tab.scene.geometry_epoch)
|
||||
});
|
||||
// A modal dialog must capture the keyboard the same way it already
|
||||
// captures the mouse. Otherwise keystrokes from the global key
|
||||
// subscription leak past the modal into the command line and fire as
|
||||
@@ -324,6 +329,17 @@ impl OpenCADStudio {
|
||||
self.otrack_active = None;
|
||||
self.otrack_kind = None;
|
||||
}
|
||||
if let Some((started, tab_id, before)) = perf_edit_before {
|
||||
if let Some(tab) = self.tabs.iter().find(|tab| tab.id == tab_id) {
|
||||
if tab.scene.geometry_epoch != before {
|
||||
tab.scene.record_nav_perf_caused(
|
||||
crate::scene::NavPerfOp::Edit,
|
||||
perf_label,
|
||||
started,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(started) = perf_started {
|
||||
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
if elapsed_ms >= 5.0 {
|
||||
@@ -1953,6 +1969,14 @@ impl OpenCADStudio {
|
||||
self.dispatch_command(&cmd)
|
||||
}
|
||||
|
||||
Message::ScriptLine(line) => {
|
||||
if line.trim().is_empty() {
|
||||
self.feed_command(crate::command::StepInput::Enter)
|
||||
} else {
|
||||
self.run_command_line(&line)
|
||||
}
|
||||
}
|
||||
|
||||
Message::ToggleLayers => {
|
||||
if self.active_modal == Some(super::ModalKind::Layers) {
|
||||
self.ribbon.deactivate_tool_if("LAYERS");
|
||||
|
||||
@@ -2468,6 +2468,14 @@ impl OpenCADStudio {
|
||||
previews.push(guide);
|
||||
}
|
||||
}
|
||||
let preview_hidden = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|command| command.preview_hidden_handles().to_vec())
|
||||
.unwrap_or_default();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.set_command_preview_hidden(&preview_hidden);
|
||||
self.tabs[i].scene.set_preview_wires(previews);
|
||||
} else {
|
||||
self.tabs[i].snap_result = None;
|
||||
|
||||
Reference in new issue
Block a user