Merge pull request #1679 from ramox81/codex/surface-associativity-sources

Associative extruded, revolved and lofted surfaces; spline and ellipse sources

# Conflicts:
#	src/app/command_driver/solid3d.rs
#	src/scene/dwg_native_constraints.rs
#	src/scene/model/mod.rs
#	src/scene/model/sweep_command_model.rs
#	src/scene/model/sweep_model.rs
#	src/scene/modify.rs
This commit is contained in:
Hakan Seven committed 2026-10-12 00:14:34 +03:00
commit 5bb0efd93e
12 files changed
+1106 -228

No files matched your search

+179 -36
View File
@@ -1268,6 +1268,98 @@ impl Scene {
rebuilt
}
/// Associative extruded, revolved and lofted surfaces follow edits of
/// their sources: made again from them with the values they were made
/// with. A surface edited along with its sources keeps its own edit.
/// Returns the surfaces rebuilt.
pub(crate) fn refresh_associative_surfaces(&mut self, changes: &[(Handle, crate::scene::ChangeKind)]) -> Vec<(Handle, crate::scene::ChangeKind)> {
use crate::scene::model::surface_sources::{axis_points, surface_sources, SurfaceSources, SURFACE_SOURCES_APP};
use crate::scene::model::sweep_model;
use codec::objects::SolidHistoryOperation;
let changed = changes.iter()
.filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed))
.map(|(handle, _)| *handle)
.collect::<rustc_hash::FxHashSet<_>>();
if changed.is_empty() || !self.document.app_ids.contains(SURFACE_SOURCES_APP) {
return Vec::new();
}
let linked = self.document.entities()
.filter(|entity| matches!(entity, EntityType::Surface(_)))
.filter_map(|entity| {
let handle = entity.common().handle;
let sources = surface_sources(entity)?;
(!changed.contains(&handle) && sources.handles().iter().any(|source| changed.contains(source)))
.then_some((handle, sources))
})
.collect::<Vec<_>>();
let mut rebuilt = Vec::new();
for (handle, sources) in linked {
let source = |handle: Handle| self.document.get_entity(handle).cloned();
// The surface made again: a history step, or a body with its data.
let made: Option<Result<SolidHistoryOperation, (kernel::brep::Body, codec::entities::SurfaceData)>> = match &sources {
SurfaceSources::Extrude { profile, height, direction, taper } => (|| {
let entity = source(*profile)?;
let (shape, _) = sweep_model::extrusion_profile_of(&entity)?;
let direction = match direction {
Some(direction) => *direction,
None => glam::DVec3::from_array(shape.plane.normal()?) * *height,
};
if self.document.solid_history_operations(handle).is_some() {
return sweep_model::extrusion_history(&entity, None, direction.to_array(), *taper, shape.plane.origin).map(Ok);
}
let body = sweep_model::extruded_surface(&entity, direction.to_array(), *taper)?;
let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_extruded_surface(direction, *taper) else { return None };
Some(Err((body, fresh.surface_data)))
})(),
SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => (|| {
let entity = source(*profile)?;
let (start, end) = axis
.and_then(|axis| self.document.get_entity(axis).and_then(axis_points))
.unwrap_or((*start, *end));
let body = sweep_model::revolved_surface(&entity, start.to_array(), end.to_array(), *angle, *start_angle)?;
let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_revolved_surface(&entity, start, end, *angle, *start_angle) else { return None };
Some(Err((body, fresh.surface_data)))
})(),
SurfaceSources::Loft { sections } => (|| {
let operations = self.document.solid_history_operations(handle)?;
let mut record = operations.into_iter().find_map(|operation| match operation {
SolidHistoryOperation::Loft(record) => Some(record),
_ => None,
})?;
if record.cross_sections.len() != sections.len() {
return None;
}
record.cross_sections = sections.iter()
.map(|section| source(*section).as_ref().and_then(crate::scene::model::loft_command_model::embedded_section))
.collect::<Option<Vec<_>>>()?;
Some(Ok(SolidHistoryOperation::Loft(record)))
})(),
};
let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new);
match made {
Some(Ok(operation)) => {
self.record_undo_before(handle, before);
if self.rebuild_solid_history(handle, operation) {
rebuilt.push((handle, crate::scene::ChangeKind::Modified));
}
}
Some(Err((body, data))) => {
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { continue };
let Some(display) = self.prepare_solid_model_display(handle, &body) else { continue };
self.record_undo_before(handle, before);
if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle) {
surface.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string());
surface.surface_data = data;
}
self.register_prepared_solid_model(handle, body, display);
rebuilt.push((handle, crate::scene::ChangeKind::Modified));
}
None => {}
}
}
rebuilt
}
/// Swept surfaces the reference keeps associative to a scale or twist
/// expression (an associative swept surface action whose value parameter
/// depends on an expression variable) are linked the same way here.
@@ -1279,23 +1371,57 @@ impl Scene {
};
let mut links = Vec::new();
let mut sources = Vec::new();
let mut others = Vec::new();
for item in self.document.objects.values() {
let ObjectType::Associative(action) = item else {
continue;
let ObjectType::Associative(action) = item else { continue };
let AssociativeData::Action(action) = &action.data else { continue };
let Some(AssociativeData::SurfaceActionBody(body)) = object(action.action_body) else { continue };
let Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency) else { continue };
// The entities a named path parameter reads (an edge of a region
// reads through a geometry dependency).
let read = |name: &str| -> Vec<Handle> {
action.owned_parameters.iter().filter_map(|parameter| {
let Some(AssociativeData::PathActionParam(path)) = object(*parameter) else { return None };
if path.compound.action_param.name != name { return None; }
let Some(AssociativeData::EdgeActionParam(edge)) = object(*path.compound.parameters.first()?) else { return None };
match object(edge.single_dependency.dependency) {
Some(AssociativeData::Dependency(dependency)) => Some(dependency.dependent_on),
Some(AssociativeData::GeomDependency(dependency)) => Some(dependency.dependency.dependent_on),
_ => None,
}
}).collect()
};
let AssociativeData::Action(action) = &action.data else {
continue;
};
let Some(AssociativeData::SurfaceActionBody(body)) = object(action.action_body) else {
continue;
};
if body.kind != AssocSurfaceActionKind::Swept {
continue;
let real = |name: &str| action.values.iter().find(|value| value.name == name)
.and_then(|value| value.variables.first())
.and_then(|variable| match variable.value.value {
codec::objects::AssocEvalValue::Real(value) => Some(value),
_ => None,
});
match body.kind {
AssocSurfaceActionKind::Extruded => {
if let (Some(profile), Some(height), Some(taper)) =
(read("ExtrudeProfile").first().copied(), real("ExtrusionHeight"), real("ExtrusionTaperAngle"))
{
others.push((written.dependent_on, (body.kind, vec![profile], None, height, taper)));
}
continue;
}
AssocSurfaceActionKind::Revolved => {
if let (Some(profile), Some(angle)) = (read("RevolveProfile").first().copied(), real("RevolveAngle")) {
others.push((written.dependent_on, (body.kind, vec![profile], read("RevolvePath").first().copied(), angle, 0.0)));
}
continue;
}
AssocSurfaceActionKind::Lofted => {
let sections = read("CrossSection");
if sections.len() >= 2 {
others.push((written.dependent_on, (body.kind, sections, None, 0.0, 0.0)));
}
continue;
}
AssocSurfaceActionKind::Swept => {}
_ => continue,
}
let Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency)
else {
continue;
};
let mut expressions: [Option<String>; 2] = [None, None];
for value in &action.values {
let slot = match value.name.as_str() {
@@ -1325,28 +1451,7 @@ impl Scene {
links.push((written.dependent_on, expressions));
}
// The profile and path the action reads.
let source = |name: &str| {
action.owned_parameters.iter().find_map(|parameter| {
let Some(AssociativeData::PathActionParam(path)) = object(*parameter) else {
return None;
};
if path.compound.action_param.name != name {
return None;
}
let Some(AssociativeData::EdgeActionParam(edge)) =
object(*path.compound.parameters.first()?)
else {
return None;
};
match object(edge.single_dependency.dependency) {
Some(AssociativeData::Dependency(dependency)) => {
Some(dependency.dependent_on)
}
_ => None,
}
})
};
if let (Some(profile), Some(path)) = (source("SweepProfile"), source("SweepPath")) {
if let (Some(profile), Some(path)) = (read("SweepProfile").first().copied(), read("SweepPath").first().copied()) {
sources.push((written.dependent_on, profile, path));
}
}
@@ -1387,6 +1492,44 @@ impl Scene {
&expressions,
);
}
// Associative extruded, revolved and lofted surfaces follow their
// sources here too.
use crate::scene::model::surface_sources::{link_surface_sources, surface_sources, SurfaceSources};
for (surface, (kind, handles, axis, amount, taper)) in others {
let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue };
if surface_sources(entity).is_some() { continue; }
let linked = match (kind, &value.surface_data) {
(AssocSurfaceActionKind::Extruded, codec::entities::SurfaceData::Extruded { sweep_vector, .. }) => {
let profile = handles[0];
let along = glam::DVec3::new(sweep_vector.x, sweep_vector.y, sweep_vector.z);
// A direction other than the profile's normal is kept.
let normal = self.document.get_entity(profile)
.and_then(|entity| crate::scene::model::sweep_model::extrusion_profile_of(entity))
.and_then(|(shape, _)| shape.plane.normal())
.map(glam::DVec3::from_array);
let direction = match normal {
Some(normal) if (normal * amount - along).length() <= 1e-9 * amount.abs().max(1.0) => None,
_ if along.length() > 1e-12 => Some(along),
_ => None,
};
SurfaceSources::Extrude { profile, height: amount, direction, taper }
}
(AssocSurfaceActionKind::Revolved, codec::entities::SurfaceData::Revolved { axis_point, axis_vector, start_angle, .. }) => {
let start = glam::DVec3::new(axis_point.x, axis_point.y, axis_point.z);
SurfaceSources::Revolve {
profile: handles[0],
axis,
start,
end: start + glam::DVec3::new(axis_vector.x, axis_vector.y, axis_vector.z),
angle: amount,
start_angle: *start_angle,
}
}
(AssocSurfaceActionKind::Lofted, _) => SurfaceSources::Loft { sections: handles },
_ => continue,
};
link_surface_sources(&mut self.document, surface, &linked);
}
}
pub fn rebuild_solid_history(