forked from 1b-agent/opencadstudio-upstream
Merge branch 'main' into feature/parametric-constraint-system
# Conflicts: # src/app/automation.rs # src/app/mod.rs # src/ui/ribbon/widgets.rs
This commit is contained in:
commit
e2d39b049d
114 files changed
+13641
-1685
No files matched your search
+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));
|
||||
}
|
||||
|
||||
+153
-64
@@ -1082,54 +1082,95 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"SHELL" | "SOLIDEDIT" => {
|
||||
use crate::modules::model::shell_cmd::ShellCommand;
|
||||
let selected = self.tabs[i]
|
||||
.scene
|
||||
.selected_handles_in_order()
|
||||
.into_iter()
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.filter(|handle| {
|
||||
matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(acadrust::EntityType::Solid3D(_))
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let target = (selected.len() == 1).then_some(selected[0]);
|
||||
let new_cmd = if cmd == "SHELL" {
|
||||
ShellCommand::direct(target)
|
||||
} else {
|
||||
ShellCommand::solid_edit(target)
|
||||
};
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
// ── Solid booleans ─────────────────────────────────────────────
|
||||
"UNION" | "INTERSECT" => {
|
||||
"UNION" => {
|
||||
use crate::modules::model::boolean_cmd::BoolOp;
|
||||
if let Some(op) = BoolOp::from_id(cmd) {
|
||||
let solid_count = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
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(_))
|
||||
)
|
||||
})
|
||||
.take(2)
|
||||
.count()
|
||||
};
|
||||
if solid_count < 2 {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let selection = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&selection.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(selection));
|
||||
} else {
|
||||
return Some(self.solid_boolean(op));
|
||||
}
|
||||
if self.union_ready() {
|
||||
return Some(self.solid_boolean(BoolOp::Union));
|
||||
}
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let selection = SelectObjectsCommand::plain("UNION", "UNIONAPPLY");
|
||||
self.command_line.push_info(&selection.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(selection));
|
||||
}
|
||||
|
||||
"UNIONAPPLY" => {
|
||||
use crate::modules::model::boolean_cmd::BoolOp;
|
||||
return Some(self.solid_boolean(BoolOp::Union));
|
||||
}
|
||||
|
||||
"INTERSECT" => {
|
||||
use crate::modules::model::boolean_cmd::BoolOp;
|
||||
if !self.intersect_ready() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let selection = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&selection.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(selection));
|
||||
} else {
|
||||
return Some(self.solid_boolean(BoolOp::Intersect));
|
||||
}
|
||||
}
|
||||
|
||||
"SUBTRACT" => {
|
||||
use crate::modules::model::boolean_cmd::SubtractCommand;
|
||||
let bases = {
|
||||
let (bases, bases_have_mesh) = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
scene
|
||||
let bases = 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(_))
|
||||
Some(
|
||||
acadrust::EntityType::Solid3D(_)
|
||||
| acadrust::EntityType::Region(_)
|
||||
| acadrust::EntityType::Surface(_)
|
||||
| acadrust::EntityType::Mesh(_)
|
||||
| acadrust::EntityType::PolygonMesh(_)
|
||||
| acadrust::EntityType::PolyfaceMesh(_)
|
||||
)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
let bases_have_mesh = bases.iter().any(|handle| {
|
||||
matches!(
|
||||
scene.document.get_entity(*handle),
|
||||
Some(
|
||||
acadrust::EntityType::Mesh(_)
|
||||
| acadrust::EntityType::PolygonMesh(_)
|
||||
| acadrust::EntityType::PolyfaceMesh(_)
|
||||
)
|
||||
)
|
||||
});
|
||||
(bases, bases_have_mesh)
|
||||
};
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let subtract = SubtractCommand::new(bases);
|
||||
let subtract = SubtractCommand::new(bases, bases_have_mesh);
|
||||
self.command_line.push_info(&subtract.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(subtract));
|
||||
}
|
||||
@@ -1180,9 +1221,9 @@ impl OpenCADStudio {
|
||||
// REGION — convert selected closed boundaries (closed polylines /
|
||||
// circles) into Region entities (one wire loop each).
|
||||
"REGION" | "REG" => {
|
||||
use acadrust::entities::{Region, Wire};
|
||||
use acadrust::entities::Region;
|
||||
use acadrust::types::Vector3;
|
||||
let mut loops: Vec<Vec<Vector3>> = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
for (_, e) in self.tabs[i].scene.selected_entities().iter() {
|
||||
let supported = matches!(
|
||||
e,
|
||||
@@ -1190,32 +1231,39 @@ impl OpenCADStudio {
|
||||
if pl.is_closed && pl.vertices.len() >= 3
|
||||
) || matches!(e, acadrust::EntityType::Circle(_));
|
||||
if supported {
|
||||
let Some(curve) = crate::entities::curve::entity_curve(e) else {
|
||||
let Some((plane, loops, true)) =
|
||||
crate::scene::model::presspull_model::profile_geometry(e)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
loops.push(
|
||||
crate::entities::curve::curve_points(&curve)
|
||||
.into_iter()
|
||||
.map(|point| Vector3::new(point[0], point[1], point[2]))
|
||||
.collect(),
|
||||
let Some(body) = cadkernel::brep::planar_region(plane, &loops) else {
|
||||
continue;
|
||||
};
|
||||
let mut region = Region::new();
|
||||
region.point_of_reference = Vector3::new(
|
||||
plane.origin[0],
|
||||
plane.origin[1],
|
||||
plane.origin[2],
|
||||
);
|
||||
region.common.layer = self.tabs[i].active_layer.clone();
|
||||
regions.push((region, body));
|
||||
}
|
||||
}
|
||||
if loops.is_empty() {
|
||||
if regions.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REGION: select closed polylines or circles.").as_ref());
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "REGION");
|
||||
let count = loops.len();
|
||||
for pts in loops {
|
||||
let mut w = Wire::new();
|
||||
let first = pts.first().copied().unwrap_or(Vector3::new(0.0, 0.0, 0.0));
|
||||
w.points = pts;
|
||||
let mut r = Region::new();
|
||||
r.point_of_reference = first;
|
||||
r.wires = vec![w];
|
||||
r.common.layer = self.tabs[i].active_layer.clone();
|
||||
self.tabs[i].scene.add_entity(acadrust::EntityType::Region(r));
|
||||
let count = regions.len();
|
||||
let mut created = Vec::with_capacity(count);
|
||||
for (region, body) in regions {
|
||||
let handle = self.add_region_model(region, body);
|
||||
if handle.is_null() {
|
||||
self.tabs[i].scene.rollback_new_entities(&created);
|
||||
self.discard_last_undo_entry(i);
|
||||
return Some(iced::Task::none());
|
||||
}
|
||||
created.push(handle);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
@@ -1370,21 +1418,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));
|
||||
}
|
||||
@@ -1403,7 +1481,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}.",
|
||||
|
||||
Reference in new issue
Block a user