From 27bf35fc9673dd0c9689e554f6ef62442b1f84d1 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:31:58 +0300 Subject: [PATCH] Associative extruded, revolved and lofted surfaces; spline and ellipse sources SURFACEASSOCIATIVITY now keeps EXTRUDE, REVOLVE and LOFT surfaces linked to their sources: the profile (and axis object) or cross sections are kept, the surface is made again when they change, and the drawing's associative network carries an action with the extruded, revolved or lofted surface action body, its path parameters and value parameters as the reference writes them. Swept, extruded, revolved and lofted surfaces can read splines (NURBS edge curve, fit-point splines through their fit points) and ellipses (elliptical edge curve). Associative surfaces of a file opened here follow their sources too. Co-Authored-By: Claude Opus 5.5 --- src/app/command_driver/solid3d.rs | 70 +++++++- src/command.rs | 3 + src/modules/insert/solid3d_cmds.rs | 10 +- src/scene/dwg_native_constraints.rs | 230 ++++++++++++++++++++++++- src/scene/mod.rs | 5 + src/scene/model/loft_command_model.rs | 2 +- src/scene/model/mod.rs | 1 + src/scene/model/surface_sources.rs | 121 +++++++++++++ src/scene/model/sweep_command_model.rs | 54 ++++++ src/scene/modify.rs | 190 ++++++++++++++++++-- 10 files changed, 661 insertions(+), 25 deletions(-) create mode 100644 src/scene/model/surface_sources.rs diff --git a/src/app/command_driver/solid3d.rs b/src/app/command_driver/solid3d.rs index 5042fb036..05b01a213 100644 --- a/src/app/command_driver/solid3d.rs +++ b/src/app/command_driver/solid3d.rs @@ -42,6 +42,7 @@ impl OpenCADStudio { } let pending = self.begin_undo(i, "EXTRUDE", handles.len(), true); let mut created_handles = Vec::new(); + let mut associative_sources = Vec::new(); let mut consumed = Vec::new(); let mut failed = 0usize; for handle in &handles { @@ -125,12 +126,27 @@ impl OpenCADStudio { entity.surface_data = data; } } - match history { + let created = match history { Some(history) => { self.add_surface_model_with_history(surface, body, history) } None => self.add_surface_model(surface, body), + }; + // An associative surface keeps reading its profile. + if sweep_model::surface_associativity() && path.is_none() && !created.is_null() { + use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources}; + let (height, given) = match extent { + ExtrudeExtent::Height(height) => (height, None), + _ => (direction.length(), Some(direction)), + }; + link_surface_sources( + &mut self.tabs[i].scene.document, + created, + &SurfaceSources::Extrude { profile: *handle, height, direction: given, taper: taper_angle }, + ); + associative_sources.push(*handle); } + created } else { let direction = direction.unwrap_or(glam::DVec3::ZERO); let history = path @@ -159,7 +175,7 @@ impl OpenCADStudio { failed += 1; } else { created_handles.push(created); - if delete_sources { + if delete_sources && !associative_sources.contains(handle) { consumed.push(*handle); } } @@ -319,6 +335,7 @@ impl OpenCADStudio { handles, axis_start, axis_end, + axis_object, angle, start_angle, mode, @@ -355,6 +372,7 @@ impl OpenCADStudio { self.begin_undo(i, "REVOLVE", editable_handles.len(), true) }; let mut created_handles = Vec::new(); + let mut associative_sources = Vec::new(); let mut consumed = Vec::new(); for handle in &editable_handles { let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() @@ -374,7 +392,7 @@ impl OpenCADStudio { failed += 1; continue; }; - self.add_surface_model( + let created = self.add_surface_model( empty_revolved_surface( &entity, axis_start, @@ -383,7 +401,25 @@ impl OpenCADStudio { start_angle, ), body, - ) + ); + // An associative surface keeps reading its profile and axis. + if sweep_model::surface_associativity() && !created.is_null() { + use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources}; + link_surface_sources( + &mut self.tabs[i].scene.document, + created, + &SurfaceSources::Revolve { + profile: *handle, + axis: axis_object, + start: axis_start, + end: axis_end, + angle, + start_angle, + }, + ); + associative_sources.push(*handle); + } + created } else { let result = sweep_model::revolve_history(&entity, from, to, angle, start_angle) @@ -410,7 +446,7 @@ impl OpenCADStudio { failed += 1; } else { created_handles.push(created); - if delete_sources { + if delete_sources && !associative_sources.contains(handle) { consumed.push(*handle); } } @@ -678,6 +714,7 @@ impl OpenCADStudio { } let dirty_before = self.tabs[i].dirty; let pending = self.begin_undo(i, "LOFT", 1, true); + let mut associative = false; let created = if surface { let handle = self.add_surface_model( loft_command_model::surface_entity(&record), @@ -688,6 +725,27 @@ impl OpenCADStudio { handle, codec::objects::SolidHistoryOperation::Loft(record), ); + // An associative surface keeps reading its cross + // sections (each section a single object). + let single: Option> = sections + .iter() + .map(|section| match section { + LoftSectionSelection::Entity(handle) => Some(*handle), + _ => None, + }) + .collect(); + if let Some(single) = single.filter(|_| { + crate::scene::model::sweep_model::surface_associativity() + && guides.is_empty() + && path.is_none() + }) { + crate::scene::model::surface_sources::link_surface_sources( + &mut self.tabs[i].scene.document, + handle, + &crate::scene::model::surface_sources::SurfaceSources::Loft { sections: single }, + ); + associative = true; + } } handle } else { @@ -705,7 +763,7 @@ impl OpenCADStudio { self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref()); return Some(Task::none()); } else { - let mut consumed = if delete_sections { + let mut consumed = if delete_sections && !associative { sources.clone() } else { Vec::new() diff --git a/src/command.rs b/src/command.rs index feb9be7f4..186874542 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2092,6 +2092,9 @@ pub enum CmdResult { handles: Vec, axis_start: glam::DVec3, axis_end: glam::DVec3, + /// The line picked as the axis (Object), which an associative + /// surface keeps reading. + axis_object: Option, angle: f64, start_angle: f64, mode: ExtrudeMode, diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index feaa80e1f..43057bb40 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -1064,6 +1064,7 @@ pub struct RevolveCommand { handles: Vec, preview_profiles: Vec<(Handle, EntityType)>, injected_axis: Option, + axis_object: Option, axis_start: DVec3, axis_end: DVec3, working_plane: WorkingPlane, @@ -1114,6 +1115,7 @@ impl RevolveCommand { handles: Vec::new(), preview_profiles: Vec::new(), injected_axis: None, + axis_object: None, axis_start: DVec3::ZERO, axis_end: DVec3::new(0.0, 0.0, 1.0), working_plane: WorkingPlane::default(), @@ -1253,6 +1255,7 @@ impl RevolveCommand { }; self.axis_start = self.working_plane.origin; self.axis_end = self.axis_start + direction; + self.axis_object = None; self.step = RevolveStep::Angle; CmdResult::NeedPoint } @@ -1269,6 +1272,7 @@ impl RevolveCommand { handles: self.handles.clone(), axis_start: self.axis_start, axis_end: self.axis_end, + axis_object: self.axis_object, angle, start_angle: self.start_angle, mode: self.mode, @@ -1352,12 +1356,13 @@ impl CadCommand for RevolveCommand { } match self.step { RevolveStep::AxisObject => { - let Some((start, end)) = self.injected_axis.take().as_ref().and_then(revolve_axis) - else { + let axis = self.injected_axis.take(); + let Some((start, end)) = axis.as_ref().and_then(revolve_axis) else { return CmdResult::NeedPoint; }; self.axis_start = start; self.axis_end = end; + self.axis_object = matches!(axis, Some(EntityType::Line(_))).then_some(handle); self.step = RevolveStep::Angle; CmdResult::NeedPoint } @@ -1368,6 +1373,7 @@ impl CadCommand for RevolveCommand { match self.step { RevolveStep::AxisStart => { self.axis_start = pt; + self.axis_object = None; self.step = RevolveStep::AxisEnd; CmdResult::NeedPoint } diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index f2779fa26..d3074f691 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -2657,6 +2657,225 @@ fn pending_swept_surfaces(document: &CadDocument) -> Vec<(Handle, Handle, Handle pending } +/// Associative extruded, revolved and lofted surfaces made here that the +/// drawing's network does not carry yet, with their sources. +fn pending_source_surfaces( + document: &CadDocument, +) -> Vec<(Handle, crate::scene::model::surface_sources::SurfaceSources)> { + use crate::scene::model::{surface_sources, 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(_))) + .filter_map(|entity| { + let sources = surface_sources::surface_sources(entity)?; + let surface = entity.common().handle; + (!carried.contains(&surface) + && sources.handles().iter().all(|handle| { + document.get_entity(*handle).is_some_and(|e| sweep_model::assoc_edge_curve(e).is_some()) + })) + .then_some((surface, sources)) + }) + .collect::>(); + pending.sort_by_key(|(surface, _)| surface.value()); + pending +} + +/// Whether the drawing has associative surfaces its network has to carry. +fn has_pending_surfaces(document: &CadDocument) -> bool { + !pending_swept_surfaces(document).is_empty() || !pending_source_surfaces(document).is_empty() +} + +/// The reference's associative network for each pending extruded, revolved +/// or lofted surface: an action with the matching surface action body that +/// writes the surface and reads its sources through path parameters (an +/// empty one for a revolve axis given by points and for a loft without a +/// path), with the surface's values as value parameters. Returns the +/// network's new actions in order. +fn materialize_source_surface_actions( + allocator: &mut Allocator, + network: Handle, + pending: &[(Handle, crate::scene::model::surface_sources::SurfaceSources)], + first_index: i32, +) -> Vec { + use crate::scene::model::surface_sources::SurfaceSources; + use crate::scene::model::sweep_model; + use codec::objects::*; + let mut added = Vec::new(); + for (surface, sources) in pending.iter() { + let surface = *surface; + let document = &*allocator.document; + // The value parameters: name, unit type, values, and whether they + // control the surface. + let real = |value: f64| AssocEvalVariant { code: 40, value: AssocEvalValue::Real(value) }; + let short = |value: i32| AssocEvalVariant { code: 70, value: AssocEvalValue::Short(value as i16) }; + let (kind, class, dxf, values, params): ( + AssocSurfaceActionKind, + &str, + &str, + Vec<(&str, i32, Vec, bool)>, + Vec<(&str, Option)>, + ) = match sources { + SurfaceSources::Extrude { profile, height, taper, .. } => ( + AssocSurfaceActionKind::Extruded, + "AcDbAssocExtrudedSurfaceActionBody", + "ACDBASSOCEXTRUDEDSURFACEACTIONBODY", + vec![("ExtrusionHeight", 1, vec![real(*height)], true), ("ExtrusionTaperAngle", 2, vec![real(*taper)], true)], + vec![("ExtrudeProfile", Some(*profile))], + ), + SurfaceSources::Revolve { profile, axis, angle, .. } => ( + AssocSurfaceActionKind::Revolved, + "AcDbAssocRevolvedSurfaceActionBody", + "ACDBASSOCREVOLVEDSURFACEACTIONBODY", + vec![("RevolveAngle", 2, vec![real(*angle)], true)], + vec![("RevolveProfile", Some(*profile)), ("RevolvePath", *axis)], + ), + SurfaceSources::Loft { sections } => { + let loft = document.solid_history_operations(surface).and_then(|operations| { + operations.into_iter().find_map(|operation| match operation { + SolidHistoryOperation::Loft(record) => record.parameters, + _ => None, + }) + }); + let (continuity, bulge) = loft.map_or(((1, 1), (0.5, 0.5)), |p| { + ((p.start_continuity, p.end_continuity), (p.start_bulge, p.end_bulge)) + }); + ( + AssocSurfaceActionKind::Lofted, + "AcDbAssocLoftedSurfaceActionBody", + "ACDBASSOCLOFTEDSURFACEACTIONBODY", + vec![ + ("Continuity", 0, vec![short(continuity.0), short(continuity.1)], false), + ("Bulge", 0, vec![real(bulge.0), real(bulge.1)], false), + ], + sections.iter().map(|section| ("CrossSection", Some(*section))) + .chain(std::iter::once(("PathCurve", None))) + .collect(), + ) + } + }; + let curves = params + .iter() + .map(|(_, source)| match source { + Some(source) => document.get_entity(*source).and_then(sweep_model::assoc_edge_curve).map(Some), + None => Some(None), + }) + .collect::>>(); + let Some(curves) = curves else { continue }; + let document = &mut *allocator.document; + let (action, body, written) = (document.allocate_handle(), document.allocate_handle(), document.allocate_handle()); + 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)]; + let mut dependencies = vec![written]; + let mut owned = Vec::new(); + objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN)))); + for ((name, source), curve) in params.iter().zip(curves) { + let document = &mut *allocator.document; + let parameter = document.allocate_handle(); + let mut children = Vec::new(); + if let (Some(source), Some((action_type, subcurve_kind, curve))) = (source, curve) { + let (edge, read) = (document.allocate_handle(), document.allocate_handle()); + objects.push((edge, parameter, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", + 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, + subcurve_kind, + curve, + }))); + objects.push((read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", + AssociativeData::Dependency(dependency(*source, true, dependencies.len() as i32 + 1, 0)))); + reactors.push((*source, read)); + dependencies.push(read); + children.push(edge); + } + objects.push((parameter, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam", + AssociativeData::PathActionParam(AssocPathActionParam { + compound: AssocCompoundActionParam { + action_param: AssocActionParam { is_r2013: 1, version: 0, name: name.to_string() }, + class_version: 0, + status: 0, + parameters: children, + child_parameter: None, + }, + version: 0, + }))); + owned.push(parameter); + } + 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: owned, + values: values + .into_iter() + .map(|(name, unit_type, amounts, controls)| AssocValueParam { + class_version: 0, + name: name.to_string(), + unit_type, + variables: amounts.into_iter().map(|value| AssocValueParamVariable { value, handle: Handle::NULL }).collect(), + controlled_object_dependency: if controls { written } else { Handle::NULL }, + }) + .collect(), + }))); + objects.push((body, action, dxf, class, AssociativeData::SurfaceActionBody(AssocSurfaceActionBody { + kind, + 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() + }))); + 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.push(action); + } + for (offset, handle) in added.iter().copied().enumerate() { + if let Some(ObjectType::Associative(AssociativeObject { data: AssociativeData::Action(action), .. })) = + allocator.document.objects.get_mut(&handle) + { + action.action_index = first_index + offset as i32 + 1; + } + } + added +} + /// The names an expression reads. fn expression_names(expression: &str) -> Vec { let mut names = Vec::new(); @@ -3005,7 +3224,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()) + && !(materialize_all_parameters && has_pending_surfaces(document)) { return None; } @@ -3191,8 +3410,13 @@ 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( + let mut surface_actions = materialize_swept_surface_actions( &mut allocator, network_handle, parameters, &pending, first_foreign + foreign_actions.len() as i32); + let pending_sources = if materialize_all_parameters { pending_source_surfaces(allocator.document) } else { Vec::new() }; + let source_actions = materialize_source_surface_actions( + &mut allocator, network_handle, &pending_sources, + first_foreign + (foreign_actions.len() + surface_actions.len()) as i32); + surface_actions.extend(source_actions); let network = AssocNetwork { action: AssocAction { @@ -3352,7 +3576,7 @@ impl Scene { if set.is_none() && !existing && (owner != model_owner - || (self.named_parameters.is_empty() && pending_swept_surfaces(&self.document).is_empty())) + || (self.named_parameters.is_empty() && !has_pending_surfaces(&self.document))) { continue; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index bb92874c9..25d998ccf 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3701,6 +3701,11 @@ impl Scene { changes.push(change); } } + for change in self.refresh_associative_surfaces(&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/loft_command_model.rs b/src/scene/model/loft_command_model.rs index aa671e172..9577096a8 100644 --- a/src/scene/model/loft_command_model.rs +++ b/src/scene/model/loft_command_model.rs @@ -7,7 +7,7 @@ use kernel::brep::Body; use crate::command::{ExtrudeMode, LoftOptions, LoftSectionSelection}; use super::sweep_model::embedded_path; -fn embedded_section(entity: &EntityType) -> Option { +pub(crate) fn embedded_section(entity: &EntityType) -> Option { match entity { EntityType::Region(region) => Some(EmbeddedEntity::Region(region.clone())), EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())), diff --git a/src/scene/model/mod.rs b/src/scene/model/mod.rs index 5206cdd5b..e3547b3f1 100644 --- a/src/scene/model/mod.rs +++ b/src/scene/model/mod.rs @@ -17,6 +17,7 @@ pub mod solid_history; pub mod sweep_model; pub mod presspull_model; pub mod loft_command_model; +pub mod surface_sources; mod sweep_command_model; pub mod visual_style_model; pub mod object; diff --git a/src/scene/model/surface_sources.rs b/src/scene/model/surface_sources.rs new file mode 100644 index 000000000..64945a93a --- /dev/null +++ b/src/scene/model/surface_sources.rs @@ -0,0 +1,121 @@ +//! Associative extruded, revolved and lofted surfaces (SURFACEASSOCIATIVITY): +//! the sources and values each surface was made from, kept with the surface +//! so it follows edits of its sources and the drawing's associative network +//! describes it on save. + +use codec::xdata::XDataValue; +use codec::{EntityType, Handle}; +use glam::DVec3; + +/// The application name under which a surface keeps its sources. +pub const SURFACE_SOURCES_APP: &str = "OCS_SURFACE_SOURCES"; + +/// What an associative surface was made from. +#[derive(Clone, Debug, PartialEq)] +pub enum SurfaceSources { + /// EXTRUDE: the profile, the height along its normal (or the length of a + /// given direction) and the taper angle. + Extrude { profile: Handle, height: f64, direction: Option, taper: f64 }, + /// REVOLVE: the profile, the axis (an axis object or two points), the + /// angle and the start angle. + Revolve { profile: Handle, axis: Option, start: DVec3, end: DVec3, angle: f64, start_angle: f64 }, + /// LOFT: the cross sections in order. + Loft { sections: Vec }, +} + +impl SurfaceSources { + /// Every entity the surface reads. + pub fn handles(&self) -> Vec { + match self { + SurfaceSources::Extrude { profile, .. } => vec![*profile], + SurfaceSources::Revolve { profile, axis, .. } => std::iter::once(*profile).chain(*axis).collect(), + SurfaceSources::Loft { sections } => sections.clone(), + } + } +} + +/// Keep the sources with the surface. +pub fn link_surface_sources(document: &mut codec::CadDocument, surface: Handle, sources: &SurfaceSources) { + if surface.is_null() { + return; + } + let point = |p: DVec3| XDataValue::Point3D(codec::types::Vector3::new(p.x, p.y, p.z)); + let values = match sources { + SurfaceSources::Extrude { profile, height, direction, taper } => { + let mut values = vec![ + XDataValue::String("EXTRUDE".into()), + XDataValue::Handle(*profile), + XDataValue::Real(*height), + XDataValue::Real(*taper), + ]; + values.extend(direction.map(point)); + values + } + SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => { + let mut values = vec![ + XDataValue::String("REVOLVE".into()), + XDataValue::Handle(*profile), + point(*start), + point(*end), + XDataValue::Real(*angle), + XDataValue::Real(*start_angle), + ]; + values.extend(axis.map(XDataValue::Handle)); + values + } + SurfaceSources::Loft { sections } => std::iter::once(XDataValue::String("LOFT".into())) + .chain(sections.iter().map(|handle| XDataValue::Handle(*handle))) + .collect(), + }; + crate::scene::view::dispatch::set_entity_xdata(document, surface, SURFACE_SOURCES_APP, Some(values)); +} + +/// The sources an associative surface keeps. +pub fn surface_sources(entity: &EntityType) -> Option { + let record = entity + .common() + .extended_data + .records() + .iter() + .find(|record| record.application_name == SURFACE_SOURCES_APP)?; + let mut kind = None; + let (mut handles, mut reals, mut points) = (Vec::new(), Vec::new(), Vec::new()); + for value in &record.values { + match value { + XDataValue::String(text) if kind.is_none() => kind = Some(text.as_str()), + XDataValue::Handle(handle) => handles.push(*handle), + XDataValue::Real(value) => reals.push(*value), + XDataValue::Point3D(p) => points.push(DVec3::new(p.x, p.y, p.z)), + _ => {} + } + } + match kind? { + "EXTRUDE" => Some(SurfaceSources::Extrude { + profile: *handles.first()?, + height: *reals.first()?, + taper: *reals.get(1)?, + direction: points.first().copied(), + }), + "REVOLVE" => Some(SurfaceSources::Revolve { + profile: *handles.first()?, + axis: handles.get(1).copied(), + start: *points.first()?, + end: *points.get(1)?, + angle: *reals.first()?, + start_angle: *reals.get(1)?, + }), + "LOFT" => (handles.len() >= 2).then_some(SurfaceSources::Loft { sections: handles }), + _ => None, + } +} + +/// The two ends of an axis object (a line). +pub fn axis_points(entity: &EntityType) -> Option<(DVec3, DVec3)> { + match entity { + EntityType::Line(line) => Some(( + DVec3::new(line.start.x, line.start.y, line.start.z), + DVec3::new(line.end.x, line.end.y, line.end.z), + )), + _ => None, + } +} diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs index 5d68a3994..6847c980c 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -525,6 +525,60 @@ pub fn assoc_edge_curve(entity: &EntityType) -> Option<(i32, codec::objects::Ass } Some((47, K::None, values)) } + // Ellipse (17): centre, unit major axis, unit minor axis (the normal + // across the major one), major and minor radius, start and end + // parameter, 0. + EntityType::Ellipse(ellipse) => { + let major = glam::DVec3::new(ellipse.major_axis.x, ellipse.major_axis.y, ellipse.major_axis.z); + let normal = glam::DVec3::new(ellipse.normal.x, ellipse.normal.y, ellipse.normal.z).try_normalize()?; + let radius = major.length(); + let axis = major.try_normalize()?; + Some((17, K::Ellipse, vec![ + point(glam::DVec3::new(ellipse.center.x, ellipse.center.y, ellipse.center.z)), + point(axis), + point(normal.cross(axis)), + V::Real(radius), + V::Real(radius * ellipse.minor_axis_ratio), + V::Real(ellipse.start_parameter), + V::Real(ellipse.end_parameter), + V::Real(0.0), + ])) + } + // NURBS (42): rational flag, a second flag (0), degree, knot + // tolerance, then knots, weights (empty unless rational) and control + // points, each as its length, the length reserved (at least 8) and the + // growth step 8, followed by the items. + EntityType::Spline(spline) => { + // A fit-point spline as the curve through its points, its knots + // in the fit parameter (chord length) as the reference keeps them. + let curve = crate::entities::spline::nurbs3(spline)?; + let mut knots = curve.knots().to_vec(); + if crate::entities::spline::uses_fit_method(spline) && spline.knot_parameterization == 0 { + let chord: f64 = spline.fit_points.windows(2) + .map(|pair| ((pair[1].x - pair[0].x).powi(2) + (pair[1].y - pair[0].y).powi(2) + (pair[1].z - pair[0].z).powi(2)).sqrt()) + .sum(); + let (low, high) = (knots[0], knots[knots.len() - 1]); + if high - low > 0.0 && chord > 0.0 && (high - low - chord).abs() > 1e-9 * chord { + knots.iter_mut().for_each(|knot| *knot = (*knot - low) / (high - low) * chord); + } + } + let points = curve.control_points(); + if points.len() < 2 || knots.is_empty() { return None; } + let rational = curve.weights().iter().any(|weight| (weight - 1.0).abs() > 1e-12); + let header = |values: &mut Vec, count: usize| { + values.extend([V::Int(count as i32), V::Int(if count == 0 { 0 } else { count.max(8) } as i32), V::Int(8)]); + }; + let tolerance = if spline.knot_tolerance > 0.0 { spline.knot_tolerance } else { 1e-9 }; + let mut values = vec![V::Bool(rational), V::Bool(false), V::Int(curve.degree() as i32), V::Real(tolerance)]; + header(&mut values, knots.len()); + values.extend(knots.iter().map(|knot| V::Real(*knot))); + let weights = if rational { curve.weights() } else { &[] }; + header(&mut values, weights.len()); + values.extend(weights.iter().map(|weight| V::Real(*weight))); + header(&mut values, points.len()); + values.extend(points.iter().map(|p| V::Point(P::new(p[0], p[1], p[2])))); + Some((42, K::Nurb3d, values)) + } _ => None, } } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index dbde57ece..a10984fc2 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1178,6 +1178,98 @@ impl Scene { rebuilt } + /// Associative extruded, revolved and lofted surfaces follow edits of + /// their sources: made again from them with the values they were made + /// with. A surface edited along with its sources keeps its own edit. + /// Returns the surfaces rebuilt. + pub(crate) fn refresh_associative_surfaces(&mut self, changes: &[(Handle, crate::scene::ChangeKind)]) -> Vec<(Handle, crate::scene::ChangeKind)> { + use crate::scene::model::surface_sources::{axis_points, surface_sources, SurfaceSources, SURFACE_SOURCES_APP}; + use crate::scene::model::sweep_model; + use codec::objects::SolidHistoryOperation; + let changed = changes.iter() + .filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed)) + .map(|(handle, _)| *handle) + .collect::>(); + if changed.is_empty() || !self.document.app_ids.contains(SURFACE_SOURCES_APP) { + return Vec::new(); + } + let linked = self.document.entities() + .filter(|entity| matches!(entity, EntityType::Surface(_))) + .filter_map(|entity| { + let handle = entity.common().handle; + let sources = surface_sources(entity)?; + (!changed.contains(&handle) && sources.handles().iter().any(|source| changed.contains(source))) + .then_some((handle, sources)) + }) + .collect::>(); + let mut rebuilt = Vec::new(); + for (handle, sources) in linked { + let source = |handle: Handle| self.document.get_entity(handle).cloned(); + // The surface made again: a history step, or a body with its data. + let made: Option> = match &sources { + SurfaceSources::Extrude { profile, height, direction, taper } => (|| { + let entity = source(*profile)?; + let (shape, _) = sweep_model::extrusion_profile_of(&entity)?; + let direction = match direction { + Some(direction) => *direction, + None => glam::DVec3::from_array(shape.plane.normal()?) * *height, + }; + if self.document.solid_history_operations(handle).is_some() { + return sweep_model::extrusion_history(&entity, None, direction.to_array(), *taper, shape.plane.origin).map(Ok); + } + let body = sweep_model::extruded_surface(&entity, direction.to_array(), *taper)?; + let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_extruded_surface(direction, *taper) else { return None }; + Some(Err((body, fresh.surface_data))) + })(), + SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => (|| { + let entity = source(*profile)?; + let (start, end) = axis + .and_then(|axis| self.document.get_entity(axis).and_then(axis_points)) + .unwrap_or((*start, *end)); + let body = sweep_model::revolved_surface(&entity, start.to_array(), end.to_array(), *angle, *start_angle)?; + let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_revolved_surface(&entity, start, end, *angle, *start_angle) else { return None }; + Some(Err((body, fresh.surface_data))) + })(), + SurfaceSources::Loft { sections } => (|| { + let operations = self.document.solid_history_operations(handle)?; + let mut record = operations.into_iter().find_map(|operation| match operation { + SolidHistoryOperation::Loft(record) => Some(record), + _ => None, + })?; + if record.cross_sections.len() != sections.len() { + return None; + } + record.cross_sections = sections.iter() + .map(|section| source(*section).as_ref().and_then(crate::scene::model::loft_command_model::embedded_section)) + .collect::>>()?; + Some(Ok(SolidHistoryOperation::Loft(record))) + })(), + }; + let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new); + match made { + Some(Ok(operation)) => { + self.record_undo_before(handle, before); + if self.rebuild_solid_history(handle, operation) { + rebuilt.push((handle, crate::scene::ChangeKind::Modified)); + } + } + Some(Err((body, data))) => { + 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 }; + 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 = data; + } + self.register_prepared_solid_model(handle, body, display); + rebuilt.push((handle, crate::scene::ChangeKind::Modified)); + } + None => {} + } + } + 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. @@ -1189,12 +1281,57 @@ impl Scene { }; let mut links = Vec::new(); let mut sources = Vec::new(); + let mut others = 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 }; + // The entities a named path parameter reads (an edge of a region + // reads through a geometry dependency). + let read = |name: &str| -> Vec { + action.owned_parameters.iter().filter_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), + Some(AssociativeData::GeomDependency(dependency)) => Some(dependency.dependency.dependent_on), + _ => None, + } + }).collect() + }; + let real = |name: &str| action.values.iter().find(|value| value.name == name) + .and_then(|value| value.variables.first()) + .and_then(|variable| match variable.value.value { + codec::objects::AssocEvalValue::Real(value) => Some(value), + _ => None, + }); + match body.kind { + AssocSurfaceActionKind::Extruded => { + if let (Some(profile), Some(height), Some(taper)) = + (read("ExtrudeProfile").first().copied(), real("ExtrusionHeight"), real("ExtrusionTaperAngle")) + { + others.push((written.dependent_on, (body.kind, vec![profile], None, height, taper))); + } + continue; + } + AssocSurfaceActionKind::Revolved => { + if let (Some(profile), Some(angle)) = (read("RevolveProfile").first().copied(), real("RevolveAngle")) { + others.push((written.dependent_on, (body.kind, vec![profile], read("RevolvePath").first().copied(), angle, 0.0))); + } + continue; + } + AssocSurfaceActionKind::Lofted => { + let sections = read("CrossSection"); + if sections.len() >= 2 { + others.push((written.dependent_on, (body.kind, sections, None, 0.0, 0.0))); + } + continue; + } + AssocSurfaceActionKind::Swept => {} + _ => 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 }; @@ -1210,18 +1347,7 @@ impl Scene { 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")) { + if let (Some(profile), Some(path)) = (read("SweepProfile").first().copied(), read("SweepPath").first().copied()) { sources.push((written.dependent_on, profile, path)); } } @@ -1241,6 +1367,44 @@ impl Scene { if crate::scene::model::sweep_model::sweep_expressions(entity).is_some() { continue; } crate::scene::model::sweep_model::link_sweep_expressions(&mut self.document, handle, &expressions); } + // Associative extruded, revolved and lofted surfaces follow their + // sources here too. + use crate::scene::model::surface_sources::{link_surface_sources, surface_sources, SurfaceSources}; + for (surface, (kind, handles, axis, amount, taper)) in others { + let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue }; + if surface_sources(entity).is_some() { continue; } + let linked = match (kind, &value.surface_data) { + (AssocSurfaceActionKind::Extruded, codec::entities::SurfaceData::Extruded { sweep_vector, .. }) => { + let profile = handles[0]; + let along = glam::DVec3::new(sweep_vector.x, sweep_vector.y, sweep_vector.z); + // A direction other than the profile's normal is kept. + let normal = self.document.get_entity(profile) + .and_then(|entity| crate::scene::model::sweep_model::extrusion_profile_of(entity)) + .and_then(|(shape, _)| shape.plane.normal()) + .map(glam::DVec3::from_array); + let direction = match normal { + Some(normal) if (normal * amount - along).length() <= 1e-9 * amount.abs().max(1.0) => None, + _ if along.length() > 1e-12 => Some(along), + _ => None, + }; + SurfaceSources::Extrude { profile, height: amount, direction, taper } + } + (AssocSurfaceActionKind::Revolved, codec::entities::SurfaceData::Revolved { axis_point, axis_vector, start_angle, .. }) => { + let start = glam::DVec3::new(axis_point.x, axis_point.y, axis_point.z); + SurfaceSources::Revolve { + profile: handles[0], + axis, + start, + end: start + glam::DVec3::new(axis_vector.x, axis_vector.y, axis_vector.z), + angle: amount, + start_angle: *start_angle, + } + } + (AssocSurfaceActionKind::Lofted, _) => SurfaceSources::Loft { sections: handles }, + _ => continue, + }; + link_surface_sources(&mut self.document, surface, &linked); + } } pub fn rebuild_solid_history(