forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
018ac455cb
commit
48c3c433a5
10 files changed
+627
-22
No files matched your search
@@ -489,6 +489,8 @@ impl OpenCADStudio {
|
||||
.collect::<Vec<_>>();
|
||||
// Group 294: a base point the user picked.
|
||||
let picked_base = options.base_point.is_some();
|
||||
let several = profiles.len() > 1;
|
||||
let picked = options;
|
||||
let options = sweep_model::sweep_selection_options(&selection, options);
|
||||
let pending = if profiles.is_empty() {
|
||||
None
|
||||
@@ -532,8 +534,10 @@ impl OpenCADStudio {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// An associative surface keeps the profile and path it reads.
|
||||
let associative = surface && sweep_model::surface_associativity();
|
||||
let deletes_path =
|
||||
crate::app::delobj_deletes_auxiliary(delete_objects, surface);
|
||||
!associative && crate::app::delobj_deletes_auxiliary(delete_objects, surface);
|
||||
if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) {
|
||||
failed += 1;
|
||||
continue;
|
||||
@@ -546,6 +550,14 @@ impl OpenCADStudio {
|
||||
created,
|
||||
&expressions,
|
||||
);
|
||||
if associative {
|
||||
// A picked base point, or the one several profiles share.
|
||||
let linked = crate::command::SweepOptions {
|
||||
base_point: if picked_base || several { options.and_then(|o| o.base_point) } else { None },
|
||||
..picked
|
||||
};
|
||||
sweep_model::link_sweep_sources(&mut self.tabs[i].scene.document, created, handle, path_handle, &linked);
|
||||
}
|
||||
created
|
||||
} else {
|
||||
self.add_solid_model(
|
||||
@@ -558,7 +570,7 @@ impl OpenCADStudio {
|
||||
failed += 1;
|
||||
} else {
|
||||
created_handles.push(created);
|
||||
if delete_profiles {
|
||||
if delete_profiles && !associative {
|
||||
consumed.push(handle);
|
||||
}
|
||||
if deletes_path {
|
||||
|
||||
@@ -697,6 +697,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"PDFOSNAP",
|
||||
"UOSNAP",
|
||||
"FIELDDISPLAY",
|
||||
"SURFACEASSOCIATIVITY",
|
||||
"FIELDEVAL",
|
||||
"PDFIMPORTMODE",
|
||||
"PDFIMPORTFILTER",
|
||||
|
||||
@@ -1093,6 +1093,7 @@ impl OpenCADStudio {
|
||||
| "DGNOSNAP"
|
||||
| "UOSNAP"
|
||||
| "FIELDDISPLAY"
|
||||
| "SURFACEASSOCIATIVITY"
|
||||
| "PDFIMPORTMODE"
|
||||
| "PDFIMPORTFILTER"
|
||||
| "PDFIMPORTLAYERS"
|
||||
@@ -1301,6 +1302,28 @@ impl OpenCADStudio {
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if name == "SURFACEASSOCIATIVITY" {
|
||||
use crate::scene::model::sweep_model;
|
||||
let current = i16::from(sweep_model::surface_associativity());
|
||||
match value.as_deref().map(|v| v.trim().parse::<i16>().ok().filter(|v| (0..=1).contains(v))) {
|
||||
Some(Some(mode)) => {
|
||||
if current != mode {
|
||||
sweep_model::set_surface_associativity(mode == 1);
|
||||
self.save_config();
|
||||
}
|
||||
}
|
||||
Some(None) => {
|
||||
self.command_line.push_error(crate::t!("Requires 0 or 1 only.").as_ref());
|
||||
self.command_line.push_output(&format!("Enter new value for SURFACEASSOCIATIVITY <{current}>:"));
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
None => {
|
||||
self.command_line.push_output(&format!("Enter new value for SURFACEASSOCIATIVITY <{current}>:"));
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if name == "FIELDDISPLAY" {
|
||||
let current = i16::from(crate::entities::field::display());
|
||||
match value.as_deref().map(|v| v.trim().parse::<i16>().ok().filter(|v| (0..=1).contains(v))) {
|
||||
|
||||
@@ -412,6 +412,8 @@ pub struct UserSettings {
|
||||
pub attdef_on_screen: bool,
|
||||
/// FIELDDISPLAY: fields show on a gray background.
|
||||
pub field_display: bool,
|
||||
/// SURFACEASSOCIATIVITY: new surfaces stay associative to their sources.
|
||||
pub surface_associativity: bool,
|
||||
/// Nested-copy symbol handling: false inserts, true binds.
|
||||
pub ncopy_bind: bool,
|
||||
/// Last Options page; unknown saved names fall back without rejecting the config.
|
||||
@@ -823,6 +825,7 @@ impl Default for UserSettings {
|
||||
image_frame: 1,
|
||||
attdef_on_screen: true,
|
||||
field_display: true,
|
||||
surface_associativity: true,
|
||||
ncopy_bind: false,
|
||||
cursor_type: CursorType::Crosshair,
|
||||
crosshair_color: None,
|
||||
|
||||
@@ -678,6 +678,7 @@ impl OpenCADStudio {
|
||||
image_frame: crate::scene::frame::profile_image_mode(),
|
||||
attdef_on_screen: crate::modules::draw::draw::attdef::session().on_screen,
|
||||
field_display: crate::entities::field::display(),
|
||||
surface_associativity: crate::scene::model::sweep_model::surface_associativity(),
|
||||
ncopy_bind: self.ncopy_bind,
|
||||
cursor_type: self.cursor_type,
|
||||
crosshair_color: self.crosshair_color,
|
||||
@@ -787,6 +788,7 @@ impl OpenCADStudio {
|
||||
crate::scene::frame::set_profile_image_mode(s.image_frame);
|
||||
crate::modules::draw::draw::attdef::session().on_screen = s.attdef_on_screen;
|
||||
crate::entities::field::set_display(s.field_display);
|
||||
crate::scene::model::sweep_model::set_surface_associativity(s.surface_associativity);
|
||||
self.ncopy_bind = s.ncopy_bind;
|
||||
self.cursor_type = s.cursor_type;
|
||||
self.crosshair_color = s.crosshair_color;
|
||||
|
||||
@@ -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(¤t) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3677,6 +3677,11 @@ impl Scene {
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
for change in self.refresh_associative_sweeps(&changes) {
|
||||
if !changes.iter().any(|(handle, _)| *handle == change.0) {
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
if solve_parametric && !self.parametric_constraints.is_empty() {
|
||||
for change in self.refresh_parametric_constraints_with_initial_policy(
|
||||
&changes,
|
||||
|
||||
@@ -184,23 +184,7 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption
|
||||
/// The path traversed the other way when it was picked nearer its end
|
||||
/// (measured in plan, as the reference measures the pick), else `None`.
|
||||
fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option<EntityType> {
|
||||
let (start, end) = match path {
|
||||
EntityType::Polyline3D(value) if !value.is_closed() => {
|
||||
let p = |v: &codec::entities::Vertex3DPolyline| glam::DVec3::new(v.position.x, v.position.y, v.position.z);
|
||||
(p(value.vertices.first()?), p(value.vertices.last()?))
|
||||
}
|
||||
EntityType::Spline(value) if !value.flags.closed => {
|
||||
let points = if value.control_points.is_empty() { &value.fit_points } else { &value.control_points };
|
||||
let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
|
||||
(p(points.first()?), p(points.last()?))
|
||||
}
|
||||
_ => {
|
||||
let planar = crate::entities::curve::entity_curve(path)?;
|
||||
if planar.curve.is_closed() { return None; }
|
||||
let at = |t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t)));
|
||||
(at(0.0), at(1.0))
|
||||
}
|
||||
};
|
||||
let (start, end) = path_ends(path)?;
|
||||
let flat = |p: glam::DVec3| p.truncate().distance(pick.truncate());
|
||||
if flat(end) >= flat(start) { return None; }
|
||||
match path {
|
||||
@@ -220,6 +204,27 @@ fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option<EntityTy
|
||||
}
|
||||
}
|
||||
|
||||
/// The start and end of an open path.
|
||||
fn path_ends(path: &EntityType) -> Option<(glam::DVec3, glam::DVec3)> {
|
||||
Some(match path {
|
||||
EntityType::Polyline3D(value) if !value.is_closed() => {
|
||||
let p = |v: &codec::entities::Vertex3DPolyline| glam::DVec3::new(v.position.x, v.position.y, v.position.z);
|
||||
(p(value.vertices.first()?), p(value.vertices.last()?))
|
||||
}
|
||||
EntityType::Spline(value) if !value.flags.closed => {
|
||||
let points = if value.control_points.is_empty() { &value.fit_points } else { &value.control_points };
|
||||
let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
|
||||
(p(points.first()?), p(points.last()?))
|
||||
}
|
||||
_ => {
|
||||
let planar = crate::entities::curve::entity_curve(path)?;
|
||||
if planar.curve.is_closed() { return None; }
|
||||
let at = |t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t)));
|
||||
(at(0.0), at(1.0))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Option<SolidHistorySweep> {
|
||||
// Placed from the picked end; the record keeps the original path, and
|
||||
// the placed frame tells which end the sweep starts from.
|
||||
@@ -337,6 +342,193 @@ pub fn link_sweep_expressions(
|
||||
crate::scene::view::dispatch::set_entity_xdata(document, handle, SWEEP_EXPRESSION_APP, Some(values));
|
||||
}
|
||||
|
||||
/// SURFACEASSOCIATIVITY: whether new surfaces stay associative to the
|
||||
/// objects they were made from. A profile setting, so one value for every
|
||||
/// drawing.
|
||||
static SURFACE_ASSOCIATIVITY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
|
||||
|
||||
pub fn surface_associativity() -> bool {
|
||||
SURFACE_ASSOCIATIVITY.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_surface_associativity(on: bool) {
|
||||
SURFACE_ASSOCIATIVITY.store(on, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// The application name under which an associative swept surface keeps
|
||||
/// the profile and path it was swept from.
|
||||
pub const SWEEP_SOURCES_APP: &str = "OCS_SWEEP_SOURCES";
|
||||
|
||||
/// Marks a swept surface associative to the profile and path it was swept
|
||||
/// from, with the options it is swept again with when they change; the
|
||||
/// drawing's associative network for it is written on save.
|
||||
pub fn link_sweep_sources(
|
||||
document: &mut codec::CadDocument,
|
||||
surface: codec::Handle,
|
||||
profile: codec::Handle,
|
||||
path: codec::Handle,
|
||||
options: &SweepOptions,
|
||||
) {
|
||||
use codec::xdata::XDataValue;
|
||||
if surface.is_null() { return; }
|
||||
let point = |p: glam::DVec3| XDataValue::Point3D(Vector3::new(p.x, p.y, p.z));
|
||||
// The path keeps the direction it was swept in when it is edited.
|
||||
let reversed = options.path_pick
|
||||
.zip(document.get_entity(path))
|
||||
.is_some_and(|(pick, path)| reversed_toward_pick(path, pick).is_some());
|
||||
let mut values = vec![
|
||||
XDataValue::Handle(profile),
|
||||
XDataValue::Handle(path),
|
||||
XDataValue::Integer16(i16::from(options.align) | (i16::from(options.bank) << 1) | (i16::from(reversed) << 2)),
|
||||
];
|
||||
if let Some(base) = options.base_point {
|
||||
values.push(XDataValue::String("BASE".to_string()));
|
||||
values.push(point(base));
|
||||
}
|
||||
crate::scene::view::dispatch::set_entity_xdata(document, surface, SWEEP_SOURCES_APP, Some(values));
|
||||
}
|
||||
|
||||
/// The profile and path an associative swept surface was swept from.
|
||||
pub fn sweep_sources(entity: &EntityType) -> Option<(codec::Handle, codec::Handle)> {
|
||||
let record = entity.common().extended_data.records().iter()
|
||||
.find(|record| record.application_name == SWEEP_SOURCES_APP)?;
|
||||
let mut handles = record.values.iter().filter_map(|value| match value {
|
||||
codec::xdata::XDataValue::Handle(handle) => Some(*handle),
|
||||
_ => None,
|
||||
});
|
||||
Some((handles.next()?, handles.next()?))
|
||||
}
|
||||
|
||||
/// The surface swept again from its current profile and path, with the
|
||||
/// options it was made with: (surface record, body).
|
||||
pub fn resweep_from_sources(
|
||||
surface: &EntityType,
|
||||
profile: &EntityType,
|
||||
path: &EntityType,
|
||||
) -> Option<(SolidHistorySweep, Body)> {
|
||||
use codec::xdata::XDataValue;
|
||||
let record = surface.common().extended_data.records().iter()
|
||||
.find(|record| record.application_name == SWEEP_SOURCES_APP)?;
|
||||
let EntityType::Surface(value) = surface else { return None };
|
||||
let SurfaceData::Swept { options: stored, .. } = &value.surface_data else { return None };
|
||||
let mut options = SweepOptions {
|
||||
scale: stored.scale_factor,
|
||||
twist_angle: stored.twist_angle,
|
||||
..SweepOptions::default()
|
||||
};
|
||||
let mut values = record.values.iter();
|
||||
while let Some(value) = values.next() {
|
||||
match value {
|
||||
XDataValue::Integer16(flags) => {
|
||||
options.align = flags & 1 != 0;
|
||||
options.bank = flags & 2 != 0;
|
||||
// Swept from the end: picked there.
|
||||
if flags & 4 != 0 {
|
||||
options.path_pick = path_ends(path).map(|(_, end)| end);
|
||||
}
|
||||
}
|
||||
XDataValue::String(key) if key == "BASE" => {
|
||||
if let Some(XDataValue::Point3D(p)) = values.next() {
|
||||
options.base_point = Some(glam::DVec3::new(p.x, p.y, p.z));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let record = if spatial_profile(profile).is_some() {
|
||||
spatial_sweep_record(profile, path, options).ok()?
|
||||
} else {
|
||||
sweep_record(profile, path, options)?
|
||||
};
|
||||
let body = kernel::acis::rebuild_sweep_with_mode(&record, true).ok()?;
|
||||
Some((record, body))
|
||||
}
|
||||
|
||||
/// An entity's curve as the reference stores it in an edge action
|
||||
/// parameter: line segment (23: start, vector to the end), arc (11:
|
||||
/// centre, normal, reference axis, radius, start and end angle, 0) or a
|
||||
/// composite of those (47: count, then each part's type and values).
|
||||
pub fn assoc_edge_curve(entity: &EntityType) -> Option<(i32, codec::objects::AssocSubcurveKind, Vec<codec::objects::AssocCurveValue>)> {
|
||||
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
|
||||
use codec::types::Vector3 as P;
|
||||
let point = |p: glam::DVec3| V::Point(P::new(p.x, p.y, p.z));
|
||||
let segment = |a: glam::DVec3, b: glam::DVec3| vec![point(a), point(b - a)];
|
||||
// An arc about `normal` from `start` through `sweep` (signed, about the
|
||||
// normal) on a circle of `radius`; measured from the reference axis.
|
||||
let arc = |centre: glam::DVec3, normal: glam::DVec3, axis: glam::DVec3, radius: f64, start: f64, end: f64| vec![
|
||||
point(centre), point(normal), point(axis), V::Real(radius), V::Real(start), V::Real(end), V::Real(0.0),
|
||||
];
|
||||
match entity {
|
||||
EntityType::Line(line) => {
|
||||
let (a, b) = (glam::DVec3::new(line.start.x, line.start.y, line.start.z), glam::DVec3::new(line.end.x, line.end.y, line.end.z));
|
||||
Some((23, K::LineSegment3d, segment(a, b)))
|
||||
}
|
||||
EntityType::Circle(circle) => {
|
||||
let normal = glam::DVec3::new(circle.normal.x, circle.normal.y, circle.normal.z).try_normalize()?;
|
||||
let axis = arbitrary_x(normal);
|
||||
Some((11, K::Arc, arc(glam::DVec3::new(circle.center.x, circle.center.y, circle.center.z), normal, axis, circle.radius, 0.0, std::f64::consts::TAU)))
|
||||
}
|
||||
EntityType::Arc(value) => {
|
||||
let normal = glam::DVec3::new(value.normal.x, value.normal.y, value.normal.z).try_normalize()?;
|
||||
let axis = arbitrary_x(normal);
|
||||
let mut end = value.end_angle;
|
||||
while end <= value.start_angle { end += std::f64::consts::TAU; }
|
||||
Some((11, K::Arc, arc(glam::DVec3::new(value.center.x, value.center.y, value.center.z), normal, axis, value.radius, value.start_angle, end)))
|
||||
}
|
||||
EntityType::LwPolyline(polyline) => {
|
||||
let normal = glam::DVec3::new(polyline.normal.x, polyline.normal.y, polyline.normal.z).try_normalize()?;
|
||||
let axis_x = arbitrary_x(normal);
|
||||
let axis_y = normal.cross(axis_x);
|
||||
let at = |v: &codec::entities::LwVertex| axis_x * v.location.x + axis_y * v.location.y + normal * polyline.elevation;
|
||||
let count = polyline.vertices.len();
|
||||
let spans = if polyline.is_closed { count } else { count.saturating_sub(1) };
|
||||
if spans == 0 { return None; }
|
||||
let mut values = vec![V::Int(spans as i32)];
|
||||
for index in 0..spans {
|
||||
let (from, to) = (&polyline.vertices[index], &polyline.vertices[(index + 1) % count]);
|
||||
let (a, b) = (at(from), at(to));
|
||||
if from.bulge.abs() <= 1e-12 {
|
||||
values.push(V::Int(23));
|
||||
values.extend(segment(a, b));
|
||||
} else {
|
||||
// The arc in the polyline's plane; a clockwise one turns about
|
||||
// the opposite normal, from the same reference axis.
|
||||
let bulge = from.bulge;
|
||||
let chord = b - a;
|
||||
let centre = a + chord * 0.5 + normal.cross(chord) * ((1.0 - bulge * bulge) / (4.0 * bulge));
|
||||
let radius = chord.length() * (1.0 + bulge * bulge) / (4.0 * bulge.abs());
|
||||
let turn = normal * bulge.signum();
|
||||
let across = turn.cross(axis_x);
|
||||
let angle = |p: glam::DVec3| (p - centre).dot(across).atan2((p - centre).dot(axis_x));
|
||||
let start = angle(a);
|
||||
let end = start + 4.0 * bulge.atan().abs();
|
||||
values.push(V::Int(11));
|
||||
values.extend(arc(centre, turn, axis_x, radius, start, end));
|
||||
}
|
||||
}
|
||||
Some((47, K::None, values))
|
||||
}
|
||||
EntityType::Polyline3D(polyline) => {
|
||||
let points = polyline.vertices.iter().map(|v| glam::DVec3::new(v.position.x, v.position.y, v.position.z)).collect::<Vec<_>>();
|
||||
let spans = if polyline.is_closed() { points.len() } else { points.len().saturating_sub(1) };
|
||||
if spans == 0 { return None; }
|
||||
let mut values = vec![V::Int(spans as i32)];
|
||||
for index in 0..spans {
|
||||
values.push(V::Int(23));
|
||||
values.extend(segment(points[index], points[(index + 1) % points.len()]));
|
||||
}
|
||||
Some((47, K::None, values))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The arbitrary-axis X direction of an entity normal.
|
||||
fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 {
|
||||
let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 { glam::DVec3::Y } else { glam::DVec3::Z };
|
||||
world.cross(normal).normalize()
|
||||
}
|
||||
|
||||
/// The scale and twist expressions a swept surface is linked to.
|
||||
pub fn sweep_expressions(entity: &EntityType) -> Option<[Option<String>; 2]> {
|
||||
let record = entity.common().extended_data.records().iter()
|
||||
|
||||
@@ -13,7 +13,7 @@ use codec::EntityType;
|
||||
|
||||
use crate::entities::curve::entity_curve;
|
||||
pub use super::sweep_command_model::{
|
||||
is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
|
||||
is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, link_sweep_sources, sweep_sources, resweep_from_sources, surface_associativity, set_surface_associativity, assoc_edge_curve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
|
||||
};
|
||||
|
||||
/// A drawn profile, as the kernel wants it: the plane it lies in and the
|
||||
|
||||
@@ -1130,6 +1130,49 @@ impl Scene {
|
||||
rebuilt
|
||||
}
|
||||
|
||||
/// Associative swept surfaces follow edits of their profile or path:
|
||||
/// swept again from them with the options they were made with. A
|
||||
/// surface edited along with its sources keeps its own edit. Returns the
|
||||
/// surfaces rebuilt.
|
||||
pub(crate) fn refresh_associative_sweeps(&mut self, changes: &[(Handle, crate::scene::ChangeKind)]) -> Vec<(Handle, crate::scene::ChangeKind)> {
|
||||
use crate::scene::model::sweep_model;
|
||||
let changed = changes.iter()
|
||||
.filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed))
|
||||
.map(|(handle, _)| *handle)
|
||||
.collect::<rustc_hash::FxHashSet<_>>();
|
||||
if changed.is_empty() { return Vec::new(); }
|
||||
let linked = self.document.entities()
|
||||
.filter_map(|entity| {
|
||||
let handle = entity.common().handle;
|
||||
let (profile, path) = sweep_model::sweep_sources(entity)?;
|
||||
(!changed.contains(&handle) && (changed.contains(&profile) || changed.contains(&path)))
|
||||
.then_some((handle, profile, path))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut rebuilt = Vec::new();
|
||||
for (handle, profile, path) in linked {
|
||||
let (Some(surface), Some(profile), Some(path)) = (
|
||||
self.document.get_entity(handle),
|
||||
self.document.get_entity(profile),
|
||||
self.document.get_entity(path),
|
||||
) else { continue };
|
||||
let Some((record, body)) = sweep_model::resweep_from_sources(surface, profile, path) else { continue };
|
||||
let EntityType::Surface(fresh) = sweep_model::swept_surface_entity(&record) else { continue };
|
||||
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 };
|
||||
let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new);
|
||||
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 = fresh.surface_data;
|
||||
surface.point_of_reference = fresh.point_of_reference;
|
||||
}
|
||||
self.register_prepared_solid_model(handle, body, display);
|
||||
rebuilt.push((handle, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
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.
|
||||
@@ -1140,6 +1183,7 @@ impl Scene {
|
||||
_ => None,
|
||||
};
|
||||
let mut links = Vec::new();
|
||||
let mut sources = Vec::new();
|
||||
for item in self.document.objects.values() {
|
||||
let ObjectType::Associative(action) = item else { continue };
|
||||
let AssociativeData::Action(action) = &action.data else { continue };
|
||||
@@ -1160,6 +1204,32 @@ impl Scene {
|
||||
if expressions.iter().any(Option::is_some) {
|
||||
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")) {
|
||||
sources.push((written.dependent_on, profile, path));
|
||||
}
|
||||
}
|
||||
for (surface, profile, path) in sources {
|
||||
let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue };
|
||||
if crate::scene::model::sweep_model::sweep_sources(entity).is_some() { continue; }
|
||||
let codec::entities::SurfaceData::Swept { options, .. } = &value.surface_data else { continue };
|
||||
let linked = crate::command::SweepOptions {
|
||||
align: options.sweep_alignment_flags == 1,
|
||||
bank: options.bank,
|
||||
..crate::command::SweepOptions::default()
|
||||
};
|
||||
crate::scene::model::sweep_model::link_sweep_sources(&mut self.document, surface, profile, path, &linked);
|
||||
}
|
||||
for (handle, expressions) in links {
|
||||
let Some(entity) = self.document.get_entity(handle) else { continue };
|
||||
|
||||
Reference in new issue
Block a user