diff --git a/src/app/command_driver/solid3d.rs b/src/app/command_driver/solid3d.rs index d9a78dc21..5042fb036 100644 --- a/src/app/command_driver/solid3d.rs +++ b/src/app/command_driver/solid3d.rs @@ -489,6 +489,8 @@ impl OpenCADStudio { .collect::>(); // 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 { diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index d1b05da03..72c664de6 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -697,6 +697,7 @@ inventory::submit!(crate::command::CommandRegistration { "PDFOSNAP", "UOSNAP", "FIELDDISPLAY", + "SURFACEASSOCIATIVITY", "FIELDEVAL", "PDFIMPORTMODE", "PDFIMPORTFILTER", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index e38b4adfa..8bd80ca92 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -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::().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::().ok().filter(|v| (0..=1).contains(v))) { diff --git a/src/app/settings.rs b/src/app/settings.rs index 93fa75b38..491097c2b 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -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, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index d5618c9dd..dc1be04bf 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -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; diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index ce2327f55..f2779fa26 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -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 = 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 = 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 { + 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 = 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::>(); + pending.sort_by_key(|(surface, _, _)| surface.value()); + pending +} + +/// The names an expression reads. +fn expression_names(expression: &str) -> Vec { + 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 { + 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)| 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; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index a2b7c264d..04208f985 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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, diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs index 7913e22a6..61be3864d 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -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 { - 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 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 { // 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)> { + 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::>(); + 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; 2]> { let record = entity.common().extended_data.records().iter() diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 3a3e3f15e..dcdc21641 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, 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 diff --git a/src/scene/modify.rs b/src/scene/modify.rs index ac069dcaf..23d798a0b 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -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::>(); + 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::>(); + 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 };