feat(model): add exact solid modelling tools

This commit is contained in:
Hakan Seven committed 2026-08-23 22:53:31 +03:00
1 parent fb2ea1e831
commit 06cd0ec6a9
51 files changed
+1487 -574

No files matched your search

+129 -148
View File
@@ -1204,15 +1204,17 @@ impl OpenCADStudio {
} => {
let label = self.history_label_from_active_cmd(i, "SOLID");
let pending = self.begin_undo(i, label, 1, true);
self.add_solid_model(entity, *solid, history);
self.tabs[i].dirty = true;
let handle = self.add_solid_model(entity, *solid, history);
if !handle.is_null() {
self.tabs[i].dirty = true;
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::CommitAndEditText(entity) => {
let label = self.history_label_from_active_cmd(i, "ENTITY");
@@ -3001,80 +3003,32 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pending);
}
}
// ── Solid3D creation (BOX / SPHERE / CYLINDER) ────────────────
CmdResult::CommitSolid3D { mesh_fn } => {
use crate::modules::insert::solid3d_cmds::empty_solid3d;
let pending = self.begin_undo(i, "SOLID3D", 1, true);
let entity = empty_solid3d();
let handle = self.tabs[i].scene.add_entity(entity);
if !handle.is_null() {
let name = format!("{}", handle.value());
let color = [0.6f32, 0.6, 0.8, 1.0]; // default colour; command embedded it
let _ = color; // color is captured inside mesh_fn
if let Some(mesh) = mesh_fn(name) {
let set = crate::scene::MeshLodSet::from_single(mesh);
if let Some(acadrust::EntityType::Solid3D(entity)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
let center = set.metrics.centroid;
entity.point_of_reference = acadrust::types::Vector3::new(
center[0], center[1], center[2],
);
}
self.tabs[i].scene.meshes.insert(handle, set);
}
self.tabs[i].dirty = true;
self.command_line.push_output(crate::t!("Solid created.").as_ref());
}
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
}
// ── EXTRUDE ────────────────────────────────────────────────────
CmdResult::ExtrudeEntity {
handle,
height,
color,
color: _,
} => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::{solid_model, sweep_model};
use crate::scene::model::sweep_model;
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
if let Some(entity) = entity_opt {
// The kernel sweeps the profile into analytic surfaces —
// a straight run becomes a plane and an arc a cylinder —
// so the solid saves as exact ACIS rather than facets.
let result = sweep_model::extruded(&entity, height as f64)
.and_then(|body| Some((solid_model::mesh_from_solid(&body, color)?, body)));
if let Some((mesh, solid)) = result {
let history = crate::scene::model::solid_history::extrusion_op(
&entity,
height as f64,
);
if let Some(solid) = sweep_model::extruded(&entity, height) {
let history = crate::scene::model::solid_history::brep_op(&solid);
let pending = self.begin_undo(i, "EXTRUDE", 1, true);
let mut s3d = empty_solid3d();
if let acadrust::EntityType::Solid3D(inner) = &mut s3d {
inner.wires = solid_model::edge_wires(&solid);
}
let new_handle = self.tabs[i].scene.add_entity(s3d);
self.tabs[i]
.scene
.create_solid_history(new_handle, history);
self.tabs[i].scene.register_solid_model(new_handle, solid);
let _ = mesh;
self.tabs[i].dirty = true;
self.command_line.push_output(crate::t!("EXTRUDE: solid created.").as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
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!("EXTRUDE: solid created.").as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
} else {
self.command_line.push_error(crate::t!("EXTRUDE: could not build profile. Select a closed 2D entity (Circle, LwPolyline, etc.).").as_ref());
@@ -3088,26 +3042,80 @@ impl OpenCADStudio {
self.restore_pre_cmd_tangent();
}
CmdResult::PresspullEntity {
handle,
pick,
distance,
drag,
color,
} => {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_))
) {
let task = self.solid_face_presspull(handle, pick, distance, drag);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
return task;
}
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::{solid_history, sweep_model};
let result = self.tabs[i].scene.document.get_entity(handle).and_then(|entity| {
let distance = match drag {
Some(point) => sweep_model::projected_drag(entity, pick, point)?,
None => distance,
};
sweep_model::extruded(entity, distance)
});
if let Some(solid) = result {
let pending = self.begin_undo(i, "PRESSPULL", 1, true);
let history = solid_history::brep_op(&solid);
let created = self.add_solid_model(empty_solid3d(), solid, history);
let _ = color;
if !created.is_null() {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::t!("PRESSPULL: solid created.").as_ref());
if let Some(delta) = pending {
self.commit_undo_delta(i, delta);
}
}
} else {
self.command_line.push_error(
crate::t!("PRESSPULL: select a closed profile or planar solid face.")
.as_ref(),
);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
}
// ── REVOLVE ────────────────────────────────────────────────────
CmdResult::RevolveEntity {
handle,
axis_start,
axis_end,
angle_deg,
color,
color: _,
} => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::{solid_model, sweep_model};
use crate::scene::model::sweep_model;
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
if let Some(entity) = entity_opt {
// A line turned about the axis sweeps into a plane, a
// cylinder or a cone, and an arc into a sphere or a
// torus, so a revolved solid keeps exact geometry too.
let result = sweep_model::revolved(
&entity,
[
@@ -3117,31 +3125,19 @@ impl OpenCADStudio {
],
[axis_end.x as f64, axis_end.y as f64, axis_end.z as f64],
(angle_deg as f64).to_radians(),
)
.and_then(|body| Some((solid_model::mesh_from_solid(&body, color)?, body)));
if let Some((mesh, solid)) = result {
let history = crate::scene::model::solid_history::revolve_op(
&entity,
axis_start.to_array(),
axis_end.to_array(),
(angle_deg as f64).to_radians(),
);
);
if let Some(solid) = result {
let history = crate::scene::model::solid_history::brep_op(&solid);
let pending = self.begin_undo(i, "REVOLVE", 1, true);
let mut s3d = empty_solid3d();
if let acadrust::EntityType::Solid3D(inner) = &mut s3d {
inner.wires = solid_model::edge_wires(&solid);
}
let new_handle = self.tabs[i].scene.add_entity(s3d);
self.tabs[i]
.scene
.create_solid_history(new_handle, history);
self.tabs[i].scene.register_solid_model(new_handle, solid);
let _ = mesh;
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
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::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref(),
);
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
} else {
self.command_line
@@ -3160,7 +3156,7 @@ impl OpenCADStudio {
CmdResult::SweepEntity {
profile_handle,
path_handle,
color,
color: _,
} => {
if self.reject_locked_edit(i, profile_handle)
|| self.reject_locked_edit(i, path_handle)
@@ -3169,7 +3165,7 @@ impl OpenCADStudio {
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::sweep_model;
use crate::scene::model::{solid_history, sweep_model};
let profile_ent = self.tabs[i]
.scene
@@ -3177,39 +3173,21 @@ impl OpenCADStudio {
.get_entity(profile_handle)
.cloned();
let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned();
let history = profile_ent
.as_ref()
.zip(path_ent.as_ref())
.map(|(profile, path)| {
crate::scene::model::solid_history::sweep_op(profile, path)
});
let result = profile_ent
.zip(path_ent)
.and_then(|(profile, path)| sweep_model::swept(&profile, &path, color));
.and_then(|(profile, path)| sweep_model::swept(&profile, &path));
if let Some(mut set) = result {
if let Some(solid) = result {
let pending = self.begin_undo(i, "SWEEP", 1, true);
let mut entity = empty_solid3d();
if let acadrust::EntityType::Solid3D(solid) = &mut entity {
let center = set.metrics.centroid;
solid.point_of_reference = acadrust::types::Vector3::new(
center[0], center[1], center[2],
);
}
let new_handle = self.tabs[i].scene.add_entity(entity);
if let Some(history) = history {
self.tabs[i]
.scene
.create_solid_history(new_handle, history);
}
for mesh in &mut set.lods {
mesh.name = format!("{}", new_handle.value());
}
self.tabs[i].scene.meshes.insert(new_handle, set);
self.tabs[i].dirty = true;
self.command_line.push_output(crate::t!("SWEEP: solid created.").as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
let history = solid_history::brep_op(&solid);
let created = self.add_solid_model(empty_solid3d(), solid, history);
if !created.is_null() {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::t!("SWEEP: solid created.").as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
} else {
self.command_line.push_error(crate::t!("SWEEP: could not sweep the profile along the path.").as_ref());
@@ -3221,7 +3199,7 @@ impl OpenCADStudio {
}
// ── LOFT ──────────────────────────────────────────────────────
CmdResult::LoftEntities { handles, color } => {
CmdResult::LoftEntities { handles, color: _ } => {
if let Some(handle) = handles
.iter()
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
@@ -3232,34 +3210,23 @@ impl OpenCADStudio {
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::sweep_model;
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();
if let Some(mut set) = sweep_model::lofted(&profiles, color) {
let history = crate::scene::model::solid_history::loft_op(&profiles);
if let Some(solid) = sweep_model::lofted(&profiles) {
let pending = self.begin_undo(i, "LOFT", 1, true);
let mut entity = empty_solid3d();
if let acadrust::EntityType::Solid3D(solid) = &mut entity {
let center = set.metrics.centroid;
solid.point_of_reference = acadrust::types::Vector3::new(
center[0], center[1], center[2],
);
}
let new_handle = self.tabs[i].scene.add_entity(entity);
self.tabs[i]
.scene
.create_solid_history(new_handle, history);
for mesh in &mut set.lods {
mesh.name = format!("{}", new_handle.value());
}
self.tabs[i].scene.meshes.insert(new_handle, set);
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);
let history = solid_history::brep_op(&solid);
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);
}
}
} else {
self.command_line.push_error(crate::t!("LOFT: select at least two closed profiles.").as_ref());
@@ -3270,6 +3237,20 @@ impl OpenCADStudio {
self.restore_pre_cmd_tangent();
}
CmdResult::SolidEdgeBlend {
handle,
pick,
value,
fillet,
} => {
let task = self.solid_edge_blend(handle, pick, value, fillet);
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,
+38
View File
@@ -1,5 +1,15 @@
use super::*;
fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
let selected = app.tabs[tab].scene.selected_handles_in_order();
(selected.len() == 1
&& matches!(
app.tabs[tab].scene.document.get_entity(selected[0]),
Some(acadrust::EntityType::Solid3D(_))
))
.then_some(selected[0])
}
impl OpenCADStudio {
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
@@ -950,6 +960,20 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"SOLIDFILLET" => {
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"FILLET" if selected_solid(self, i).is_some() => {
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"FILLET" => {
use crate::modules::draw::modify::fillet::FilletCommand;
let entities: Vec<_> = self.tabs[i]
@@ -1083,6 +1107,20 @@ impl OpenCADStudio {
}
}
"SOLIDCHAMFER" => {
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
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));
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"CHAMFER" => {
use crate::modules::draw::modify::fillet::ChamferCommand;
let entities: Vec<_> = self.tabs[i]
+34 -10
View File
@@ -466,31 +466,55 @@ impl OpenCADStudio {
}
}
// BOX / SPHERE / CYLINDER / CONE / WEDGE / TORUS are handled by the
// Model-tab primitive command above (with the kernel boolean caching).
// ── EXTRUDE ────────────────────────────────────────────────────
// PRESSPULL on a closed boundary creates a solid by extruding it to a
// height — the same operation as EXTRUDE. THICKEN turns a closed planar
// profile into a solid of the given thickness, which is also an extrude.
"EXTRUDE" | "PRESSPULL" | "THICKEN" => {
"EXTRUDE" | "THICKEN" => {
use crate::modules::insert::solid3d_cmds::ExtrudeCommand;
// If a single entity is already selected, skip the pick step.
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
if selected.len() == 1 {
let handle = selected[0].0;
let mut cmd = ExtrudeCommand::new(color);
cmd.on_entity_pick(handle, glam::DVec3::ZERO);
let mut cmd = ExtrudeCommand::new_named(cmd, color);
if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
cmd.set_entity_pick_direction(
curve.plane.normal().map(glam::DVec3::from_array),
);
cmd.on_entity_pick(
handle,
glam::DVec3::from_array(curve.plane.origin),
);
}
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
let cmd = ExtrudeCommand::new(color);
let cmd = ExtrudeCommand::new_named(cmd, color);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
}
"PRESSPULL" => {
use crate::modules::insert::solid3d_cmds::PresspullCommand;
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let mut command = PresspullCommand::new(color);
if selected.len() == 1
&& !matches!(selected[0].1, acadrust::EntityType::Solid3D(_))
{
if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
command.set_entity_pick_direction(
curve.plane.normal().map(glam::DVec3::from_array),
);
command.on_entity_pick(
selected[0].0,
glam::DVec3::from_array(curve.plane.origin),
);
}
}
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
// ── REVOLVE ────────────────────────────────────────────────────
"REVOLVE" => {
use crate::modules::insert::solid3d_cmds::RevolveCommand;
+4 -13
View File
@@ -925,14 +925,15 @@ impl OpenCADStudio {
}
// ── Model commands (3D primitives) ─────────────────────────────
"BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "TORUS" => {
"BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "PYRAMID" | "PYR"
| "TORUS" => {
use crate::modules::model::primitive_cmd::PrimitiveCommand;
let new_cmd = PrimitiveCommand::new(cmd);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
// ── Design commands (solid booleans) ───────────────────────────
// ── Solid booleans ─────────────────────────────────────────────
"UNION" | "SUBTRACT" | "INTERSECT" => {
use crate::modules::model::boolean_cmd::BoolOp;
if let Some(op) = BoolOp::from_id(cmd) {
@@ -1033,17 +1034,7 @@ impl OpenCADStudio {
}
}
// PYRAMID <radius> <height> [sides] — create an n-sided pyramid mesh.
"PYRAMID" | "PYR" => {
use crate::command::TwoValuePromptCommand;
let c = TwoValuePromptCommand::new(
"PYRAMID",
"PYRAMID base radius:",
"PYRAMID height (add sides by typing a 3rd number):",
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
// PYRAMID <radius> <height> [sides] keeps the direct numeric form.
cmd if cmd.starts_with("PYRAMID ") || cmd.starts_with("PYR ") => {
let nums: Vec<f64> = cmd
.split_whitespace()
+204 -79
View File
@@ -1,15 +1,9 @@
// 3D solid modelling support on the App: committing Model-tab primitives,
// and the Design-group boolean operations over the scene's cached
// B-reps.
//
// The bodies come from the geometry kernel, so every operation here is on
// analytic surfaces rather than on facets — a turned cylinder is still a
// cylinder afterwards, and saves as one.
// Kernel B-rep solid modelling and exact ACIS persistence.
use acadrust::entities::Solid3D;
use acadrust::objects::SolidHistoryOperation;
use acadrust::{
entities::Solid3D, objects::SolidHistoryOperation, EntityType, Handle,
};
use cadkernel::brep::Body;
use acadrust::{EntityType, Handle};
use iced::Task;
use super::Message;
@@ -18,16 +12,25 @@ use crate::scene::model::solid_history;
use crate::scene::model::solid_model::{self, Bool};
impl super::OpenCADStudio {
/// Commit a Model-tab solid: add its acadrust entity to the document, then
/// register the B-rep (caches it for booleans + tessellates it into the
/// shaded mesh pipeline). Returns the new entity handle.
/// Add a solid and register its persistent B-rep.
pub(super) fn add_solid_model(
&mut self,
entity: EntityType,
mut entity: EntityType,
solid: Body,
history: SolidHistoryOperation,
) -> Handle {
let i = self.active_tab;
let EntityType::Solid3D(inner) = &mut entity else {
return Handle::NULL;
};
inner.wires = solid_model::edge_wires(&solid);
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&solid)
else {
self.command_line
.push_error(crate::t!("The solid could not be encoded as ACIS.").as_ref());
return Handle::NULL;
};
inner.set_sat_document(&document);
let Some(handle) = self.commit_entity_handle(entity) else {
return Handle::NULL;
};
@@ -55,35 +58,183 @@ impl super::OpenCADStudio {
handles
}
/// Run a boolean (`union` / `subtract` / `intersect`) on exactly two
/// selected solids whose B-reps can be restored by the kernel.
fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> 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);
self.push_undo_snapshot(i, label);
self.tabs[i].scene.delete_solid_history(handle);
if !self.tabs[i]
.scene
.update_entity(EntityType::Solid3D(entity))
{
self.command_line
.push_error(crate::t!("The solid could not be updated.").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_solid_model(handle, result);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].dirty = true;
self.refresh_properties();
true
}
pub(super) fn solid_edge_blend(
&mut self,
handle: Handle,
pick: glam::DVec3,
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(_))
) {
self.command_line
.push_error(crate::t!("Select a 3D solid edge.").as_ref());
return Task::none();
}
self.tabs[i].scene.restore_solid_models(&[handle]);
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
self.command_line
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
return Task::none();
};
let Some(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)
} else {
cadkernel::brep::chamfer(&body, edge, value)
};
let Some(result) = result else {
self.command_line.push_error(
crate::t!("Edge operation failed. Use a convex planar solid and a smaller value.")
.as_ref(),
);
return Task::none();
};
let label = if fillet { "SOLIDFILLET" } else { "SOLIDCHAMFER" };
if self.replace_solid_body(handle, result, label) {
self.command_line
.push_output(crate::tf!("{label}: solid updated.").as_ref());
}
Task::none()
}
pub(super) fn solid_face_presspull(
&mut self,
handle: Handle,
pick: glam::DVec3,
distance: f64,
drag: Option<glam::DVec3>,
) -> Task<Message> {
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return Task::none();
}
self.tabs[i].scene.restore_solid_models(&[handle]);
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
self.command_line
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
return Task::none();
};
let Some(face) = solid_model::nearest_planar_face(&body, pick.to_array()) else {
self.command_line
.push_error(crate::t!("Select a planar solid face.").as_ref());
return Task::none();
};
let distance = match drag {
Some(point) => {
let Some(normal) = solid_model::planar_face_normal(&body, face) else {
self.command_line
.push_error(crate::t!("Select a planar solid face.").as_ref());
return Task::none();
};
(point - pick).dot(glam::DVec3::from_array(normal))
}
None => distance,
};
if !distance.is_finite() || distance.abs() <= 1e-6 {
self.command_line.push_error(
crate::t!("PRESSPULL: drag along the selected face normal.").as_ref(),
);
return Task::none();
}
let Some(result) = cadkernel::brep::presspull(&body, face, distance) else {
self.command_line.push_error(
crate::t!("PRESSPULL: the face move would collapse or invalidate the solid.")
.as_ref(),
);
return Task::none();
};
if self.replace_solid_body(handle, result, "PRESSPULL") {
self.command_line
.push_output(crate::t!("PRESSPULL: solid updated.").as_ref());
}
Task::none()
}
/// Run a boolean over the selected solids in selection order.
pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task<Message> {
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.len() != 2 {
if handles.len() < 2 {
self.command_line
.push_error(crate::t!("Boolean: select exactly two solids created this session.").as_ref());
.push_error(crate::t!("Boolean: select at least two solids.").as_ref());
return Task::none();
}
let a = self.tabs[i].scene.solid_models[&handles[0]].clone();
let b = self.tabs[i].scene.solid_models[&handles[1]].clone();
let kind = match op {
BoolOp::Union => Bool::Union,
BoolOp::Subtract => Bool::Subtract,
BoolOp::Intersect => Bool::Intersect,
};
let Some(result) = solid_model::boolean(kind, &a, &b) else {
let mut operands = handles
.iter()
.filter_map(|handle| self.tabs[i].scene.solid_models.get(handle).cloned());
let mut result = operands.next().expect("at least two restored solids");
for operand in operands {
let Some(combined) = solid_model::boolean(kind, &result, &operand) else {
self.command_line.push_error(
crate::t!("Boolean failed while combining the selected solids.").as_ref(),
);
return Task::none();
};
result = combined;
}
if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() {
self.command_line
.push_error(crate::t!("Boolean failed — the solids may not overlap.").as_ref());
.push_error(crate::t!("The boolean result could not be encoded as ACIS.").as_ref());
return Task::none();
};
}
self.push_undo_snapshot(i, "BOOLEAN");
// Remove the two operands (entity + mesh + cached B-rep).
self.tabs[i].scene.erase_entities(&handles);
// The result is freshly combined geometry with no ACIS parametrisation,
// so it lives as a Solid3D whose render/boolean data is the injected
// mesh + cached B-rep; its edge wires make it pickable.
let mut s3d = Solid3D::new();
s3d.wires = solid_model::edge_wires(&result);
let history = solid_history::brep_op(&result);
@@ -100,7 +251,7 @@ impl super::OpenCADStudio {
/// 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 the Design-group tools use.
/// the same boolean path as the modelling tools.
pub(super) fn solid_slice(&mut self, axis: usize, value: f64, keep_low: bool) -> Task<Message> {
let i = self.active_tab;
let handles = self.selected_solid_handles();
@@ -243,63 +394,34 @@ impl super::OpenCADStudio {
Task::none()
}
/// POLYSOLID — build a wall-like solid from a selected polyline: one box per
/// segment (oriented along it, `width` wide, `height` tall) unioned together,
/// reusing the box primitive + the boolean union path.
/// POLYSOLID — build a wall solid around an exact polyline offset.
pub(super) fn solid_polysolid(&mut self, width: f64, height: f64) -> Task<Message> {
let i = self.active_tab;
let found: Option<(Handle, Vec<[f64; 3]>)> = self.tabs[i]
let found: Option<(Handle, EntityType)> = self.tabs[i]
.scene
.selected_entities()
.iter()
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
.find_map(|(h, e)| match e {
EntityType::LwPolyline(_) => crate::entities::curve::entity_curve(e)
.map(|curve| (*h, crate::entities::curve::curve_points(&curve))),
EntityType::LwPolyline(_) => Some((*h, (*e).clone())),
_ => None,
});
let Some((handle, pts)) = found else {
let Some((handle, entity)) = found else {
self.command_line
.push_error(crate::t!("POLYSOLID: select a polyline first.").as_ref());
return Task::none();
};
let segs: Vec<([f64; 3], [f64; 3])> = pts.windows(2).map(|w| (w[0], w[1])).collect();
let mut acc: Option<Body> = None;
let mut used = 0usize;
for (a, b) in &segs {
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-9 {
continue;
}
let angle = dy.atan2(dx);
// Local box: X 0..len, Y -w/2..w/2, Z 0..h, then turned to lie
// along the segment and moved onto its start.
let (sin, cos) = angle.sin_cos();
let Some(bx) = solid_model::box_solid([len / 2.0, 0.0, height / 2.0], len, width, height)
.and_then(|bx| {
solid_model::placed(
&bx,
[cos, sin, 0.0],
[-sin, cos, 0.0],
[0.0, 0.0, 1.0],
[a[0], a[1], a[2]],
)
})
else {
continue;
};
acc = Some(match acc {
None => bx,
Some(prev) => solid_model::boolean(Bool::Union, &prev, &bx).unwrap_or(prev),
});
used += 1;
}
let Some(result) = acc else {
let Some(result) = crate::scene::model::sweep_model::polysolid(&entity, width, height)
else {
self.command_line
.push_error(crate::t!("POLYSOLID: the polyline has no usable segments.").as_ref());
.push_error(crate::t!("POLYSOLID: the kernel could not offset the polyline.").as_ref());
return Task::none();
};
if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() {
self.command_line
.push_error(crate::t!("POLYSOLID: the result could not be encoded as ACIS.").as_ref());
return Task::none();
}
self.push_undo_snapshot(i, "POLYSOLID");
self.tabs[i].scene.erase_entities(&[handle]);
let mut s3d = Solid3D::new();
@@ -310,11 +432,14 @@ impl super::OpenCADStudio {
if !h.is_null() {
self.tabs[i].scene.select_entity(h, false);
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(crate::tf!(
"POLYSOLID: built a wall solid from {used} segment(s) (width {width}, height {height})."
).as_ref());
if !h.is_null() {
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::tf!("POLYSOLID: created a wall solid (width {width}, height {height}).")
.as_ref(),
);
}
Task::none()
}
@@ -507,12 +632,12 @@ impl super::OpenCADStudio {
self.tabs[i].scene.deselect_all();
if !handle.is_null() {
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(crate::tf!(
"PYRAMID: created a {n}-sided pyramid (radius {radius}, height {height})."
).as_ref());
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(crate::tf!(
"PYRAMID: created a {n}-sided pyramid (radius {radius}, height {height})."
).as_ref());
Task::none()
}
+1 -1
View File
@@ -258,7 +258,7 @@ impl OpenCADStudio {
let body = scene.solid_models.get(&h);
let sat = needs_acis
.then(|| {
body.and_then(crate::scene::convert::acis_export::planar_solid_to_sat)
body.and_then(crate::scene::convert::acis_export::solid_to_sat)
})
.flatten();
let wires = needs_wires.then(|| {
+35 -3
View File
@@ -3454,6 +3454,38 @@ impl OpenCADStudio {
})?
});
if let Some(handle) = hit {
let uses_surface_point = self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| command.entity_pick_uses_surface_point());
let entity_pick_point = if uses_surface_point {
self.tabs[i]
.scene
.solid_click_point_for(p, view_rot2, eye2, bounds, handle)
.unwrap_or(pick_wcs)
} else {
pick_wcs
};
let entity_pick_direction = if uses_surface_point {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_))
) {
self.tabs[i]
.scene
.solid_planar_face_normal_at(handle, entity_pick_point)
} else {
self.tabs[i]
.scene
.document
.get_entity(handle)
.and_then(crate::entities::curve::entity_curve)
.and_then(|curve| curve.plane.normal())
.map(glam::DVec3::from_array)
}
} else {
None
};
// Some commands (e.g. SS_CATCHMENT) need the entity
// body before `on_entity_pick` can advance.
let inject_first = self.tabs[i]
@@ -3484,7 +3516,8 @@ impl OpenCADStudio {
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
// Shift-swap state for TRIM/EXTEND (#336).
c.set_shift(shift);
c.on_entity_pick(handle, pick_wcs)
c.set_entity_pick_direction(entity_pick_direction);
c.on_entity_pick(handle, entity_pick_point)
});
// HATCHEDIT: after pick, inject hatch model data into the command.
if self.tabs[i]
@@ -4011,8 +4044,7 @@ impl OpenCADStudio {
)
})
.or_else(|| {
// Block-internal hatch: resolve to the
// parent Insert (AutoCAD behaviour).
// Block-internal hatch: resolve to the parent Insert.
scene::pick::hit_test::click_hit_insert_hatch(
p,
self.tabs[i].scene.insert_hatches_for_click().as_ref(),