diff --git a/Cargo.lock b/Cargo.lock index ccd944b08..94a71d2e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 43cbad840..8a6c8aa03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/src/app/command_driver/parameters.rs b/src/app/command_driver/parameters.rs index 7308293d6..573798723 100644 --- a/src/app/command_driver/parameters.rs +++ b/src/app/command_driver/parameters.rs @@ -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 diff --git a/src/app/command_driver/solid3d.rs b/src/app/command_driver/solid3d.rs index d39a55caa..23c2e8814 100644 --- a/src/app/command_driver/solid3d.rs +++ b/src/app/command_driver/solid3d.rs @@ -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> { 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(), diff --git a/src/app/command_driver/tests.rs b/src/app/command_driver/tests.rs index be67ee374..c5a250660 100644 --- a/src/app/command_driver/tests.rs +++ b/src/app/command_driver/tests.rs @@ -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] diff --git a/src/command.rs b/src/command.rs index cc9fbe178..feb9be7f4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -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; 2], }, /// Loft through ordered cross-sections, with optional guides or a path. LoftEntities { diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 933ad656c..feaa80e1f 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -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; 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::() { + 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; diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index 2c2fb6e2b..90c8175f2 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -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, Vec) { +) -> (FxHashMap, Vec, Vec) { 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 = 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, preserved_groups: Vec, + foreign_actions: Vec, materialize_all_parameters: bool, ) -> Option { 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); diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs index 296b3699f..7318e7eda 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -43,7 +43,71 @@ fn polyline_wire(value: &codec::entities::Polyline3D) -> Option 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> { + 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::>(); + 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 { + 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; 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; 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::>(); + 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 { + 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() }, diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 3cb12ce46..3a3e3f15e 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -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 diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 52665d718..05ec7fd41 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -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 { + use crate::scene::model::sweep_model; + let linked = self.document.entities() + .filter_map(|entity| Some((entity.common().handle, sweep_model::sweep_expressions(entity)?))) + .collect::>(); + 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; 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::().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,