forked from 1b-agent/opencadstudio-upstream
Complete SLICE command behavior
This commit is contained in:
1 parent
febedad916
commit
21a357f55a
12 files changed
+969
-76
No files matched your search
Generated
+1
-1
@@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4486076cb003a5909e3383900c596f92ef599192#4486076cb003a5909e3383900c596f92ef599192"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=70aa5178c59d277b14c13dddffa502633d4cfbbc#70aa5178c59d277b14c13dddffa502633d4cfbbc"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive", "string"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4486076cb003a5909e3383900c596f92ef599192", features = ["acis", "offset"] }
|
||||
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "70aa5178c59d277b14c13dddffa502633d4cfbbc", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -15,7 +15,7 @@ thiserror = "1"
|
||||
|
||||
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"], optional = true }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4486076cb003a5909e3383900c596f92ef599192", features = ["acis", "offset"], optional = true }
|
||||
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "70aa5178c59d277b14c13dddffa502633d4cfbbc", features = ["acis", "offset"], optional = true }
|
||||
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -767,6 +767,19 @@ common =
|
||||
.slice-cut-at-ax-value-kept-the-half = SLICE: cut at __ocs_fmt_0__=__ocs_fmt_1__, kept the __ocs_fmt_2__ half.
|
||||
.slice-select-exactly-one-solid-created-this-se = SLICE: select exactly one solid created this session.
|
||||
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: the plane does not cross the solid on the kept side.
|
||||
.both = Both
|
||||
.slice-select-at-least-one-target = SLICE: select at least one solid or surface.
|
||||
.slice-invalid-plane = SLICE: the cutting plane is invalid.
|
||||
.slice-side-point-on-plane = SLICE: choose a point away from the cutting plane.
|
||||
.slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed.
|
||||
.slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed.
|
||||
.slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed.
|
||||
.slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed.
|
||||
.slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed.
|
||||
.slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed.
|
||||
.slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained.
|
||||
.slice-retained-update-failed = SLICE: a retained result could not be updated.
|
||||
.slice-created-results = SLICE: created __ocs_fmt_0__ result object(s).
|
||||
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: fit a spline through __ocs_fmt_0__ points.
|
||||
.splinefit-need-at-least-3-points = SPLINEFIT: need at least 3 points.
|
||||
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: select a polyline to fit a spline through.
|
||||
@@ -2196,6 +2209,26 @@ draw =
|
||||
.slice-offset-along-x = SLICE offset along X:
|
||||
.slice-offset-along-y = SLICE offset along Y:
|
||||
.slice-offset-along-z = SLICE offset along Z:
|
||||
.slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter:
|
||||
.slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:
|
||||
.slice-first-plane-point = SLICE Specify first point on slicing plane:
|
||||
.slice-second-plane-point = SLICE Specify second point on slicing plane:
|
||||
.slice-third-plane-point = SLICE Specify third point on slicing plane:
|
||||
.slice-point-on-plane = SLICE Specify a point on the slicing plane:
|
||||
.slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis:
|
||||
.slice-point-on-view-plane = SLICE Specify a point on the current view plane:
|
||||
.slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane:
|
||||
.slice-select-planar-object = SLICE Select a planar curve object:
|
||||
.slice-select-planar-surface = SLICE Select a planar surface:
|
||||
.slice-select-cutting-surface = SLICE Select a cutting surface:
|
||||
.slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] <Both>:
|
||||
.slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] <Both>:
|
||||
.slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface:
|
||||
.slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface:
|
||||
.slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object:
|
||||
.slice-planar-object = planar Object
|
||||
.slice-zaxis = Zaxis
|
||||
.slice-three-points = 3points
|
||||
.solid-specify-first-point = SOLID Specify first point:
|
||||
.solid-specify-fourth-point = SOLID Specify fourth point:
|
||||
.solid-specify-second-point = SOLID Specify second point:
|
||||
|
||||
@@ -754,6 +754,19 @@ common =
|
||||
.slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__ konumunda kesildi, __ocs_fmt_2__ yarısı korundu.
|
||||
.slice-select-exactly-one-solid-created-this-se = SLICE: Bu oturumda oluşturulan tam bir katı seçin.
|
||||
.slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Düzlem, korunan tarafta katıyı kesmiyor.
|
||||
.both = Her ikisi
|
||||
.slice-select-at-least-one-target = SLICE: En az bir katı veya yüzey seçin.
|
||||
.slice-invalid-plane = SLICE: Kesme düzlemi geçersiz.
|
||||
.slice-side-point-on-plane = SLICE: Kesme düzleminden uzakta bir nokta seçin.
|
||||
.slice-plane-misses-target = SLICE: Kesme düzlemi seçilen her nesneyi kesmiyor; hiçbir nesne değiştirilmedi.
|
||||
.slice-unsupported-intersection = SLICE: Seçilen geometri desteklenmeyen bir yüzey kesişimi içeriyor; hiçbir nesne değiştirilmedi.
|
||||
.slice-coincident-plane = SLICE: Kesme düzlemi kaynak geometriyle çakışıyor; hiçbir nesne değiştirilmedi.
|
||||
.slice-topology-refused = SLICE: Seçilen topoloji güvenle bölünemedi; hiçbir nesne değiştirilmedi.
|
||||
.slice-result-encode-failed = SLICE: Bir sonuç kodlanamadı; hiçbir nesne değiştirilmedi.
|
||||
.slice-result-display-failed = SLICE: Bir sonuç eksiksiz görüntülenemedi; hiçbir nesne değiştirilmedi.
|
||||
.slice-result-commit-failed = SLICE: Bir sonuç işlenemedi; tüm kaynak nesneler korundu.
|
||||
.slice-retained-update-failed = SLICE: Korunan sonuç güncellenemedi.
|
||||
.slice-created-results = SLICE: __ocs_fmt_0__ sonuç nesnesi oluşturuldu.
|
||||
.splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ noktadan geçen bir spline uyduruldu.
|
||||
.splinefit-need-at-least-3-points = SPLINEFIT: En az 3 nokta gerekir.
|
||||
.splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: İçinden spline geçirilecek bir çoklu çizgi seçin.
|
||||
@@ -2177,6 +2190,26 @@ draw =
|
||||
.slice-offset-along-x = SLICE X boyunca ofset:
|
||||
.slice-offset-along-y = SLICE Y boyunca ofset:
|
||||
.slice-offset-along-z = SLICE Z boyunca ofset:
|
||||
.slice-select-targets = SLICE Dilimlenecek katıları veya yüzeyleri seçin, ardından Enter'a basın:
|
||||
.slice-first-point-or-method = SLICE Kesme düzlemindeki ilk noktayı belirtin veya [düzlemsel Nesne/Yüzey/Zekseni/Görünüm/XY/YZ/ZX/3nokta] <3nokta>:
|
||||
.slice-first-plane-point = SLICE Kesme düzlemindeki ilk noktayı belirtin:
|
||||
.slice-second-plane-point = SLICE Kesme düzlemindeki ikinci noktayı belirtin:
|
||||
.slice-third-plane-point = SLICE Kesme düzlemindeki üçüncü noktayı belirtin:
|
||||
.slice-point-on-plane = SLICE Kesme düzleminde bir nokta belirtin:
|
||||
.slice-point-on-normal-axis = SLICE Düzlemin normal ekseninde bir nokta belirtin:
|
||||
.slice-point-on-view-plane = SLICE Geçerli görünüm düzleminde bir nokta belirtin:
|
||||
.slice-point-on-ucs-plane = SLICE Geçerli UCS düzleminde bir nokta belirtin:
|
||||
.slice-select-planar-object = SLICE Düzlemsel bir eğri nesnesi seçin:
|
||||
.slice-select-planar-surface = SLICE Düzlemsel bir yüzey seçin:
|
||||
.slice-select-cutting-surface = SLICE Bir kesme yüzeyi seçin:
|
||||
.slice-select-side-or-both = SLICE İstenen tarafta bir nokta belirtin veya [Her ikisi] <Her ikisi>:
|
||||
.slice-select-object-or-both = SLICE Korunacak dilimlenmiş nesneyi seçin veya [Her ikisi] <Her ikisi>:
|
||||
.slice-surface-not-planar = SLICE Seçilen yüzey düzlemsel değil. Düzlemsel bir yüzey seçin:
|
||||
.slice-surface-not-supported = SLICE Seçilen yüzey desteklenen tek bir kesme yüzeyi tanımlamıyor. Başka bir yüzey seçin:
|
||||
.slice-object-not-planar = SLICE Seçilen nesne bir düzlem tanımlamıyor. Düzlemsel bir eğri nesnesi seçin:
|
||||
.slice-planar-object = düzlemsel Nesne
|
||||
.slice-zaxis = Zekseni
|
||||
.slice-three-points = 3nokta
|
||||
.solid-specify-first-point = SOLID İlk noktayı belirtin:
|
||||
.solid-specify-fourth-point = SOLID Dördüncü noktayı belirtin:
|
||||
.solid-specify-second-point = SOLID İkinci noktayı belirtin:
|
||||
|
||||
@@ -734,6 +734,8 @@ impl OpenCADStudio {
|
||||
CmdResult::Relaunch(..)
|
||||
| CmdResult::Dispatch(..)
|
||||
| CmdResult::SolidSubtract { .. }
|
||||
| CmdResult::SliceEntities { .. }
|
||||
| CmdResult::SliceSurfaceEntities { .. }
|
||||
);
|
||||
let task = self.apply_cmd_result_inner(result);
|
||||
let i = self.active_tab;
|
||||
@@ -4128,6 +4130,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,
|
||||
|
||||
+56
-15
@@ -1283,21 +1283,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));
|
||||
}
|
||||
@@ -1316,7 +1346,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(),
|
||||
),
|
||||
|
||||
+258
-58
@@ -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>,
|
||||
@@ -1273,77 +1302,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> {
|
||||
|
||||
@@ -1559,6 +1559,20 @@ pub enum CmdResult {
|
||||
cutters: Vec<Handle>,
|
||||
convert_meshes: bool,
|
||||
},
|
||||
/// Split every selected solid or surface with one arbitrary plane.
|
||||
/// `keep_point == None` retains both sides; otherwise it selects the side
|
||||
/// containing that WCS point.
|
||||
SliceEntities {
|
||||
targets: Vec<Handle>,
|
||||
plane: cadkernel::space::Plane,
|
||||
keep_point: Option<DVec3>,
|
||||
},
|
||||
/// Split selected solids or surfaces with one selected analytic sheet.
|
||||
SliceSurfaceEntities {
|
||||
targets: Vec<Handle>,
|
||||
cutter: Box<cadkernel::brep::Body>,
|
||||
keep_point: Option<DVec3>,
|
||||
},
|
||||
/// INSERT landed on a block that has AttributeDefinitions.
|
||||
/// The host should look up the attdefs for `block_name` from the document
|
||||
/// and call `attreq_set_attdefs()` on the command, then loop on text input.
|
||||
|
||||
@@ -2346,11 +2346,44 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"SLICE offset along X:" => Some(("draw", "slice-offset-along-x")),
|
||||
"SLICE offset along Y:" => Some(("draw", "slice-offset-along-y")),
|
||||
"SLICE offset along Z:" => Some(("draw", "slice-offset-along-z")),
|
||||
"SLICE Select solids or surfaces to slice, then press Enter:" => Some(("draw", "slice-select-targets")),
|
||||
"SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:" => Some(("draw", "slice-first-point-or-method")),
|
||||
"SLICE Specify first point on slicing plane:" => Some(("draw", "slice-first-plane-point")),
|
||||
"SLICE Specify second point on slicing plane:" => Some(("draw", "slice-second-plane-point")),
|
||||
"SLICE Specify third point on slicing plane:" => Some(("draw", "slice-third-plane-point")),
|
||||
"SLICE Specify a point on the slicing plane:" => Some(("draw", "slice-point-on-plane")),
|
||||
"SLICE Specify a point on the plane's normal axis:" => Some(("draw", "slice-point-on-normal-axis")),
|
||||
"SLICE Specify a point on the current view plane:" => Some(("draw", "slice-point-on-view-plane")),
|
||||
"SLICE Specify a point on the current UCS plane:" => Some(("draw", "slice-point-on-ucs-plane")),
|
||||
"SLICE Select a planar curve object:" => Some(("draw", "slice-select-planar-object")),
|
||||
"SLICE Select a planar surface:" => Some(("draw", "slice-select-planar-surface")),
|
||||
"SLICE Select a cutting surface:" => Some(("draw", "slice-select-cutting-surface")),
|
||||
"SLICE Specify a point on the desired side or [Both] <Both>:" => Some(("draw", "slice-select-side-or-both")),
|
||||
"SLICE Select a sliced object to keep or [Both] <Both>:" => Some(("draw", "slice-select-object-or-both")),
|
||||
"SLICE The selected surface is not planar. Select a planar surface:" => Some(("draw", "slice-surface-not-planar")),
|
||||
"SLICE The selected surface does not define one supported cutting sheet. Select another surface:" => Some(("draw", "slice-surface-not-supported")),
|
||||
"SLICE The selected object does not define a plane. Select a planar curve object:" => Some(("draw", "slice-object-not-planar")),
|
||||
"planar Object" => Some(("draw", "slice-planar-object")),
|
||||
"Zaxis" => Some(("draw", "slice-zaxis")),
|
||||
"3points" => Some(("draw", "slice-three-points")),
|
||||
"Both" => Some(("common", "both")),
|
||||
"SLICE failed — the plane may not cross the solid." => Some(("common", "slice-failed-the-plane-may-not-cross-the-solid")),
|
||||
"SLICE: could not determine the solid's extent." => Some(("common", "slice-could-not-determine-the-solid-s-extent")),
|
||||
"SLICE: cut at {ax}={value}, kept the {} half." => Some(("common", "slice-cut-at-ax-value-kept-the-half")),
|
||||
"SLICE: select exactly one solid created this session." => Some(("common", "slice-select-exactly-one-solid-created-this-se")),
|
||||
"SLICE: the plane does not cross the solid on the kept side." => Some(("common", "slice-the-plane-does-not-cross-the-solid-on-th")),
|
||||
"SLICE: select at least one solid or surface." => Some(("common", "slice-select-at-least-one-target")),
|
||||
"SLICE: the cutting plane is invalid." => Some(("common", "slice-invalid-plane")),
|
||||
"SLICE: choose a point away from the cutting plane." => Some(("common", "slice-side-point-on-plane")),
|
||||
"SLICE: the cutting plane does not cross every selected object; no object was changed." => Some(("common", "slice-plane-misses-target")),
|
||||
"SLICE: the selected geometry has an unsupported surface intersection; no object was changed." => Some(("common", "slice-unsupported-intersection")),
|
||||
"SLICE: the cutting plane is coincident with source geometry; no object was changed." => Some(("common", "slice-coincident-plane")),
|
||||
"SLICE: the selected topology could not be split safely; no object was changed." => Some(("common", "slice-topology-refused")),
|
||||
"SLICE: a result could not be encoded; no object was changed." => Some(("common", "slice-result-encode-failed")),
|
||||
"SLICE: a result could not be displayed completely; no object was changed." => Some(("common", "slice-result-display-failed")),
|
||||
"SLICE: a result could not be committed; all source objects were retained." => Some(("common", "slice-result-commit-failed")),
|
||||
"SLICE: a retained result could not be updated." => Some(("common", "slice-retained-update-failed")),
|
||||
"SLICE: created %{count} result object(s)." => Some(("common", "slice-created-results")),
|
||||
"SOLID Specify first point:" => Some(("draw", "solid-specify-first-point")),
|
||||
"SOLID Specify fourth point:" => Some(("draw", "solid-specify-fourth-point")),
|
||||
"SOLID Specify second point:" => Some(("draw", "solid-specify-second-point")),
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod cylinder_cmd;
|
||||
pub mod edge_cmd;
|
||||
pub mod polysolid_cmd;
|
||||
pub mod primitive_cmd;
|
||||
pub mod slice_cmd;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::space::Plane;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, SelectionEntity, WorkingPlane};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PlaneKind {
|
||||
Xy,
|
||||
Yz,
|
||||
Zx,
|
||||
View,
|
||||
}
|
||||
|
||||
enum Step {
|
||||
Targets,
|
||||
FirstPoint,
|
||||
ThreeFirst,
|
||||
ThreeSecond(DVec3),
|
||||
ThirdPoint(DVec3, DVec3),
|
||||
AxisFirst,
|
||||
AxisSecond(DVec3),
|
||||
PlanePoint(PlaneKind),
|
||||
PickObject,
|
||||
PickSurface,
|
||||
Side(Plane),
|
||||
SurfaceSide,
|
||||
}
|
||||
|
||||
/// Interactive front-end for plane-based SLICE.
|
||||
pub struct SliceCommand {
|
||||
step: Step,
|
||||
targets: Vec<Handle>,
|
||||
selected: Vec<Handle>,
|
||||
working: WorkingPlane,
|
||||
view_normal: DVec3,
|
||||
preview_center: DVec3,
|
||||
preview_radius: f64,
|
||||
picked: Option<EntityType>,
|
||||
surface_tool: Option<cadkernel::brep::Body>,
|
||||
notice: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl SliceCommand {
|
||||
pub fn new(
|
||||
targets: Vec<Handle>,
|
||||
view_normal: DVec3,
|
||||
preview_center: DVec3,
|
||||
preview_radius: f64,
|
||||
) -> Self {
|
||||
let step = if targets.is_empty() { Step::Targets } else { Step::FirstPoint };
|
||||
Self {
|
||||
step,
|
||||
targets,
|
||||
selected: Vec::new(),
|
||||
working: WorkingPlane::default(),
|
||||
view_normal: view_normal.normalize_or(DVec3::Z),
|
||||
preview_center,
|
||||
preview_radius: preview_radius.max(1.0),
|
||||
picked: None,
|
||||
surface_tool: None,
|
||||
notice: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn plane(origin: DVec3, x: DVec3, normal: DVec3) -> Option<Plane> {
|
||||
Plane::orthonormal(origin.to_array(), x.to_array(), normal.to_array())
|
||||
}
|
||||
|
||||
fn enter_side(&mut self, plane: Plane) -> CmdResult {
|
||||
self.notice = None;
|
||||
self.step = Step::Side(plane);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn finish(&mut self, keep_point: Option<DVec3>) -> CmdResult {
|
||||
let Step::Side(plane) = self.step else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
CmdResult::SliceEntities {
|
||||
targets: std::mem::take(&mut self.targets),
|
||||
plane,
|
||||
keep_point,
|
||||
}
|
||||
}
|
||||
|
||||
fn picked_plane(&self) -> Option<Plane> {
|
||||
let entity = self.picked.as_ref()?;
|
||||
if !matches!(
|
||||
entity,
|
||||
EntityType::Arc(_)
|
||||
| EntityType::Circle(_)
|
||||
| EntityType::Ellipse(_)
|
||||
| EntityType::Spline(_)
|
||||
| EntityType::LwPolyline(_)
|
||||
| EntityType::Polyline(_)
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
crate::scene::model::presspull_model::profile_geometry(entity)
|
||||
.map(|(plane, _, _)| plane)
|
||||
}
|
||||
|
||||
fn finish_surface(&mut self, keep_point: Option<DVec3>) -> CmdResult {
|
||||
let Some(cutter) = self.surface_tool.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
CmdResult::SliceSurfaceEntities {
|
||||
targets: std::mem::take(&mut self.targets),
|
||||
cutter: Box::new(cutter),
|
||||
keep_point,
|
||||
}
|
||||
}
|
||||
|
||||
fn plane_at(&self, point: DVec3, kind: PlaneKind) -> Option<Plane> {
|
||||
let (x, normal) = match kind {
|
||||
PlaneKind::Xy => (self.working.x, self.working.z),
|
||||
PlaneKind::Yz => (self.working.y, self.working.x),
|
||||
PlaneKind::Zx => (self.working.z, self.working.y),
|
||||
PlaneKind::View => {
|
||||
let candidate = if self.working.x.dot(self.view_normal).abs() < 0.98 {
|
||||
self.working.x
|
||||
} else {
|
||||
self.working.y
|
||||
};
|
||||
(candidate, self.view_normal)
|
||||
}
|
||||
};
|
||||
Self::plane(point, x, normal)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for SliceCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"SLICE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if let Some(notice) = self.notice {
|
||||
return crate::t!(notice).into_owned();
|
||||
}
|
||||
match self.step {
|
||||
Step::Targets => crate::t!(
|
||||
"SLICE Select solids or surfaces to slice, then press Enter:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::FirstPoint => crate::t!(
|
||||
"SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::ThreeFirst => {
|
||||
crate::t!("SLICE Specify first point on slicing plane:").into_owned()
|
||||
}
|
||||
Step::ThreeSecond(_) => {
|
||||
crate::t!("SLICE Specify second point on slicing plane:").into_owned()
|
||||
}
|
||||
Step::ThirdPoint(_, _) => {
|
||||
crate::t!("SLICE Specify third point on slicing plane:").into_owned()
|
||||
}
|
||||
Step::AxisFirst => {
|
||||
crate::t!("SLICE Specify a point on the slicing plane:").into_owned()
|
||||
}
|
||||
Step::AxisSecond(_) => {
|
||||
crate::t!("SLICE Specify a point on the plane's normal axis:").into_owned()
|
||||
}
|
||||
Step::PlanePoint(PlaneKind::View) => {
|
||||
crate::t!("SLICE Specify a point on the current view plane:").into_owned()
|
||||
}
|
||||
Step::PlanePoint(_) => {
|
||||
crate::t!("SLICE Specify a point on the current UCS plane:").into_owned()
|
||||
}
|
||||
Step::PickObject => crate::t!("SLICE Select a planar curve object:").into_owned(),
|
||||
Step::PickSurface => crate::t!("SLICE Select a cutting surface:").into_owned(),
|
||||
Step::Side(_) => crate::t!(
|
||||
"SLICE Specify a point on the desired side or [Both] <Both>:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::SurfaceSide => crate::t!(
|
||||
"SLICE Select a sliced object to keep or [Both] <Both>:"
|
||||
)
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::FirstPoint => vec![
|
||||
CmdOption::new(crate::t!("planar Object").as_ref(), "O"),
|
||||
CmdOption::new(crate::t!("Surface").as_ref(), "S"),
|
||||
CmdOption::new(crate::t!("Zaxis").as_ref(), "Z"),
|
||||
CmdOption::new(crate::t!("View").as_ref(), "V"),
|
||||
CmdOption::new("XY", "XY"),
|
||||
CmdOption::new("YZ", "YZ"),
|
||||
CmdOption::new("ZX", "ZX"),
|
||||
CmdOption::new(crate::t!("3points").as_ref(), "3"),
|
||||
],
|
||||
Step::Side(_) | Step::SurfaceSide => {
|
||||
vec![CmdOption::new(crate::t!("Both").as_ref(), "B")]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.working = plane;
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
self.notice = None;
|
||||
match self.step {
|
||||
Step::FirstPoint => {
|
||||
self.step = Step::ThreeSecond(point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::ThreeFirst => {
|
||||
self.step = Step::ThreeSecond(point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::ThreeSecond(first) => {
|
||||
self.step = Step::ThirdPoint(first, point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::ThirdPoint(first, second) => {
|
||||
let x = second - first;
|
||||
let normal = x.cross(point - first);
|
||||
Self::plane(first, x, normal)
|
||||
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane))
|
||||
}
|
||||
Step::AxisFirst => {
|
||||
self.step = Step::AxisSecond(point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::AxisSecond(origin) => {
|
||||
let normal = point - origin;
|
||||
let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 {
|
||||
self.working.x
|
||||
} else {
|
||||
self.working.y
|
||||
};
|
||||
Self::plane(origin, x, normal)
|
||||
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane))
|
||||
}
|
||||
Step::PlanePoint(kind) => self
|
||||
.plane_at(point, kind)
|
||||
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane)),
|
||||
Step::Side(_) => self.finish(Some(point)),
|
||||
Step::SurfaceSide => self.finish_surface(Some(point)),
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Targets if !self.selected.is_empty() => {
|
||||
self.targets = std::mem::take(&mut self.selected);
|
||||
self.step = Step::FirstPoint;
|
||||
CmdResult::DeselectAndContinue
|
||||
}
|
||||
Step::FirstPoint => {
|
||||
self.step = Step::ThreeFirst;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Side(_) => self.finish(None),
|
||||
Step::SurfaceSide => self.finish_surface(None),
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim().to_ascii_uppercase();
|
||||
Some(match self.step {
|
||||
Step::FirstPoint => match keyword.as_str() {
|
||||
"O" | "OBJECT" => {
|
||||
self.step = Step::PickObject;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"S" | "SURFACE" => {
|
||||
self.step = Step::PickSurface;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"Z" | "ZAXIS" => {
|
||||
self.step = Step::AxisFirst;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"V" | "VIEW" => {
|
||||
self.step = Step::PlanePoint(PlaneKind::View);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"XY" => {
|
||||
self.step = Step::PlanePoint(PlaneKind::Xy);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"YZ" => {
|
||||
self.step = Step::PlanePoint(PlaneKind::Yz);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"ZX" | "XZ" => {
|
||||
self.step = Step::PlanePoint(PlaneKind::Zx);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
"3" | "3P" | "3POINTS" => {
|
||||
self.step = Step::ThreeFirst;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
},
|
||||
Step::Side(_) if matches!(keyword.as_str(), "" | "B" | "BOTH") => self.finish(None),
|
||||
Step::SurfaceSide if matches!(keyword.as_str(), "" | "B" | "BOTH") => {
|
||||
self.finish_surface(None)
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
})
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::FirstPoint | Step::Side(_) | Step::SurfaceSide)
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(self.step, Step::Targets)
|
||||
}
|
||||
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||
self.selected = entities
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
matches!(item.entity, EntityType::Solid3D(_) | EntityType::Surface(_))
|
||||
.then_some(item.handle)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
Step::PickObject | Step::PickSurface | Step::SurfaceSide
|
||||
)
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
self.needs_entity_pick()
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickObject | Step::PickSurface)
|
||||
}
|
||||
|
||||
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||
matches!(self.step, Step::SurfaceSide)
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, _handle: Handle, _point: DVec3) -> CmdResult {
|
||||
if matches!(self.step, Step::SurfaceSide) {
|
||||
return self.finish_surface(Some(_point));
|
||||
}
|
||||
let surface_only = matches!(self.step, Step::PickSurface);
|
||||
if surface_only {
|
||||
let body = self.picked.as_ref().and_then(|entity| match entity {
|
||||
EntityType::Surface(surface) => {
|
||||
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some(body) = body {
|
||||
if let Some(plane) = planar_body_plane(&body) {
|
||||
return self.enter_side(plane);
|
||||
}
|
||||
if body.face_keys().count() == 1 {
|
||||
self.surface_tool = Some(body);
|
||||
self.notice = None;
|
||||
self.step = Step::SurfaceSide;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
}
|
||||
self.notice = Some(
|
||||
"SLICE The selected surface does not define one supported cutting sheet. Select another surface:",
|
||||
);
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match self.picked_plane() {
|
||||
Some(plane) => self.enter_side(plane),
|
||||
None => {
|
||||
self.notice = Some(
|
||||
"SLICE The selected object does not define a plane. Select a planar curve object:",
|
||||
);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
|
||||
let mut wires = Vec::new();
|
||||
let plane = match self.step {
|
||||
Step::ThirdPoint(first, second) => {
|
||||
wires.push(WireModel::solid_f64(
|
||||
"slice_triangle".into(),
|
||||
vec![
|
||||
first.to_array(),
|
||||
second.to_array(),
|
||||
point.to_array(),
|
||||
first.to_array(),
|
||||
],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
Self::plane(first, second - first, (second - first).cross(point - first))
|
||||
}
|
||||
Step::AxisSecond(origin) => {
|
||||
wires.push(WireModel::solid_f64(
|
||||
"slice_normal".into(),
|
||||
vec![origin.to_array(), point.to_array()],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
let normal = point - origin;
|
||||
let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 {
|
||||
self.working.x
|
||||
} else {
|
||||
self.working.y
|
||||
};
|
||||
Self::plane(origin, x, normal)
|
||||
}
|
||||
Step::PlanePoint(kind) => self.plane_at(point, kind),
|
||||
Step::Side(plane) => Some(plane),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(plane) = plane {
|
||||
wires.extend(plane_grid(
|
||||
plane,
|
||||
self.preview_center,
|
||||
self.preview_radius,
|
||||
));
|
||||
}
|
||||
wires
|
||||
}
|
||||
}
|
||||
|
||||
fn planar_body_plane(body: &cadkernel::brep::Body) -> Option<Plane> {
|
||||
let mut faces = body.face_keys();
|
||||
let first = cadkernel::brep::planar_face_profile(body, faces.next()?)?.plane;
|
||||
let normal = DVec3::from_array(first.normal()?);
|
||||
let tolerance = cadkernel::brep::operation_tolerance(&[body]);
|
||||
faces
|
||||
.all(|face| {
|
||||
let Some(profile) = cadkernel::brep::planar_face_profile(body, face) else {
|
||||
return false;
|
||||
};
|
||||
profile.plane.normal().is_some_and(|other| {
|
||||
normal.dot(DVec3::from_array(other)).abs() >= 1.0 - 1e-9
|
||||
&& first
|
||||
.distance_to(profile.plane.origin)
|
||||
.is_some_and(|gap| gap.abs() <= tolerance * 4.0)
|
||||
})
|
||||
})
|
||||
.then_some(first)
|
||||
}
|
||||
|
||||
fn plane_grid(plane: Plane, centre: DVec3, radius: f64) -> Vec<WireModel> {
|
||||
let normal = DVec3::from_array(plane.normal().unwrap_or([0.0, 0.0, 1.0]));
|
||||
let projected = centre
|
||||
- normal
|
||||
* plane
|
||||
.distance_to(centre.to_array())
|
||||
.unwrap_or_default();
|
||||
let x = DVec3::from_array(plane.x_axis);
|
||||
let y = DVec3::from_array(plane.y_axis);
|
||||
let color = [0.2, 0.55, 1.0, 1.0];
|
||||
(-5..=5)
|
||||
.flat_map(|index| {
|
||||
let offset = radius * index as f64 / 5.0;
|
||||
[
|
||||
WireModel::solid_f64(
|
||||
format!("slice_grid_x_{index}"),
|
||||
vec![
|
||||
(projected + x * offset - y * radius).to_array(),
|
||||
(projected + x * offset + y * radius).to_array(),
|
||||
],
|
||||
color,
|
||||
false,
|
||||
),
|
||||
WireModel::solid_f64(
|
||||
format!("slice_grid_y_{index}"),
|
||||
vec![
|
||||
(projected + y * offset - x * radius).to_array(),
|
||||
(projected + y * offset + x * radius).to_array(),
|
||||
],
|
||||
color,
|
||||
false,
|
||||
),
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["SLICE", "SL"]
|
||||
});
|
||||
Reference in new issue
Block a user