forked from 1b-agent/opencadstudio-upstream
feat(model): add exact solid modelling tools
This commit is contained in:
1 parent
fb2ea1e831
commit
06cd0ec6a9
51 files changed
+1487
-574
No files matched your search
@@ -1,8 +1,8 @@
|
||||
// Boolean operations on 3D solids (Design group). The actual CSG runs in
|
||||
// Boolean operations on 3D solids. The actual CSG runs in
|
||||
// `App::solid_boolean` (src/app/model_ops.rs) using the kernel on the
|
||||
// cached B-reps; this module just names the operations.
|
||||
|
||||
/// Which boolean a Design-group tool performs on the two selected solids.
|
||||
/// Which boolean operation to apply to selected solids.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum BoolOp {
|
||||
Union,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
use acadrust::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{
|
||||
CadCommand, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EdgeOperation {
|
||||
Fillet,
|
||||
Chamfer,
|
||||
}
|
||||
|
||||
pub struct SolidEdgeCommand {
|
||||
operation: EdgeOperation,
|
||||
target: Option<Handle>,
|
||||
picked: Option<(Handle, DVec3)>,
|
||||
default_value: f64,
|
||||
}
|
||||
|
||||
impl SolidEdgeCommand {
|
||||
pub fn new(operation: EdgeOperation, target: Option<Handle>) -> Self {
|
||||
Self {
|
||||
operation,
|
||||
target,
|
||||
picked: None,
|
||||
default_value: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, value: f64) -> CmdResult {
|
||||
let Some((handle, pick)) = self.picked else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
CmdResult::SolidEdgeBlend {
|
||||
handle,
|
||||
pick,
|
||||
value,
|
||||
fillet: matches!(self.operation, EdgeOperation::Fillet),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for SolidEdgeCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
match self.operation {
|
||||
EdgeOperation::Fillet => "SOLIDFILLET",
|
||||
EdgeOperation::Chamfer => "SOLIDCHAMFER",
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.picked.is_none() {
|
||||
return crate::t!("Select a solid edge:").into_owned();
|
||||
}
|
||||
match self.operation {
|
||||
EdgeOperation::Fillet => {
|
||||
crate::tf!("Specify fillet radius <{:.3}>:", self.default_value).into_owned()
|
||||
}
|
||||
EdgeOperation::Chamfer => {
|
||||
crate::tf!("Specify chamfer distance <{:.3}>:", self.default_value).into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.picked.is_none()
|
||||
}
|
||||
|
||||
fn entity_pick_includes_fills(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||
if handle.is_null() || self.target.is_some_and(|target| target != handle) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.picked = Some((handle, point));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
let Some((_, pick)) = self.picked else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let value = point.distance(pick);
|
||||
if value > 0.0 && value.is_finite() {
|
||||
self.finish(value)
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.picked.is_none() {
|
||||
return None;
|
||||
}
|
||||
let value = text.trim().parse::<f64>().ok()?;
|
||||
(value > 0.0 && value.is_finite()).then(|| self.finish(value))
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.picked.is_some() {
|
||||
self.finish(self.default_value)
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.picked.is_some()
|
||||
}
|
||||
|
||||
fn dyn_commit_as_text(&self) -> bool {
|
||||
self.picked.is_some()
|
||||
}
|
||||
|
||||
fn dyn_spec(&self) -> Option<DynSpec> {
|
||||
let (_, pick) = self.picked?;
|
||||
Some(DynSpec {
|
||||
anchor: DynAnchor::Point(pick),
|
||||
fields: vec![DynFieldSpec::new(match self.operation {
|
||||
EdgeOperation::Fillet => DynRole::Radius,
|
||||
EdgeOperation::Chamfer => DynRole::Distance,
|
||||
})],
|
||||
guide: DynGuide::Radius,
|
||||
ref_point: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
self.picked.map(|(_, pick)| cursor.distance(pick))
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["SOLIDFILLET", "SOLIDCHAMFER"]
|
||||
});
|
||||
+37
-17
@@ -1,11 +1,7 @@
|
||||
// Model module — 3D solid modelling.
|
||||
//
|
||||
// Model group : create primitive solids (box, cylinder, cone, sphere, …)
|
||||
// as ACIS Solid3D entities via acadrust's primitive builders.
|
||||
// Design group : combine solids with the kernel boolean operations
|
||||
// (union / subtract / intersect).
|
||||
// Solid creation and kernel modelling tools.
|
||||
|
||||
pub mod boolean_cmd;
|
||||
pub mod edge_cmd;
|
||||
pub mod primitive_cmd;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||
@@ -16,11 +12,20 @@ const BOX_ICON: &[u8] = include_bytes!("../../../assets/icons/box3d.svg");
|
||||
const CYLINDER_ICON: &[u8] = include_bytes!("../../../assets/icons/cylinder3d.svg");
|
||||
const CONE_ICON: &[u8] = include_bytes!("../../../assets/icons/cone3d.svg");
|
||||
const SPHERE_ICON: &[u8] = include_bytes!("../../../assets/icons/sphere3d.svg");
|
||||
const PYRAMID_ICON: &[u8] = include_bytes!("../../../assets/icons/pyramid3d.svg");
|
||||
const WEDGE_ICON: &[u8] = include_bytes!("../../../assets/icons/wedge3d.svg");
|
||||
const TORUS_ICON: &[u8] = include_bytes!("../../../assets/icons/torus3d.svg");
|
||||
const POLYSOLID_ICON: &[u8] = include_bytes!("../../../assets/icons/polysolid.svg");
|
||||
const EXTRUDE_ICON: &[u8] = include_bytes!("../../../assets/icons/extrude.svg");
|
||||
const REVOLVE_ICON: &[u8] = include_bytes!("../../../assets/icons/revolve.svg");
|
||||
const LOFT_ICON: &[u8] = include_bytes!("../../../assets/icons/loft.svg");
|
||||
const SWEEP_ICON: &[u8] = include_bytes!("../../../assets/icons/sweep.svg");
|
||||
const PRESSPULL_ICON: &[u8] = include_bytes!("../../../assets/icons/presspull.svg");
|
||||
const UNION_ICON: &[u8] = include_bytes!("../../../assets/icons/union.svg");
|
||||
const SUBTRACT_ICON: &[u8] = include_bytes!("../../../assets/icons/subtract.svg");
|
||||
const INTERSECT_ICON: &[u8] = include_bytes!("../../../assets/icons/intersect.svg");
|
||||
const FILLET_ICON: &[u8] = include_bytes!("../../../assets/icons/fillet.svg");
|
||||
const CHAMFER_ICON: &[u8] = include_bytes!("../../../assets/icons/chamfer.svg");
|
||||
|
||||
/// Helper to declare a ribbon tool that fires a named command.
|
||||
fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
|
||||
@@ -37,7 +42,7 @@ impl CadModule for ModelModule {
|
||||
"model"
|
||||
}
|
||||
fn title(&self) -> &'static str {
|
||||
"Model"
|
||||
"Modelling"
|
||||
}
|
||||
|
||||
fn ribbon_groups(&self) -> &[RibbonGroup] {
|
||||
@@ -45,31 +50,46 @@ impl CadModule for ModelModule {
|
||||
GROUPS.get_or_init(|| {
|
||||
vec![
|
||||
RibbonGroup {
|
||||
title: "Model",
|
||||
title: "Create",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("BOX", "Box", BOX_ICON)),
|
||||
RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", CYLINDER_ICON)),
|
||||
RibbonItem::LargeTool(tool("CONE", "Cone", CONE_ICON)),
|
||||
RibbonItem::LargeTool(tool("SPHERE", "Sphere", SPHERE_ICON)),
|
||||
RibbonItem::Dropdown {
|
||||
id: "MODEL_MORE",
|
||||
icon: IconKind::Svg(WEDGE_ICON),
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "MODEL_PRIMITIVES",
|
||||
label: "Box",
|
||||
icon: IconKind::Svg(BOX_ICON),
|
||||
items: vec![
|
||||
("BOX", "Box", IconKind::Svg(BOX_ICON)),
|
||||
("CYLINDER", "Cylinder", IconKind::Svg(CYLINDER_ICON)),
|
||||
("CONE", "Cone", IconKind::Svg(CONE_ICON)),
|
||||
("SPHERE", "Sphere", IconKind::Svg(SPHERE_ICON)),
|
||||
("PYRAMID", "Pyramid", IconKind::Svg(PYRAMID_ICON)),
|
||||
("WEDGE", "Wedge", IconKind::Svg(WEDGE_ICON)),
|
||||
("TORUS", "Torus", IconKind::Svg(TORUS_ICON)),
|
||||
("POLYSOLID", "Polysolid", IconKind::Svg(POLYSOLID_ICON)),
|
||||
],
|
||||
default: "WEDGE",
|
||||
default: "BOX",
|
||||
},
|
||||
RibbonItem::LargeTool(tool("EXTRUDE", "Extrude", EXTRUDE_ICON)),
|
||||
RibbonItem::LargeTool(tool("REVOLVE", "Revolve", REVOLVE_ICON)),
|
||||
RibbonItem::LargeTool(tool("LOFT", "Loft", LOFT_ICON)),
|
||||
RibbonItem::LargeTool(tool("SWEEP", "Sweep", SWEEP_ICON)),
|
||||
RibbonItem::LargeTool(tool("PRESSPULL", "Presspull", PRESSPULL_ICON)),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Design",
|
||||
title: "Boolean",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)),
|
||||
RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_ICON)),
|
||||
RibbonItem::LargeTool(tool("INTERSECT", "Intersect", INTERSECT_ICON)),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Edges",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)),
|
||||
RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)),
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
// 3D primitive creation — BOX / CYLINDER / CONE / SPHERE / WEDGE / PYRAMID /
|
||||
// TORUS. Each is placed CAD-style with a few clicks (planar footprint first,
|
||||
// then a height value), then built as a real ACIS `Solid3D` via acadrust's
|
||||
// `acis::primitives` builders. `Scene::add_entity` tessellates the SAT B-rep
|
||||
// into the 3D mesh pipeline, so the solid renders, selects, and saves to DXF.
|
||||
//
|
||||
// A matching the kernel `Solid` is cached on the scene (see model/mod.rs) when the
|
||||
// entity is committed, so the Design-group boolean tools can combine it.
|
||||
// Interactive kernel primitives stored as ACIS Solid3D entities.
|
||||
|
||||
use acadrust::entities::Solid3D;
|
||||
use acadrust::objects::SolidHistoryOperation;
|
||||
use acadrust::{primitives, EntityType};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
use acadrust::EntityType;
|
||||
use cadkernel::brep::Body;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::scene::model::solid_model;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
||||
/// Which primitive a `PrimitiveCommand` builds.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -26,6 +19,7 @@ pub enum Shape {
|
||||
Cylinder,
|
||||
Cone,
|
||||
Sphere,
|
||||
Pyramid,
|
||||
Torus,
|
||||
}
|
||||
|
||||
@@ -37,6 +31,7 @@ impl Shape {
|
||||
"CYLINDER" => Shape::Cylinder,
|
||||
"CONE" => Shape::Cone,
|
||||
"SPHERE" => Shape::Sphere,
|
||||
"PYRAMID" | "PYR" => Shape::Pyramid,
|
||||
"TORUS" => Shape::Torus,
|
||||
_ => return None,
|
||||
})
|
||||
@@ -48,6 +43,7 @@ impl Shape {
|
||||
Shape::Cylinder => "CYLINDER",
|
||||
Shape::Cone => "CONE",
|
||||
Shape::Sphere => "SPHERE",
|
||||
Shape::Pyramid => "PYRAMID",
|
||||
Shape::Torus => "TORUS",
|
||||
}
|
||||
}
|
||||
@@ -129,12 +125,11 @@ impl PrimitiveCommand {
|
||||
.to_cols_array()
|
||||
}
|
||||
|
||||
/// Build both the acadrust `Solid3D` (ACIS, for persistence) and the
|
||||
/// kernel `Body` (rendering + booleans) from the footprint + `height`.
|
||||
fn build(&self, height: f64) -> Option<(EntityType, Body, SolidHistoryOperation)> {
|
||||
/// Build the persistent kernel body and its history node.
|
||||
fn build(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
|
||||
use crate::scene::model::solid_history;
|
||||
|
||||
let (doc, solid, history) = match self.shape {
|
||||
let (solid, history) = match self.shape {
|
||||
Shape::Box | Shape::Wedge => {
|
||||
let (a, b) = (self.pts[0], self.pts[1]);
|
||||
let length = (b.x - a.x).abs();
|
||||
@@ -150,7 +145,6 @@ impl PrimitiveCommand {
|
||||
a.z + height / 2.0,
|
||||
];
|
||||
(
|
||||
primitives::build_box(center, length, width, height),
|
||||
solid_model::box_solid(center, length, width, height),
|
||||
solid_history::box_op(
|
||||
self.history_transform(origin),
|
||||
@@ -162,7 +156,6 @@ impl PrimitiveCommand {
|
||||
} else {
|
||||
let origin = [a.x.min(b.x), a.y.min(b.y), a.z];
|
||||
(
|
||||
primitives::build_wedge(origin, length, width, height),
|
||||
solid_model::wedge_solid(origin, length, width, height),
|
||||
solid_history::wedge_op(
|
||||
self.history_transform(DVec3::from_array(origin)),
|
||||
@@ -182,7 +175,6 @@ impl PrimitiveCommand {
|
||||
let center = [c.x, c.y, c.z];
|
||||
if self.shape == Shape::Cylinder {
|
||||
(
|
||||
primitives::build_cylinder(center, r, height),
|
||||
solid_model::cylinder_solid(center, r, height),
|
||||
solid_history::cylinder_op(
|
||||
self.history_transform(c),
|
||||
@@ -192,7 +184,6 @@ impl PrimitiveCommand {
|
||||
)
|
||||
} else {
|
||||
(
|
||||
primitives::build_cone(center, r, height),
|
||||
solid_model::cone_solid(center, r, height),
|
||||
solid_history::cone_op(
|
||||
self.history_transform(c),
|
||||
@@ -210,11 +201,21 @@ impl PrimitiveCommand {
|
||||
}
|
||||
let center = [c.x, c.y, c.z];
|
||||
(
|
||||
primitives::build_sphere(center, r),
|
||||
solid_model::sphere_solid(center, r),
|
||||
solid_history::sphere_op(self.history_transform(c), r),
|
||||
)
|
||||
}
|
||||
Shape::Pyramid => {
|
||||
let c = self.pts[0];
|
||||
let r = (self.pts[1] - c).length();
|
||||
if r < 1e-6 || height < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
(
|
||||
solid_model::pyramid_solid([c.x, c.y, c.z], r, height, 4),
|
||||
solid_history::pyramid_op(self.history_transform(c), r, height, 4),
|
||||
)
|
||||
}
|
||||
Shape::Torus => {
|
||||
let c = self.pts[0];
|
||||
let first = (self.pts[1] - c).length();
|
||||
@@ -228,7 +229,6 @@ impl PrimitiveCommand {
|
||||
let minor = (outer - inner) * 0.5;
|
||||
let center = [c.x, c.y, c.z];
|
||||
(
|
||||
primitives::build_torus(center, major, minor),
|
||||
solid_model::torus_solid(center, major, minor),
|
||||
solid_history::torus_op(
|
||||
self.history_transform(c),
|
||||
@@ -239,14 +239,12 @@ impl PrimitiveCommand {
|
||||
}
|
||||
};
|
||||
let solid = solid?;
|
||||
let mut s3d = Solid3D::new();
|
||||
s3d.set_sat_document(&doc);
|
||||
Some((EntityType::Solid3D(s3d), solid, history))
|
||||
Some((solid, history))
|
||||
}
|
||||
|
||||
fn commit(&self, height: f64) -> CmdResult {
|
||||
match self.build(height) {
|
||||
Some((entity, solid, history)) => {
|
||||
Some((solid, history)) => {
|
||||
// Built upright in its own frame, then put on the working
|
||||
// plane — the same move `place_entity` makes for the ACIS
|
||||
// copy, so the two stay on top of each other.
|
||||
@@ -262,11 +260,21 @@ impl PrimitiveCommand {
|
||||
],
|
||||
);
|
||||
match placed {
|
||||
Some(placed) => CmdResult::CommitSolid {
|
||||
entity: self.plane.place_entity(entity),
|
||||
solid: Box::new(placed),
|
||||
history,
|
||||
},
|
||||
Some(placed) => {
|
||||
let Some(document) =
|
||||
crate::scene::convert::acis_export::solid_to_sat(&placed)
|
||||
else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let mut entity = Solid3D::new();
|
||||
entity.set_sat_document(&document);
|
||||
entity.wires = solid_model::edge_wires(&placed);
|
||||
CmdResult::CommitSolid {
|
||||
entity: EntityType::Solid3D(entity),
|
||||
solid: Box::new(placed),
|
||||
history,
|
||||
}
|
||||
}
|
||||
None => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
@@ -390,7 +398,15 @@ impl CadCommand for PrimitiveCommand {
|
||||
|
||||
fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
if shape.radial() {
|
||||
if shape == Shape::Pyramid {
|
||||
let center = pts[0];
|
||||
let radius = (pts[1] - center).length();
|
||||
let corners: [DVec3; 4] = std::array::from_fn(|index| {
|
||||
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
||||
center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
|
||||
});
|
||||
push_loop(&mut points, &corners);
|
||||
} else if shape.radial() {
|
||||
let c = pts[0];
|
||||
let r = (pts[1] - c).length();
|
||||
circle_points(&mut points, c, r);
|
||||
@@ -446,9 +462,21 @@ fn height_wire(shape: Shape, pts: &[DVec3], height: f64) -> WireModel {
|
||||
push_segment(&mut points, low[i], high[i]);
|
||||
}
|
||||
}
|
||||
Shape::Cylinder | Shape::Cone => {
|
||||
Shape::Cylinder | Shape::Cone | Shape::Pyramid => {
|
||||
let center = pts[0];
|
||||
let radius = (pts[1] - center).length();
|
||||
if shape == Shape::Pyramid {
|
||||
let base: [DVec3; 4] = std::array::from_fn(|index| {
|
||||
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
||||
center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
|
||||
});
|
||||
push_loop(&mut points, &base);
|
||||
let apex = center + DVec3::Z * height;
|
||||
for corner in base {
|
||||
push_segment(&mut points, corner, apex);
|
||||
}
|
||||
return wire("primitive_height_preview", points);
|
||||
}
|
||||
push_circle(&mut points, center, radius);
|
||||
if shape == Shape::Cylinder {
|
||||
push_circle(&mut points, center + DVec3::Z * height, radius);
|
||||
@@ -506,7 +534,7 @@ fn circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) {
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["BOX", "WEDGE", "CYLINDER", "CONE", "SPHERE", "TORUS"]
|
||||
names: &["BOX", "WEDGE", "CYLINDER", "CONE", "SPHERE", "PYRAMID", "PYR", "TORUS"]
|
||||
});
|
||||
|
||||
fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
||||
@@ -522,9 +550,9 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
||||
render_instance: None,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: name.into(),
|
||||
points,
|
||||
points_low: Vec::new(),
|
||||
|
||||
Reference in new issue
Block a user