feat(model): SECTIONCULL erases what lies outside a section

SECTIONCULL keeps only what the selected section object (or the live
section of the space) shows: solids, surfaces and closed meshes are cut
to it with the same planes the live section uses, open meshes are
clipped face by face, and every other object wholly outside it is
erased. Objects across the boundary that cannot be cut stay whole. One
undo step.

The section's bounding planes are gathered once (section_planes) and
shared by the live section and the cull.

Refs #1407

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 21:59:25 +03:00
1 parent adbcac8a02
commit d4b4fc37e1
28 files changed
+450 -77

No files matched your search

+5
View File
@@ -2148,6 +2148,11 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(subtract));
}
// SECTIONCULL — erase what lies outside a section object.
"SECTIONCULL" => {
return Some(self.section_cull());
}
// INTERFERE — non-destructive intersect: solid from the overlap.
"INTERFERE" => {
return Some(self.solid_interfere());
+1
View File
@@ -621,6 +621,7 @@ inventory::submit!(crate::command::CommandRegistration {
"SL",
"INTERFERE",
"INF",
"SECTIONCULL",
"PRESSPULL",
"THICKEN",
"3DROTATE",
+70
View File
@@ -225,6 +225,8 @@ fn subtract_bodies(
Ok(result)
}
inventory::submit!(crate::command::CommandRegistration { names: &["SECTIONCULL"] });
fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option<EntityType> {
let document = crate::scene::convert::acis_export::solid_to_sat(body)?;
let wires = solid_model::edge_wires(body);
@@ -2073,6 +2075,74 @@ impl super::OpenCADStudio {
self.solid_slice(&handles, plane, keep_point)
}
/// SECTIONCULL — keep only what lies inside the selected section object
/// (or the live section): bodies are cut to it, open meshes clipped, and
/// everything wholly outside it erased. One undo step.
pub(super) fn section_cull(&mut self) -> Task<Message> {
let i = self.active_tab;
let Some(section) = self.tabs[i].scene.cull_section() else {
self.command_line.push_error(
crate::t!("SECTIONCULL: select a section object or turn on a live section.")
.as_ref(),
);
return Task::none();
};
let Some(cull) = self.tabs[i].scene.section_cull(section) else {
return Task::none();
};
let mut cut = Vec::with_capacity(cull.cut.len());
for (handle, body) in cull.cut {
let scene = &self.tabs[i].scene;
let entity = scene
.document
.get_entity(handle)
.cloned()
.and_then(|source| entity_with_boolean_body(source, &body));
let display = scene
.prepare_solid_model_display(handle, &body)
.filter(|display| display.0.complete);
let (Some(entity), Some(display)) = (entity, display) else {
self.command_line.push_error(
crate::t!(
"SECTIONCULL: a cut object could not be prepared; nothing was changed."
)
.as_ref(),
);
return Task::none();
};
cut.push((handle, entity, body, display));
}
let changed = cut.len() + cull.clipped.len() + cull.outside.len();
if changed == 0 {
self.command_line
.push_output(crate::t!("SECTIONCULL: nothing lies outside the section.").as_ref());
return Task::none();
}
self.push_undo_snapshot(i, "SECTIONCULL");
let scene = &mut self.tabs[i].scene;
for (handle, entity, body, display) in cut {
scene.delete_solid_history(handle);
scene.update_entity(entity);
scene.register_prepared_solid_model(handle, body, display);
}
for mesh in cull.clipped {
scene.update_entity(mesh);
}
scene.erase_entities(&cull.outside);
scene.deselect_all();
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::t!(
"SECTIONCULL: erased %{erased} object(s) and cut %{cut} across the section.",
erased = cull.outside.len(),
cut = changed - cull.outside.len()
)
.as_ref(),
);
Task::none()
}
/// 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> {
+4
View File
@@ -2885,6 +2885,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"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")),
"SECTIONCULL: a cut object could not be prepared; nothing was changed." => Some(("common", "sectioncull-prepare-failed")),
"SECTIONCULL: erased %{erased} object(s) and cut %{cut} across the section." => Some(("common", "sectioncull-result")),
"SECTIONCULL: nothing lies outside the section." => Some(("common", "sectioncull-nothing-outside")),
"SECTIONCULL: select a section object or turn on a live section." => Some(("common", "sectioncull-no-section")),
"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")),
+53 -77
View File
@@ -8,6 +8,7 @@
pub mod annotative;
pub mod assoc_array;
pub mod compare;
pub mod section_cull;
pub mod block_service;
pub mod cache;
pub mod convert;
@@ -8899,16 +8900,29 @@ impl Scene {
slice_depth: Option<f64>,
body_to_world: Option<&codec::types::Transform>,
) -> Result<Option<kernel::brep::Body>, ()> {
let Some((origin, tangent, vertical, viewing)) = section_frame(data) else {
return Err(());
};
let front = section_plane(origin, tangent, viewing, body_to_world).ok_or(())?;
let mut clipped = keep_section_positive(body, front)?;
let Some(mut current) = clipped.take() else {
return Ok(None);
};
let planes = Self::section_planes(data, slice_depth, body_to_world).ok_or(())?;
let mut current = body.clone();
for plane in planes {
match keep_section_positive(&current, plane)? {
Some(next) => current = next,
None => return Ok(None),
}
}
Ok(Some(current))
}
/// The planes bounding what section `data` keeps, each facing inward:
/// the section plane, then the back of a slice, the ends of a boundary
/// and the top and bottom of a volume.
fn section_planes(
data: &codec::entities::SectionObjectData,
slice_depth: Option<f64>,
body_to_world: Option<&codec::types::Transform>,
) -> Option<Vec<kernel::space::Plane>> {
let (origin, tangent, vertical, viewing) = section_frame(data)?;
let mut planes = vec![section_plane(origin, tangent, viewing, body_to_world)?];
if data.state == 1 && slice_depth.is_none() {
return Ok(Some(current));
return Some(planes);
}
let depth = slice_depth.unwrap_or_else(|| {
@@ -8923,82 +8937,44 @@ impl Scene {
})
});
if depth <= 1e-12 {
return Ok(Some(current));
return Some(planes);
}
let back =
section_plane(origin + viewing * depth, tangent, -viewing, body_to_world).ok_or(())?;
let Some(next) = keep_section_positive(&current, back)? else {
return Ok(None);
};
current = next;
planes.push(section_plane(
origin + viewing * depth,
tangent,
-viewing,
body_to_world,
)?);
if data.state == 1 {
return Ok(Some(current));
return Some(planes);
}
let span = data
.vertices
.first()
.zip(data.vertices.last())
.map_or(0.0, |(first, last)| {
(glam::DVec3::new(last.x, last.y, last.z)
- glam::DVec3::new(first.x, first.y, first.z))
.length()
});
if span <= 1e-12 {
return Ok(Some(current));
let first = data.vertices.first()?;
let last = data.vertices.last()?;
let first = glam::DVec3::new(first.x, first.y, first.z);
let last = glam::DVec3::new(last.x, last.y, last.z);
if (last - first).length() <= 1e-12 {
return Some(planes);
}
let first = data
.vertices
.first()
.map(|point| glam::DVec3::new(point.x, point.y, point.z))
.ok_or(())?;
let last = data
.vertices
.last()
.map(|point| glam::DVec3::new(point.x, point.y, point.z))
.ok_or(())?;
for plane in [
section_plane(first, vertical, tangent, body_to_world),
section_plane(last, vertical, -tangent, body_to_world),
] {
let Some(plane) = plane else {
return Err(());
};
let Some(next) = keep_section_positive(&current, plane)? else {
return Ok(None);
};
current = next;
}
planes.push(section_plane(first, vertical, tangent, body_to_world)?);
planes.push(section_plane(last, vertical, -tangent, body_to_world)?);
if data.state != 4 {
return Ok(Some(current));
return Some(planes);
}
for plane in [
section_plane(
origin - vertical * data.bottom_height,
tangent,
vertical,
body_to_world,
),
section_plane(
origin + vertical * data.top_height,
tangent,
-vertical,
body_to_world,
),
] {
let Some(plane) = plane else {
return Err(());
};
let Some(next) = keep_section_positive(&current, plane)? else {
return Ok(None);
};
current = next;
}
Ok(Some(current))
planes.push(section_plane(
origin - vertical * data.bottom_height,
tangent,
vertical,
body_to_world,
)?);
planes.push(section_plane(
origin + vertical * data.top_height,
tangent,
-vertical,
body_to_world,
)?);
Some(planes)
}
/// True when `layer` is turned off or frozen — entities on it never render.
+229
View File
@@ -0,0 +1,229 @@
//! SECTIONCULL: keep only what lies inside a section object — the part a
//! live section shows — and remove the rest for good. Solids, surfaces and
//! closed meshes are cut to it, open meshes are clipped face by face, and
//! every other object wholly outside it is erased. Objects across its
//! boundary that cannot be cut stay whole.
use codec::{EntityType, Handle};
use glam::DVec3;
use rustc_hash::FxHashMap;
use super::Scene;
/// What culling to a section does to the space it lives in.
#[derive(Default)]
pub struct SectionCull {
/// Bodies the section crosses, with what stays of each.
pub cut: Vec<(Handle, kernel::brep::Body)>,
/// Open meshes the section crosses, clipped to it.
pub clipped: Vec<EntityType>,
/// Objects wholly outside the section.
pub outside: Vec<Handle>,
}
enum Place {
Inside,
Outside,
Across,
}
const TOLERANCE: f64 = 1e-9;
fn place(planes: &[kernel::space::Plane], points: &[[f64; 3]]) -> Place {
let mut inside = true;
for plane in planes {
let behind = points
.iter()
.filter(|point| plane.distance_to(**point).unwrap_or(0.0) < -TOLERANCE)
.count();
if behind == points.len() {
return Place::Outside;
}
inside &= behind == 0;
}
if inside {
Place::Inside
} else {
Place::Across
}
}
fn same_extent(a: &kernel::brep::Body, b: &kernel::brep::Body) -> bool {
match (kernel::brep::body_bounds(a), kernel::brep::body_bounds(b)) {
(Some(a), Some(b)) => (0..3).all(|axis| {
(a.min[axis] - b.min[axis]).abs() <= 1e-7 && (a.max[axis] - b.max[axis]).abs() <= 1e-7
}),
_ => false,
}
}
impl Scene {
/// The section SECTIONCULL works with: the selected section object, or
/// else the live section of the current space.
pub fn cull_section(&self) -> Option<Handle> {
let is_section = |handle: &Handle| {
matches!(
self.document.get_entity(*handle),
Some(EntityType::Extended(extended))
if matches!(
extended.data,
codec::entities::ExtendedEntityData::SectionObject(_)
)
)
};
if let Some(handle) = self.selected_handles_in_order().into_iter().find(is_section) {
return Some(handle);
}
let block = self.current_layout_block_handle();
self.document
.entities()
.filter(|entity| match entity {
EntityType::Extended(extended) => match &extended.data {
codec::entities::ExtendedEntityData::SectionObject(data) => {
let owner = extended.common.owner_handle;
data.flags & 1 != 0
&& !extended.common.invisible
&& (owner.is_null() || owner == block)
}
_ => false,
},
_ => false,
})
.map(|entity| entity.common().handle)
.last()
}
/// Plan culling the current space to section object `section`; `None`
/// when it is not a section object.
pub fn section_cull(&self, section: Handle) -> Option<SectionCull> {
let Some(EntityType::Extended(extended)) = self.document.get_entity(section) else {
return None;
};
let codec::entities::ExtendedEntityData::SectionObject(data) = &extended.data else {
return None;
};
let slice_depth = crate::entities::extended::section_is_slice(extended).then(|| {
crate::entities::extended::section_slice_depth(extended).unwrap_or(0.0)
});
let planes = Self::section_planes(data, slice_depth, None)?;
// Every drawn point of every object in the space, from its wires and
// its shaded mesh.
let block = self.current_layout_block_handle();
let mut points: FxHashMap<Handle, Vec<[f64; 3]>> = FxHashMap::default();
let wires = self.wires_for_block_culled(
block,
None,
None,
None,
None,
None,
true,
None,
);
for wire in &wires {
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
continue;
};
let entry = points.entry(handle).or_default();
for (index, &[x, y, z]) in wire.points.iter().enumerate() {
let [lx, ly, lz] = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
let point = DVec3::new(
f64::from(x) + f64::from(lx),
f64::from(y) + f64::from(ly),
f64::from(z) + f64::from(lz),
);
if point.is_finite() {
entry.push(point.to_array());
}
}
}
for set in self.interaction_meshes_arc().iter() {
let handle = set.entity_handle();
let (Some(handle), Some(aabb)) = (handle, super::mesh_interaction_aabb(set)) else {
continue;
};
let entry = points.entry(handle).or_default();
for corner in 0..8 {
entry.push([
aabb[if corner & 1 == 0 { 0 } else { 3 }],
aabb[if corner & 2 == 0 { 1 } else { 4 }],
aabb[if corner & 4 == 0 { 2 } else { 5 }],
]);
}
}
let mut cull = SectionCull::default();
// A live section hides what lies wholly outside it, so the space's
// own objects are walked too.
let mut handles: Vec<Handle> = points
.keys()
.copied()
.chain(
self.document
.entities()
.filter(|entity| entity.common().owner_handle == block)
.map(|entity| entity.common().handle),
)
.collect();
handles.sort_unstable_by_key(|handle| handle.value());
handles.dedup();
for handle in handles {
if handle == section || self.is_layer_locked(handle) {
continue;
}
let Some(entity) = self.document.get_entity(handle) else {
continue;
};
// Bodies are cut with the kernel: their drawn wires may already
// show the live cut, so they say nothing about the source.
if let Some(body) = self.section_source_body(handle) {
match Self::section_body(&body, data, slice_depth, None) {
Ok(None) => cull.outside.push(handle),
Ok(Some(kept)) if !same_extent(&body, &kept) => cull.cut.push((handle, kept)),
_ => {}
}
continue;
}
if let Some((vertices, _)) = crate::entities::mesh::mesh_polygons(entity) {
match place(&planes, &vertices) {
Place::Outside => cull.outside.push(handle),
Place::Inside => {}
Place::Across => {
let mut mesh = entity.clone();
for plane in &planes {
let Some((vertices, faces)) =
crate::entities::mesh::mesh_polygons(&mesh)
else {
break;
};
let side = |point: [f64; 3]| plane.distance_to(point).unwrap_or(0.0);
if vertices.iter().all(|point| side(*point) >= -TOLERANCE) {
continue;
}
let Some(mut clipped) = crate::entities::mesh::clipped_mesh(
&vertices,
&faces,
side,
true,
mesh.common(),
) else {
break;
};
clipped.common.handle = handle;
mesh = EntityType::Mesh(clipped);
}
cull.clipped.push(mesh);
}
}
continue;
}
if let Some(points) = points.get(&handle) {
if matches!(place(&planes, points), Place::Outside) {
cull.outside.push(handle);
}
}
}
Some(cull)
}
}