feat(solid history): booleans keep the solids they joined editable

With SOLIDHIST on, UNION, SUBTRACT and INTERSECT no longer reduce the
result's history to a frozen body: each solid's history joins the
result's under a boolean step, in the order the result applied them.
The Properties palette lists a composite's solids under Operand; the one
chosen shows its own geometry rows and grips, placed where it sits in
the composite, and editing it replays the whole tree. A composite read
from a file keeps its saved body; the tree is replayed only on edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-07 11:14:13 +03:00
1 parent d1f3bf3013
commit d3033caf24
28 files changed
+383 -52

No files matched your search

+79 -7
View File
@@ -4,7 +4,7 @@ use codec::{
entities::{
AcisData, EntityCommon, Region, Solid3D, Surface, SurfaceData, SurfaceKind, Wire,
},
objects::SolidHistoryOperation,
objects::{SolidHistoryBoolean, SolidHistoryOperation},
EntityType, Handle,
};
use kernel::brep::{Body, EdgeKey, FaceKey};
@@ -1006,6 +1006,32 @@ impl super::OpenCADStudio {
}
/// Intersect compatible selected solids, Regions, and coplanar Surfaces.
/// Before a boolean replaces `retained` with `entity`: with SOLIDHIST on,
/// join the histories of the solids it combined (`steps`, in the order
/// the result applied them) into `retained`'s and hand that history to
/// `entity`; otherwise drop it. True when the history was kept.
fn join_boolean_history(
&mut self,
retained: Handle,
entity: &mut EntityType,
steps: &[(Handle, u8)],
bodies: &HashMap<Handle, Body>,
) -> bool {
let scene = &mut self.tabs[self.active_tab].scene;
let joined = scene.document.header.record_solid_history
&& scene.join_boolean_histories(retained, retained, steps, bodies);
if !joined {
scene.delete_solid_history(retained);
return false;
}
if let (EntityType::Solid3D(entity), Some(EntityType::Solid3D(current))) =
(entity, scene.document.get_entity(retained))
{
entity.history_handle = current.history_handle;
}
true
}
pub(super) fn solid_intersect(&mut self) -> Task<Message> {
let i = self.active_tab;
let mut handles = self.selected_intersect_handles();
@@ -1139,11 +1165,17 @@ impl super::OpenCADStudio {
match group.outcome {
PreparedIntersectOutcome::Replace {
retained,
entity,
mut entity,
body,
display,
} => {
self.tabs[i].scene.delete_solid_history(retained);
let steps = group
.handles
.iter()
.filter(|handle| **handle != retained)
.map(|handle| (*handle, SolidHistoryBoolean::INTERSECT))
.collect::<Vec<_>>();
let joined = self.join_boolean_history(retained, &mut entity, &steps, &bodies);
if !self.tabs[i].scene.update_entity(entity) {
self.command_line.push_error(
crate::t!("INTERSECT: the retained object could not be updated.")
@@ -1152,6 +1184,7 @@ impl super::OpenCADStudio {
return Task::none();
}
if record_history
&& !joined
&& matches!(
self.tabs[i].scene.document.get_entity(retained),
Some(EntityType::Solid3D(_))
@@ -1328,11 +1361,37 @@ impl super::OpenCADStudio {
let history = solid_history::brep_op(&group.body);
let mut entity = Solid3D::new();
entity.common = group.common;
self.add_solid_model_preserving_style(
let handle = self.add_solid_model_preserving_style(
EntityType::Solid3D(entity),
group.body,
history,
)
);
// The new solid's history becomes the joined histories
// of the solids it unites.
let record = self.tabs[i].scene.document.header.record_solid_history;
if !handle.is_null() && record {
let bodies = group
.handles
.iter()
.filter_map(|operand| {
let body = self.tabs[i].scene.solid_models.get(operand)?;
Some((*operand, body.clone()))
})
.collect::<HashMap<_, _>>();
let steps = group
.handles
.iter()
.skip(1)
.map(|operand| (*operand, SolidHistoryBoolean::UNION))
.collect::<Vec<_>>();
let scene = &mut self.tabs[i].scene;
if !scene.join_boolean_histories(handle, group.handles[0], &steps, &bodies) {
if let Some(body) = scene.solid_models.get(&handle).cloned() {
scene.create_solid_history(handle, solid_history::brep_op(&body));
}
}
}
handle
}
UnionEntityKind::Region => {
let mut entity = Region::new();
@@ -1588,8 +1647,20 @@ impl super::OpenCADStudio {
let mut retained = Vec::new();
let mut consumed = Vec::new();
for group in prepared {
if let Some((handle, entity, body, display)) = group.result {
self.tabs[i].scene.delete_solid_history(handle);
if let Some((handle, mut entity, body, display)) = group.result {
let steps = group
.bases
.iter()
.skip(1)
.map(|base| (*base, SolidHistoryBoolean::UNION))
.chain(
group
.cutters
.iter()
.map(|cutter| (*cutter, SolidHistoryBoolean::SUBTRACT)),
)
.collect::<Vec<_>>();
let joined = self.join_boolean_history(handle, &mut entity, &steps, &operand_bodies);
if !self.tabs[i].scene.update_entity(entity) {
self.command_line.push_error(
crate::t!("SUBTRACT: the retained base object could not be updated.").as_ref(),
@@ -1597,6 +1668,7 @@ impl super::OpenCADStudio {
return Task::none();
}
if record_history
&& !joined
&& matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(EntityType::Solid3D(_))
+13
View File
@@ -2732,6 +2732,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.tabs[i].properties.edit_choice_open = false;
return Task::none();
}
if field == crate::scene::model::solid_history::PROP_OPERAND {
// Which solid of a composite to edit: palette state only.
for handle in &handles {
crate::scene::model::solid_history::select_operand(
&self.tabs[i].scene.document,
*handle,
&value,
);
}
self.tabs[i].properties.edit_choice_open = false;
self.refresh_properties();
return Task::none();
}
if crate::scene::model::solid_history::is_history_choice(field) {
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if app.tabs[i].scene.is_layer_locked(handle) {