forked from 1b-agent/opencadstudio-upstream
Sweep spatial profiles as surfaces, keep swept surfaces linked to expressions
A closed 3D polyline profile that does not lie in one plane sweeps a surface with the reference's refusals. Swept surfaces made with an Expression scale or twist follow the named parameters, and reference surfaces linked through their associative action are picked up on open. Saving keeps the associative actions the constraint graph does not rebuild and stores each scope network through its own dictionary. Kernel 23138fc, codec 64fe2a6. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0c3ca96822
commit
07499968d8
11 files changed
+444
-24
No files matched your search
Generated
+3
-3
@@ -4205,7 +4205,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadcodec"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=063c106#063c10671fe7833d562f772159771318c7a0ebb9"
|
||||
source = "git+https://github.com/ramox81/cadcodec.git?rev=64fe2a6#64fe2a6f8896d674d606ded65f4c9cc4b581ac66"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.1",
|
||||
@@ -4237,7 +4237,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=3f3781a#3f3781a227008aec6847c894dc843245b539c682"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=23138fc#23138fcd3ef251c5624164a2311b8b264e22341e"
|
||||
dependencies = [
|
||||
"cavalier_contours",
|
||||
"opencadcodec",
|
||||
@@ -4248,7 +4248,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadkernel-constraints"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=3f3781a#3f3781a227008aec6847c894dc843245b539c682"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=23138fc#23138fcd3ef251c5624164a2311b8b264e22341e"
|
||||
dependencies = [
|
||||
"nalgebra",
|
||||
]
|
||||
|
||||
+5
-2
@@ -183,5 +183,8 @@ name = "integration"
|
||||
path = "tests/integration.rs"
|
||||
|
||||
[patch."https://github.com/HakanSeven12/opencadkernel.git"]
|
||||
opencadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "1df75f5" }
|
||||
opencadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "1df75f5" }
|
||||
opencadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "23138fc" }
|
||||
opencadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "23138fc" }
|
||||
|
||||
[patch."https://github.com/HakanSeven12/opencadcodec.git"]
|
||||
opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "64fe2a6" }
|
||||
@@ -448,6 +448,7 @@ impl OpenCADStudio {
|
||||
/// table, so unlike that whole-table Apply this can cheaply scope the
|
||||
/// re-solve to just the entities that actually reference it.
|
||||
fn resolve_named_parameter_edit(&mut self, i: usize, name: &str) {
|
||||
self.tabs[i].scene.refresh_expression_sweeps();
|
||||
let readers: Vec<&crate::scene::parametric_constraints::ParametricConstraint> = self
|
||||
.tabs[i]
|
||||
.scene
|
||||
|
||||
@@ -459,6 +459,7 @@ impl OpenCADStudio {
|
||||
mode,
|
||||
options,
|
||||
color: _,
|
||||
expressions,
|
||||
} = result
|
||||
else {
|
||||
unreachable!("router only routes the matching variant");
|
||||
@@ -501,6 +502,11 @@ impl OpenCADStudio {
|
||||
// reference does.
|
||||
let result = (|| -> Result<_, Option<u32>> {
|
||||
let (path, options) = path.as_ref().zip(options).ok_or(None)?;
|
||||
if sweep_model::spatial_profile(&profile).is_some() {
|
||||
let record = sweep_model::spatial_sweep_record(&profile, path, options)?;
|
||||
let body = kernel::acis::rebuild_sweep_with_mode(&record, true).map_err(|_| None)?;
|
||||
return Ok((body, record, true));
|
||||
}
|
||||
let mut record = sweep_model::sweep_record(&profile, path, options).ok_or(None)?;
|
||||
record.flags_294_296[0] |= picked_base;
|
||||
let (_, _, closed) = kernel::acis::sweep_profile_geometry(
|
||||
@@ -533,7 +539,9 @@ impl OpenCADStudio {
|
||||
continue;
|
||||
}
|
||||
let created = if surface {
|
||||
self.add_surface_model(sweep_model::swept_surface_entity(&record), body)
|
||||
let mut entity = sweep_model::swept_surface_entity(&record);
|
||||
sweep_model::link_sweep_expressions(&mut entity, &expressions);
|
||||
self.add_surface_model(entity, body)
|
||||
} else {
|
||||
self.add_solid_model(
|
||||
empty_solid3d(),
|
||||
|
||||
@@ -904,6 +904,7 @@ mod delobj_tests {
|
||||
mode,
|
||||
options: crate::command::SweepOptions::default(),
|
||||
color: [1.0; 4],
|
||||
expressions: [None, None],
|
||||
});
|
||||
|
||||
let created = app.tabs[i]
|
||||
|
||||
@@ -2104,6 +2104,9 @@ pub enum CmdResult {
|
||||
mode: ExtrudeMode,
|
||||
options: SweepOptions,
|
||||
color: [f32; 4],
|
||||
/// Scale and twist expressions entered with the Expression option;
|
||||
/// a swept surface stays linked to them.
|
||||
expressions: [Option<String>; 2],
|
||||
},
|
||||
/// Loft through ordered cross-sections, with optional guides or a path.
|
||||
LoftEntities {
|
||||
|
||||
@@ -1625,6 +1625,8 @@ pub struct SweepCommand {
|
||||
end_length: f64,
|
||||
/// The drawing's named parameters, for Expression input.
|
||||
parameters: crate::scene::named_parameters::ParameterTable,
|
||||
/// The scale and twist expressions entered, if any.
|
||||
expressions: [Option<String>; 2],
|
||||
isolines: usize,
|
||||
color: [f32; 4],
|
||||
}
|
||||
@@ -1667,6 +1669,7 @@ impl SweepCommand {
|
||||
new_length_start: None,
|
||||
end_length: 1.0,
|
||||
parameters: Default::default(),
|
||||
expressions: [None, None],
|
||||
isolines,
|
||||
color,
|
||||
}
|
||||
@@ -1720,6 +1723,7 @@ impl SweepCommand {
|
||||
mode: self.mode,
|
||||
options: self.options,
|
||||
color: self.color,
|
||||
expressions: self.expressions.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1945,6 +1949,7 @@ impl CadCommand for SweepCommand {
|
||||
self.new_length_start = None;
|
||||
self.step = SweepStep::ReferenceLength;
|
||||
} else if let Ok(scale) = value.parse::<f64>() {
|
||||
self.expressions[0] = None;
|
||||
self.set_scale(scale);
|
||||
}
|
||||
}
|
||||
@@ -1968,6 +1973,8 @@ impl CadCommand for SweepCommand {
|
||||
"The syntax is incorrect or a variable is not defined. Reenter the expression or value."
|
||||
).into_owned()));
|
||||
};
|
||||
let slot = if self.step == SweepStep::ScaleExpression { 0 } else { 1 };
|
||||
self.expressions[slot] = Some(text.trim().to_string());
|
||||
if self.step == SweepStep::ScaleExpression {
|
||||
self.set_scale(value);
|
||||
} else {
|
||||
@@ -1987,6 +1994,7 @@ impl CadCommand for SweepCommand {
|
||||
} else if let Some(angle) = crate::entities::common::parse_angle(value) {
|
||||
if angle.is_finite() {
|
||||
self.options.twist_angle = angle;
|
||||
self.expressions[1] = None;
|
||||
self.options.bank = false;
|
||||
SWEEP_BANK.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
self.step = SweepStep::PickPath;
|
||||
|
||||
@@ -1250,7 +1250,7 @@ impl Allocator<'_> {
|
||||
has_cached_value: false,
|
||||
cached_value: String::new(),
|
||||
flag: false,
|
||||
reserved: 0,
|
||||
dependencies: Vec::new(),
|
||||
}),
|
||||
);
|
||||
self.variables.insert(name.to_string(), handle);
|
||||
@@ -2463,9 +2463,31 @@ fn sync_retained_group_constraints(document: &mut CadDocument, set: &ParametricC
|
||||
fn detach_preserved_scope(
|
||||
document: &mut CadDocument,
|
||||
owner: Handle,
|
||||
) -> (FxHashMap<String, Handle>, Vec<Handle>) {
|
||||
) -> (FxHashMap<String, Handle>, Vec<Handle>, Vec<Handle>) {
|
||||
let Some(network_handle) = native_scope_network_handle(document, owner) else {
|
||||
return (FxHashMap::default(), Vec::new());
|
||||
return (FxHashMap::default(), Vec::new(), Vec::new());
|
||||
};
|
||||
// Actions the constraint graph does not rebuild (a swept surface's
|
||||
// action and the unnamed variables holding its expressions) stay in the
|
||||
// network with everything they own.
|
||||
let foreign_actions: Vec<Handle> = match document.objects.get(&network_handle) {
|
||||
Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::Network(network),
|
||||
..
|
||||
})) => network
|
||||
.actions
|
||||
.iter()
|
||||
.map(|action| action.dependency)
|
||||
.filter(|handle| match document.objects.get(handle) {
|
||||
Some(ObjectType::Associative(object)) => match &object.data {
|
||||
AssociativeData::ConstraintGroup(_) | AssociativeData::Network(_) => false,
|
||||
AssociativeData::Variable(variable) => variable.name.trim().is_empty(),
|
||||
_ => true,
|
||||
},
|
||||
_ => false,
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let preserved_groups: Vec<_> = native_group_handles(document, owner)
|
||||
.into_iter()
|
||||
@@ -2479,11 +2501,33 @@ fn detach_preserved_scope(
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if preserved_groups.is_empty() {
|
||||
return (FxHashMap::default(), Vec::new());
|
||||
if preserved_groups.is_empty() && foreign_actions.is_empty() {
|
||||
return (FxHashMap::default(), Vec::new(), Vec::new());
|
||||
}
|
||||
|
||||
let mut variable_handles = Vec::new();
|
||||
// The named variables the kept actions read keep their handles.
|
||||
for (_, object) in &document.objects {
|
||||
let ObjectType::Associative(AssociativeObject {
|
||||
owner,
|
||||
data: AssociativeData::ValueDependency(value),
|
||||
..
|
||||
}) = object
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let handle = value.dependency.dependent_on;
|
||||
let named = matches!(
|
||||
document.objects.get(&handle),
|
||||
Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::Variable(variable),
|
||||
..
|
||||
})) if !variable.name.trim().is_empty()
|
||||
);
|
||||
if named && foreign_actions.contains(owner) && !variable_handles.contains(&handle) {
|
||||
variable_handles.push(handle);
|
||||
}
|
||||
}
|
||||
for group_handle in &preserved_groups {
|
||||
let Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::ConstraintGroup(group),
|
||||
@@ -2510,7 +2554,7 @@ fn detach_preserved_scope(
|
||||
let dictionary = document
|
||||
.extension_dictionary_handle(owner)
|
||||
.unwrap_or(Handle::NULL);
|
||||
for handle in &preserved_groups {
|
||||
for handle in preserved_groups.iter().chain(&foreign_actions) {
|
||||
if let Some(ObjectType::Associative(object)) = document.objects.get_mut(handle) {
|
||||
object.owner = dictionary;
|
||||
}
|
||||
@@ -2527,7 +2571,100 @@ fn detach_preserved_scope(
|
||||
}
|
||||
|
||||
debug_assert!(document.objects.contains_key(&network_handle));
|
||||
(variables, preserved_groups)
|
||||
(variables, preserved_groups, foreign_actions)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn refresh_foreign_values(
|
||||
document: &mut CadDocument,
|
||||
foreign_actions: &[Handle],
|
||||
parameters: &ParameterTable,
|
||||
) {
|
||||
let value_of = |document: &CadDocument, handle: Handle| match document.objects.get(&handle) {
|
||||
Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::Variable(variable),
|
||||
..
|
||||
})) => {
|
||||
let mut table = parameters.clone();
|
||||
table.set("foreignExpression", &variable.expression).ok()?;
|
||||
table.resolve("foreignExpression").ok().filter(|value| value.is_finite())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let dependencies: Vec<(Handle, Handle)> = document
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(handle, object)| match object {
|
||||
ObjectType::Associative(AssociativeObject {
|
||||
owner,
|
||||
data: AssociativeData::ValueDependency(value),
|
||||
..
|
||||
}) if foreign_actions.contains(owner) => Some((*handle, value.dependency.dependent_on)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for &handle in foreign_actions {
|
||||
if let Some(value) = value_of(document, handle) {
|
||||
if let Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::Variable(variable),
|
||||
..
|
||||
})) = document.objects.get_mut(&handle)
|
||||
{
|
||||
variable.value = Allocator::numeric_eval(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (dependency, source) in dependencies {
|
||||
let Some(value) = value_of(document, source) else { continue };
|
||||
if let Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::ValueDependency(dependency),
|
||||
..
|
||||
})) = document.objects.get_mut(&dependency)
|
||||
{
|
||||
dependency.value = Allocator::numeric_eval(value);
|
||||
}
|
||||
}
|
||||
for &handle in foreign_actions {
|
||||
let Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::Action(action),
|
||||
..
|
||||
})) = document.objects.get(&handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut updates = Vec::new();
|
||||
for (index, param) in action.values.iter().enumerate() {
|
||||
for (slot, variable) in param.variables.iter().enumerate() {
|
||||
let Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::ValueDependency(dependency),
|
||||
..
|
||||
})) = document.objects.get(&variable.handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(value) = value_of(document, dependency.dependency.dependent_on) else {
|
||||
continue;
|
||||
};
|
||||
// Angle parameters are cached in radians.
|
||||
let value = if param.unit_type == 2 { value.to_radians() } else { value };
|
||||
updates.push((index, slot, value));
|
||||
}
|
||||
}
|
||||
if let Some(ObjectType::Associative(AssociativeObject {
|
||||
data: AssociativeData::Action(action),
|
||||
..
|
||||
})) = document.objects.get_mut(&handle)
|
||||
{
|
||||
for (index, slot, value) in updates {
|
||||
action.values[index].variables[slot].value = AssocEvalVariant {
|
||||
code: 40,
|
||||
value: AssocEvalValue::Real(value),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes one scope into the drawing's associative object graph.
|
||||
@@ -2540,6 +2677,7 @@ fn materialize_scope(
|
||||
action_index: i32,
|
||||
existing_variables: FxHashMap<String, Handle>,
|
||||
preserved_groups: Vec<Handle>,
|
||||
foreign_actions: Vec<Handle>,
|
||||
materialize_all_parameters: bool,
|
||||
) -> Option<Handle> {
|
||||
let dictionary_handle = ensure_extension_dictionary(document, owner);
|
||||
@@ -2575,6 +2713,7 @@ fn materialize_scope(
|
||||
&& parameters.is_empty()
|
||||
&& existing_variables.is_empty()
|
||||
&& preserved_groups.is_empty()
|
||||
&& foreign_actions.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -2741,6 +2880,25 @@ fn materialize_scope(
|
||||
}
|
||||
}
|
||||
|
||||
let first_foreign = allocator.variable_actions.len() as i32
|
||||
+ i32::from(group_handle.is_some())
|
||||
+ preserved_groups.len() as i32;
|
||||
for (offset, handle) in foreign_actions.iter().copied().enumerate() {
|
||||
if let Some(ObjectType::Associative(object)) = allocator.document.objects.get_mut(&handle) {
|
||||
object.owner = network_handle;
|
||||
let action = match &mut object.data {
|
||||
AssociativeData::Action(action) => Some(action),
|
||||
AssociativeData::Variable(variable) => Some(&mut variable.action),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(action) = action {
|
||||
action.owning_network = network_handle;
|
||||
action.action_index = first_foreign + offset as i32 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh_foreign_values(allocator.document, &foreign_actions, parameters);
|
||||
|
||||
let network = AssocNetwork {
|
||||
action: AssocAction {
|
||||
class_version: 2,
|
||||
@@ -2749,15 +2907,14 @@ fn materialize_scope(
|
||||
..Default::default()
|
||||
},
|
||||
network_version: 0,
|
||||
network_action_index: allocator.variable_actions.len() as i32
|
||||
+ i32::from(group_handle.is_some())
|
||||
+ preserved_groups.len() as i32,
|
||||
network_action_index: first_foreign + foreign_actions.len() as i32,
|
||||
actions: allocator
|
||||
.variable_actions
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(group_handle)
|
||||
.chain(preserved_groups)
|
||||
.chain(foreign_actions)
|
||||
.map(|dependency| AssocActionDependency {
|
||||
is_owned: true,
|
||||
dependency,
|
||||
@@ -2765,9 +2922,23 @@ fn materialize_scope(
|
||||
.collect(),
|
||||
owned_actions: Vec::new(),
|
||||
};
|
||||
// The reference holds a scope's network through a dictionary of its own
|
||||
// under the owner's extension dictionary; its parameter commands do not
|
||||
// find a network stored directly there.
|
||||
let holder_handle = allocator.document.allocate_handle();
|
||||
let mut holder = codec::objects::Dictionary::new();
|
||||
holder.handle = holder_handle;
|
||||
holder.owner = dictionary_handle;
|
||||
holder.duplicate_cloning = 1;
|
||||
holder.reactors.push(dictionary_handle);
|
||||
holder.add_entry(NETWORK_DICTIONARY_KEY, network_handle);
|
||||
allocator
|
||||
.document
|
||||
.objects
|
||||
.insert(holder_handle, ObjectType::Dictionary(holder));
|
||||
allocator.insert_associative_at(
|
||||
network_handle,
|
||||
dictionary_handle,
|
||||
holder_handle,
|
||||
"ASSOCNETWORK",
|
||||
"AcDbAssocNetwork",
|
||||
AssociativeData::Network(network),
|
||||
@@ -2775,14 +2946,14 @@ fn materialize_scope(
|
||||
if let Some(ObjectType::Associative(network)) =
|
||||
allocator.document.objects.get_mut(&network_handle)
|
||||
{
|
||||
network.reactors.push(dictionary_handle);
|
||||
network.reactors.push(holder_handle);
|
||||
}
|
||||
|
||||
set_dictionary_entry(
|
||||
allocator.document,
|
||||
dictionary_handle,
|
||||
NETWORK_DICTIONARY_KEY,
|
||||
network_handle,
|
||||
holder_handle,
|
||||
);
|
||||
Some(network_handle)
|
||||
}
|
||||
@@ -2845,6 +3016,7 @@ impl Scene {
|
||||
// `enabled` / `axis_direction` / `distance_direction` / `angle_sector`
|
||||
// writes, which happen on the local set during decode.
|
||||
self.bump_constraints_epoch();
|
||||
self.import_sweep_expression_links();
|
||||
}
|
||||
|
||||
/// Makes the standard associative graph match the live command model.
|
||||
@@ -2890,7 +3062,7 @@ impl Scene {
|
||||
if let Some(set) = &set {
|
||||
sync_retained_group_constraints(&mut self.document, set);
|
||||
}
|
||||
let (variables, groups) = detach_preserved_scope(&mut self.document, owner);
|
||||
let (variables, groups, foreign) = detach_preserved_scope(&mut self.document, owner);
|
||||
let mut set = set.unwrap_or_else(|| {
|
||||
if owner == model_owner {
|
||||
ParametricConstraintSet::new(ParametricScope::ModelSpace)
|
||||
@@ -2906,7 +3078,7 @@ impl Scene {
|
||||
for constraint in &mut set.constraints {
|
||||
constraint.native_origin = None;
|
||||
}
|
||||
scopes.push((owner, set, variables, groups));
|
||||
scopes.push((owner, set, variables, groups, foreign));
|
||||
}
|
||||
if scopes.is_empty() {
|
||||
return;
|
||||
@@ -2921,7 +3093,7 @@ impl Scene {
|
||||
);
|
||||
let root_network_handle = self.document.allocate_handle();
|
||||
let mut child_networks = Vec::new();
|
||||
for (index, (owner, set, variables, groups)) in scopes.into_iter().enumerate() {
|
||||
for (index, (owner, set, variables, groups, foreign)) in scopes.into_iter().enumerate() {
|
||||
let parameters = if set.local_parameters.is_empty() {
|
||||
&self.named_parameters
|
||||
} else {
|
||||
@@ -2936,6 +3108,7 @@ impl Scene {
|
||||
index as i32 + 1,
|
||||
variables,
|
||||
groups,
|
||||
foreign,
|
||||
owner == model_owner,
|
||||
) {
|
||||
child_networks.push(handle);
|
||||
|
||||
@@ -43,7 +43,71 @@ fn polyline_wire(value: &codec::entities::Polyline3D) -> Option<codec::entities:
|
||||
pub fn is_sweep_profile(entity: &EntityType) -> bool {
|
||||
embedded_sweep_profile(entity).is_some_and(|(profile, transform)| {
|
||||
kernel::acis::sweep_profile_geometry(&profile, transform).is_ok()
|
||||
})
|
||||
}) || spatial_profile(entity).is_some()
|
||||
}
|
||||
|
||||
/// The vertices of a 3D polyline profile that does not lie in one plane.
|
||||
pub fn spatial_profile(entity: &EntityType) -> Option<(Vec<[f64; 3]>, bool)> {
|
||||
let EntityType::Polyline3D(_) = entity else { return None };
|
||||
let (profile, transform) = embedded_sweep_profile(entity)?;
|
||||
kernel::acis::sweep_spatial_profile(&profile, transform)
|
||||
}
|
||||
|
||||
/// The sweep record of a spatial 3D polyline profile, which the reference
|
||||
/// sweeps as a surface: the profile stays in its XY plane and each point's
|
||||
/// height above it is carried along the path. Refused as the reference
|
||||
/// refuses it: along a path with a corner (85036), and when the profile seen
|
||||
/// from above crosses itself (85021); a side that is a point when seen from
|
||||
/// above cannot be swept at all (`None` code).
|
||||
pub fn spatial_sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Result<SolidHistorySweep, Option<u32>> {
|
||||
let (points, closed) = spatial_profile(profile).ok_or(None)?;
|
||||
let count = points.len();
|
||||
let sides = if closed { count } else { count - 1 };
|
||||
let plan = |index: usize| glam::DVec2::new(points[index % count][0], points[index % count][1]);
|
||||
if (0..sides).any(|index| plan(index).distance(plan(index + 1)) <= 1e-9) {
|
||||
return Err(None);
|
||||
}
|
||||
let cross = |a: glam::DVec2, b: glam::DVec2| a.x * b.y - a.y * b.x;
|
||||
for i in 0..sides {
|
||||
for j in i + 1..sides {
|
||||
// Neighbouring sides share a corner.
|
||||
if j == i + 1 || (closed && i == 0 && j == sides - 1) { continue; }
|
||||
let (p, r) = (plan(i), plan(i + 1) - plan(i));
|
||||
let (q, s) = (plan(j), plan(j + 1) - plan(j));
|
||||
let denominator = cross(r, s);
|
||||
if denominator.abs() <= 1e-12 { continue; }
|
||||
let t = cross(q - p, s) / denominator;
|
||||
let u = cross(q - p, r) / denominator;
|
||||
if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
|
||||
return Err(Some(85021));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (sweep_entity, _) = embedded_sweep_profile(profile).ok_or(None)?;
|
||||
let base_point = options.base_point.map(|point| point.to_array());
|
||||
let mut base = SolidHistoryNodeBase::new(1);
|
||||
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
||||
let record = SolidHistorySweep {
|
||||
base,
|
||||
operation_major: 1,
|
||||
sweep_entity: Some(sweep_entity),
|
||||
path_entity: Some(embedded_sweep_path(path).ok_or(None)?),
|
||||
scale_factor: options.scale,
|
||||
twist_angle: options.twist_angle,
|
||||
align_option: if options.align { 1 } else { 2 },
|
||||
has_align_start: true,
|
||||
flags_294_296: [base_point.is_some(), false, false],
|
||||
bank: options.bank,
|
||||
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
reference_point: base_point.map_or(Vector3::new(0.0, 0.0, 0.0), |p| Vector3::new(p[0], p[1], p[2])),
|
||||
miter_option: 2,
|
||||
..SolidHistorySweep::default()
|
||||
};
|
||||
if kernel::acis::sweep_history_path_has_corner(&record).ok_or(None)? {
|
||||
return Err(Some(85036));
|
||||
}
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// The modeling error code the reference reports for a refused sweep.
|
||||
@@ -101,6 +165,11 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption
|
||||
if options.base_point.is_some() {
|
||||
return Some(options);
|
||||
}
|
||||
// Spatial profiles keep their own anchor.
|
||||
let profiles = profiles.iter().filter(|profile| spatial_profile(profile).is_none()).collect::<Vec<_>>();
|
||||
if profiles.is_empty() {
|
||||
return Some(options);
|
||||
}
|
||||
let geometry = profiles.iter().map(|profile| {
|
||||
let (entity, transform) = embedded_sweep_profile(profile)?;
|
||||
let (plane, wires, _) = kernel::acis::sweep_profile_geometry(&entity, transform).ok()?;
|
||||
@@ -239,11 +308,80 @@ fn placed_sweep_record(profile: &EntityType, mut record: SolidHistorySweep) -> O
|
||||
}
|
||||
|
||||
pub fn swept_with_options(profile: &EntityType, path: &EntityType, mode: ExtrudeMode, options: SweepOptions) -> Option<Body> {
|
||||
if spatial_profile(profile).is_some() {
|
||||
return kernel::acis::rebuild_sweep_with_mode(&spatial_sweep_record(profile, path, options).ok()?, true).ok();
|
||||
}
|
||||
let record = sweep_record(profile, path, options)?;
|
||||
kernel::acis::rebuild_sweep_with_mode(&record, mode == ExtrudeMode::Surface).ok()
|
||||
}
|
||||
|
||||
/// Preserve native construction parameters alongside the sheet's saved B-rep.
|
||||
/// The application name of the expressions a swept surface stays linked to.
|
||||
pub const SWEEP_EXPRESSION_APP: &str = "OCS_SWEEP_EXPRESSION";
|
||||
|
||||
/// Links a swept surface to its scale and twist expressions (kept as the
|
||||
/// surface's own data, so they are saved with the drawing).
|
||||
pub fn link_sweep_expressions(entity: &mut EntityType, expressions: &[Option<String>; 2]) {
|
||||
if expressions.iter().all(Option::is_none) { return; }
|
||||
let mut record = codec::xdata::ExtendedDataRecord::new(SWEEP_EXPRESSION_APP);
|
||||
for (name, expression) in ["ScaleFactor", "TwistAngle"].iter().zip(expressions) {
|
||||
if let Some(expression) = expression {
|
||||
record.add_value(codec::xdata::XDataValue::String(name.to_string()));
|
||||
record.add_value(codec::xdata::XDataValue::String(expression.clone()));
|
||||
}
|
||||
}
|
||||
entity.common_mut().extended_data.add_record(record);
|
||||
}
|
||||
|
||||
/// 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()
|
||||
.find(|record| record.application_name == SWEEP_EXPRESSION_APP)?;
|
||||
let strings = record.values.iter().filter_map(|value| match value {
|
||||
codec::xdata::XDataValue::String(text) => Some(text.clone()),
|
||||
_ => None,
|
||||
}).collect::<Vec<_>>();
|
||||
let mut result = [None, None];
|
||||
for pair in strings.chunks(2) {
|
||||
if let [name, expression] = pair {
|
||||
match name.as_str() {
|
||||
"ScaleFactor" => result[0] = Some(expression.clone()),
|
||||
"TwistAngle" => result[1] = Some(expression.clone()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// The sweep record a swept surface was made from.
|
||||
pub fn surface_sweep_record(entity: &EntityType) -> Option<SolidHistorySweep> {
|
||||
let EntityType::Surface(surface) = entity else { return None };
|
||||
let SurfaceData::Swept { sweep_entity, path_entity, options, .. } = &surface.surface_data else { return None };
|
||||
let mut base = SolidHistoryNodeBase::new(1);
|
||||
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
||||
Some(SolidHistorySweep {
|
||||
base,
|
||||
operation_major: 1,
|
||||
sweep_entity: sweep_entity.clone(),
|
||||
path_entity: path_entity.clone(),
|
||||
draft_angle: options.draft_angle,
|
||||
scale_factor: options.scale_factor,
|
||||
twist_angle: options.twist_angle,
|
||||
align_angle: options.align_angle,
|
||||
align_option: options.sweep_alignment_flags as u8,
|
||||
has_align_start: true,
|
||||
align_start: options.align_start,
|
||||
bank: options.bank,
|
||||
sweep_entity_transform: options.sweep_entity_transform,
|
||||
path_entity_transform: options.path_entity_transform,
|
||||
reference_point: options.reference_vector,
|
||||
flags_294_296: [false, options.sweep_entity_transform_computed, options.path_entity_transform_computed],
|
||||
miter_option: 2,
|
||||
..SolidHistorySweep::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn swept_surface_entity(record: &SolidHistorySweep) -> EntityType {
|
||||
let mut surface = Surface::new(SurfaceKind::Swept);
|
||||
if let Ok(point) = kernel::acis::sweep_history_reference_point(record) {
|
||||
@@ -266,6 +404,8 @@ pub fn swept_surface_entity(record: &SolidHistorySweep) -> EntityType {
|
||||
align_start: record.align_start,
|
||||
bank: record.bank,
|
||||
base_point_set: true,
|
||||
sweep_entity_transform_computed: record.flags_294_296[1],
|
||||
path_entity_transform_computed: record.flags_294_296[2],
|
||||
reference_vector: record.reference_point,
|
||||
..SurfaceSweepOptions::default()
|
||||
},
|
||||
|
||||
@@ -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, 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, 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
|
||||
|
||||
@@ -1084,6 +1084,89 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/// Swept surfaces linked to scale/twist expressions follow the named
|
||||
/// parameters: re-evaluated and rebuilt when a value changes, as the
|
||||
/// reference keeps them associative. Returns the surfaces rebuilt.
|
||||
pub fn refresh_expression_sweeps(&mut self) -> Vec<Handle> {
|
||||
use crate::scene::model::sweep_model;
|
||||
let linked = self.document.entities()
|
||||
.filter_map(|entity| Some((entity.common().handle, sweep_model::sweep_expressions(entity)?)))
|
||||
.collect::<Vec<_>>();
|
||||
let mut rebuilt = Vec::new();
|
||||
for (handle, [scale, twist]) in linked {
|
||||
let Some(entity) = self.document.get_entity(handle) else { continue };
|
||||
let Some(mut record) = sweep_model::surface_sweep_record(entity) else { continue };
|
||||
let evaluate = |expression: &str| {
|
||||
let mut table = self.named_parameters().clone();
|
||||
table.set("sweepExpression", expression).ok()?;
|
||||
table.resolve("sweepExpression").ok().filter(|value| value.is_finite())
|
||||
};
|
||||
let mut changed = false;
|
||||
if let Some(value) = scale.as_deref().and_then(evaluate) {
|
||||
changed |= (record.scale_factor - value).abs() > 1e-12;
|
||||
record.scale_factor = value;
|
||||
}
|
||||
if let Some(value) = twist.as_deref().and_then(evaluate).map(f64::to_radians) {
|
||||
changed |= (record.twist_angle - value).abs() > 1e-12;
|
||||
record.twist_angle = value;
|
||||
}
|
||||
if !changed { continue; }
|
||||
let Ok(body) = kernel::acis::rebuild_sweep_with_mode(&record, true) 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());
|
||||
if let codec::entities::SurfaceData::Swept { options, .. } = &mut surface.surface_data {
|
||||
options.scale_factor = record.scale_factor;
|
||||
options.twist_angle = record.twist_angle;
|
||||
}
|
||||
}
|
||||
self.register_prepared_solid_model(handle, body, display);
|
||||
rebuilt.push(handle);
|
||||
}
|
||||
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.
|
||||
pub(crate) fn import_sweep_expression_links(&mut self) {
|
||||
use codec::objects::{AssocSurfaceActionKind, AssociativeData, ObjectType};
|
||||
let object = |handle: Handle| match self.document.objects.get(&handle) {
|
||||
Some(ObjectType::Associative(object)) => Some(&object.data),
|
||||
_ => None,
|
||||
};
|
||||
let mut links = Vec::new();
|
||||
for item in self.document.objects.values() {
|
||||
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 };
|
||||
if body.kind != 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() { "ScaleFactor" => 0, "TwistAngle" => 1, _ => continue };
|
||||
for variable in &value.variables {
|
||||
let Some(AssociativeData::ValueDependency(dependency)) = object(variable.handle) else { continue };
|
||||
let Some(AssociativeData::Variable(source)) = object(dependency.dependency.dependent_on) else { continue };
|
||||
if !source.expression.trim().is_empty() && source.expression.trim().parse::<f64>().is_err() {
|
||||
expressions[slot] = Some(source.expression.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if expressions.iter().any(Option::is_some) {
|
||||
links.push((written.dependent_on, expressions));
|
||||
}
|
||||
}
|
||||
for (handle, expressions) in links {
|
||||
let Some(entity) = self.document.get_entity_mut(handle) else { continue };
|
||||
if crate::scene::model::sweep_model::sweep_expressions(entity).is_some() { continue; }
|
||||
crate::scene::model::sweep_model::link_sweep_expressions(entity, &expressions);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rebuild_solid_history(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
|
||||
Reference in new issue
Block a user