feat(sweep): associative swept surfaces in the drawing's network

Swept surfaces made with SURFACEASSOCIATIVITY on keep their profile and
path and are written as a swept-surface action of the drawing's
associative network, reading the profile and path and their scale and
twist expressions. They follow edits of their sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-08 13:29:55 +03:00
1 parent 018ac455cb
commit 48c3c433a5
10 files changed
+627 -22

No files matched your search

+299 -2
View File
@@ -2482,6 +2482,9 @@ fn detach_preserved_scope(
Some(ObjectType::Associative(object)) => match &object.data {
AssociativeData::ConstraintGroup(_) | AssociativeData::Network(_) => false,
AssociativeData::Variable(variable) => variable.name.trim().is_empty(),
// A surface action whose surface was erased goes with it.
AssociativeData::Action(_) => action_surface(document, *handle)
.is_none_or(|surface| document.get_entity(surface).is_some()),
_ => true,
},
_ => false,
@@ -2489,6 +2492,35 @@ fn detach_preserved_scope(
.collect(),
_ => Vec::new(),
};
// Unnamed variables stay only while a kept action reads them.
let read_variables: Vec<Handle> = foreign_actions
.iter()
.filter_map(|handle| match document.objects.get(handle) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Action(action),
..
})) => Some(action),
_ => None,
})
.flat_map(|action| action.values.iter().flat_map(|value| value.variables.iter().map(|variable| variable.handle)))
.filter_map(|handle| match document.objects.get(&handle) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ValueDependency(value),
..
})) => Some(value.dependency.dependent_on),
_ => None,
})
.collect();
let foreign_actions: Vec<Handle> = foreign_actions
.into_iter()
.filter(|handle| match document.objects.get(handle) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Variable(_),
..
})) => read_variables.contains(handle),
_ => true,
})
.collect();
let preserved_groups: Vec<_> = native_group_handles(document, owner)
.into_iter()
.filter(|handle| {
@@ -2574,6 +2606,265 @@ fn detach_preserved_scope(
(variables, preserved_groups, foreign_actions)
}
/// The surface an associative surface action writes.
fn action_surface(document: &CadDocument, action: Handle) -> Option<Handle> {
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Action(action),
..
})) = document.objects.get(&action)
else {
return None;
};
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::SurfaceActionBody(body),
..
})) = document.objects.get(&action.action_body)
else {
return None;
};
match document.objects.get(&body.surface_body.dependency) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Dependency(dependency),
..
})) => Some(dependency.dependent_on),
_ => None,
}
}
/// Associative swept surfaces made here that the drawing's network does
/// not carry yet: (surface, profile, path).
fn pending_swept_surfaces(document: &CadDocument) -> Vec<(Handle, Handle, Handle)> {
use crate::scene::model::sweep_model;
let carried: Vec<Handle> = document
.objects
.keys()
.filter_map(|handle| action_surface(document, *handle))
.collect();
let mut pending = document
.entities()
.filter(|entity| matches!(entity, codec::EntityType::Surface(surface)
if matches!(surface.surface_data, codec::entities::SurfaceData::Swept { .. })))
.filter_map(|entity| {
let (profile, path) = sweep_model::sweep_sources(entity)?;
let surface = entity.common().handle;
(!carried.contains(&surface)
&& document.get_entity(profile).is_some_and(|e| sweep_model::assoc_edge_curve(e).is_some())
&& document.get_entity(path).is_some_and(|e| sweep_model::assoc_edge_curve(e).is_some()))
.then_some((surface, profile, path))
})
.collect::<Vec<_>>();
pending.sort_by_key(|(surface, _, _)| surface.value());
pending
}
/// The names an expression reads.
fn expression_names(expression: &str) -> Vec<String> {
let mut names = Vec::new();
let mut current = String::new();
for character in expression.chars().chain(std::iter::once(' ')) {
if character.is_alphanumeric() || character == '_' {
current.push(character);
} else if !current.is_empty() {
if current.chars().next().is_some_and(|c| c.is_alphabetic()) && !names.contains(&current) {
names.push(current.clone());
}
current.clear();
}
}
names
}
/// The reference's associative network for each pending swept surface: an
/// action with a swept-surface body that writes the surface and reads its
/// profile and path, with the rotation, scale and twist as value
/// parameters; a scale or twist expression is an unnamed variable reading
/// the named ones. Returns the network's new actions in order.
fn materialize_swept_surface_actions(
allocator: &mut Allocator,
network: Handle,
parameters: &ParameterTable,
pending: &[(Handle, Handle, Handle)],
first_index: i32,
) -> Vec<Handle> {
use crate::scene::model::sweep_model;
use codec::objects::*;
let mut added = Vec::new();
for (surface, profile, path) in pending.iter().copied() {
let document = &*allocator.document;
let Some(surface_entity) = document.get_entity(surface) else { continue };
let codec::EntityType::Surface(surface_value) = surface_entity else { continue };
let codec::entities::SurfaceData::Swept { options, .. } = &surface_value.surface_data else { continue };
let (rotation, scale, twist) = (options.align_angle, options.scale_factor, options.twist_angle);
let expressions = sweep_model::sweep_expressions(surface_entity).unwrap_or([None, None]);
let (Some(profile_curve), Some(path_curve)) = (
document.get_entity(profile).and_then(sweep_model::assoc_edge_curve),
document.get_entity(path).and_then(sweep_model::assoc_edge_curve),
) else { continue };
let evaluate = |expression: &str| {
let mut table = parameters.clone();
table.set("sweepExpression", expression).ok()?;
table.resolve("sweepExpression").ok().filter(|value| value.is_finite())
};
let document = &mut *allocator.document;
let mut next = || document.allocate_handle();
let (action, body, written, profile_param, profile_edge, profile_read, path_param, path_edge, path_read) =
(next(), next(), next(), next(), next(), next(), next(), next(), next());
let dependency = |dependent_on: Handle, read: bool, id: i32, order: i32| AssocDependency {
class_version: 2,
status: 0,
is_read_dependency: read,
is_write_dependency: !read,
is_attached_to_object: true,
is_delegating_to_owning_action: true,
order,
dependent_on,
name: None,
read_dependency: Handle::NULL,
node: Handle::NULL,
dependency_body: Handle::NULL,
dependency_body_id: id,
};
let mut objects: Vec<(Handle, Handle, &str, &str, AssociativeData)> = Vec::new();
let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written), (profile, profile_read), (path, path_read)];
let mut dependencies = vec![written, profile_read, path_read];
let mut value_handles = [Handle::NULL, Handle::NULL];
let mut network_actions = vec![action];
// Scale (slot 0) and twist (slot 1) expressions.
for (slot, expression) in expressions.iter().enumerate() {
let Some(expression) = expression else { continue };
let Some(value) = evaluate(expression) else { continue };
let (variable, read_by_action) = (document.allocate_handle(), document.allocate_handle());
let mut variable_dependencies = Vec::new();
for (index, name) in expression_names(expression).iter().enumerate() {
let Some(named) = allocator.variables.get(name).copied() else { continue };
let reads = document.allocate_handle();
let named_value = parameters.resolve(name).unwrap_or(0.0);
objects.push((reads, variable, "ACDBASSOCVALUEDEPENDENCY", "AcDbAssocValueDependency",
AssociativeData::ValueDependency(AssocValueDependency {
dependency: dependency(named, true, index as i32 + 1, 0),
class_version: 0,
name: String::new(),
value: Allocator::numeric_eval(named_value),
})));
reactors.push((named, reads));
variable_dependencies.push(AssocVariableDependency { dependency: reads, flags: 0 });
}
objects.push((variable, network, "ACDBASSOCVARIABLE", "AcDbAssocVariable", AssociativeData::Variable(AssocVariable {
action: AssocAction {
class_version: 2,
owning_network: network,
max_dependency_index: variable_dependencies.len() as i32,
..Default::default()
},
class_version: 2,
name: String::new(),
expression: expression.clone(),
evaluator: "AcDbCalc:1.0".to_string(),
description: String::new(),
value: Allocator::numeric_eval(value),
has_cached_value: false,
cached_value: String::new(),
flag: false,
dependencies: variable_dependencies,
})));
objects.push((read_by_action, action, "ACDBASSOCVALUEDEPENDENCY", "AcDbAssocValueDependency",
AssociativeData::ValueDependency(AssocValueDependency {
dependency: dependency(variable, true, dependencies.len() as i32 + 1, 0),
class_version: 0,
name: String::new(),
value: Allocator::numeric_eval(value),
})));
reactors.push((variable, read_by_action));
dependencies.push(read_by_action);
value_handles[slot] = read_by_action;
network_actions.push(variable);
}
let real = |value: f64| AssocEvalVariant { code: 40, value: AssocEvalValue::Real(value) };
let value = |name: &str, unit_type: i32, amount: f64, handle: Handle| AssocValueParam {
class_version: 0,
name: name.to_string(),
unit_type,
variables: vec![AssocValueParamVariable { value: real(amount), handle }],
controlled_object_dependency: written,
};
objects.push((action, network, "ACDBASSOCACTION", "AcDbAssocAction", AssociativeData::Action(AssocAction {
class_version: 2,
geometry_status: 0,
owning_network: network,
action_body: body,
action_index: 0,
max_dependency_index: dependencies.len() as i32,
dependencies: dependencies.iter().map(|handle| AssocActionDependency { is_owned: true, dependency: *handle }).collect(),
owned_parameters: vec![profile_param, path_param],
values: vec![
value("RotationAngle", 2, rotation, Handle::NULL),
value("ScaleFactor", 0, scale, value_handles[0]),
value("TwistAngle", 2, twist, value_handles[1]),
],
})));
objects.push((body, action, "ACDBASSOCSWEPTSURFACEACTIONBODY", "AcDbAssocSweptSurfaceActionBody",
AssociativeData::SurfaceActionBody(AssocSurfaceActionBody {
kind: AssocSurfaceActionKind::Swept,
action_body: AssocActionBody { version: 2 },
surface_body: AssocSurfaceBody { version: 0, dependency: written, is_semi_associative: false, marker: 1, is_semi_override: false, grip_status: 0 },
..Default::default()
})));
objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN))));
let edge_param = |read: Handle, (kind, subcurve, curve): (i32, AssocSubcurveKind, Vec<AssocCurveValue>)| AssociativeData::EdgeActionParam(AssocEdgeActionParam {
single_dependency: AssocSingleDependencyActionParam {
action_param: AssocActionParam { is_r2013: 1, version: 0, name: String::new() },
dependency_class_version: 0,
dependency: read,
class_version: 0,
},
parameter: Handle::NULL,
has_action: true,
action_type: kind,
subcurve_kind: subcurve,
curve,
});
let path_parameter = |name: &str, child: Handle| AssociativeData::PathActionParam(AssocPathActionParam {
compound: AssocCompoundActionParam {
action_param: AssocActionParam { is_r2013: 1, version: 0, name: name.to_string() },
class_version: 0,
status: 0,
parameters: vec![child],
child_parameter: None,
},
version: 0,
});
objects.push((profile_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam", path_parameter("SweepProfile", profile_edge)));
objects.push((profile_edge, profile_param, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_param(profile_read, profile_curve)));
objects.push((profile_read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(profile, true, 2, 0))));
objects.push((path_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam", path_parameter("SweepPath", path_edge)));
objects.push((path_edge, path_param, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_param(path_read, path_curve)));
objects.push((path_read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(path, true, 3, 0))));
for (handle, owner, dxf, class, data) in objects {
allocator.insert_associative_at(handle, owner, dxf, class, data);
}
for (target, reactor) in reactors {
let document = &mut *allocator.document;
if let Some(entity) = document.get_entity_mut(target) {
entity.common_mut().reactors.push(reactor);
} else if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&target) {
object.reactors.push(reactor);
}
}
added.extend(network_actions);
}
for (offset, handle) in added.iter().copied().enumerate() {
if let Some(ObjectType::Associative(object)) = allocator.document.objects.get_mut(&handle) {
let action = match &mut object.data {
AssociativeData::Action(action) => action,
AssociativeData::Variable(variable) => &mut variable.action,
_ => continue,
};
action.action_index = first_index + offset as i32 + 1;
}
}
added
}
/// Re-evaluates the unnamed variables of kept actions (a swept surface's
/// scale or twist expression) and the values their actions cache, so the
/// file carries the values the named parameters give.
@@ -2714,6 +3005,7 @@ fn materialize_scope(
&& existing_variables.is_empty()
&& preserved_groups.is_empty()
&& foreign_actions.is_empty()
&& !(materialize_all_parameters && !pending_swept_surfaces(document).is_empty())
{
return None;
}
@@ -2898,6 +3190,9 @@ fn materialize_scope(
}
}
refresh_foreign_values(allocator.document, &foreign_actions, parameters);
let pending = if materialize_all_parameters { pending_swept_surfaces(allocator.document) } else { Vec::new() };
let surface_actions = materialize_swept_surface_actions(
&mut allocator, network_handle, parameters, &pending, first_foreign + foreign_actions.len() as i32);
let network = AssocNetwork {
action: AssocAction {
@@ -2907,7 +3202,7 @@ fn materialize_scope(
..Default::default()
},
network_version: 0,
network_action_index: first_foreign + foreign_actions.len() as i32,
network_action_index: first_foreign + (foreign_actions.len() + surface_actions.len()) as i32,
actions: allocator
.variable_actions
.iter()
@@ -2915,6 +3210,7 @@ fn materialize_scope(
.chain(group_handle)
.chain(preserved_groups)
.chain(foreign_actions)
.chain(surface_actions)
.map(|dependency| AssocActionDependency {
is_owned: true,
dependency,
@@ -3055,7 +3351,8 @@ impl Scene {
};
if set.is_none()
&& !existing
&& (owner != model_owner || self.named_parameters.is_empty())
&& (owner != model_owner
|| (self.named_parameters.is_empty() && pending_swept_surfaces(&self.document).is_empty()))
{
continue;
}