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(