diff --git a/src/app/command_driver/solid3d.rs b/src/app/command_driver/solid3d.rs index 758f7d864..708a79459 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 { @@ -112,10 +113,27 @@ impl OpenCADStudio { entity.surface_data = data; } } - match history { - Some(history) => self.add_surface_model_with_history(surface, body, 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 @@ -139,7 +157,7 @@ impl OpenCADStudio { failed += 1; } else { created_handles.push(created); - if delete_sources { + if delete_sources && !associative_sources.contains(handle) { consumed.push(*handle); } } @@ -310,6 +328,7 @@ impl OpenCADStudio { handles, axis_start, axis_end, + axis_object, angle, start_angle, mode, @@ -346,6 +365,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() else { @@ -364,10 +384,34 @@ impl OpenCADStudio { failed += 1; continue; }; - self.add_surface_model( - empty_revolved_surface(&entity, axis_start, axis_end, angle, start_angle), + let created = self.add_surface_model( + empty_revolved_surface( + &entity, + axis_start, + axis_end, + angle, + 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) .and_then(|history| { @@ -391,7 +435,7 @@ impl OpenCADStudio { failed += 1; } else { created_handles.push(created); - if delete_sources { + if delete_sources && !associative_sources.contains(handle) { consumed.push(*handle); } } @@ -688,6 +732,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), body); @@ -696,6 +741,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 { @@ -713,7 +779,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 a03e3e77f..788286362 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2094,6 +2094,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 580036e78..3e3d1ab1b 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -1131,6 +1131,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, @@ -1183,6 +1184,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(), @@ -1319,6 +1321,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 } @@ -1337,6 +1340,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, @@ -1430,12 +1434,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 } @@ -1446,6 +1451,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 d7ee880ac..d28c28c6e 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -2652,19 +2652,396 @@ fn pending_swept_surfaces(document: &CadDocument) -> Vec<(Handle, Handle, Handle 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)) + && document.get_entity(profile).is_some_and(|e| sweep_model::assoc_source_supported(e)) + && document.get_entity(path).is_some_and(|e| sweep_model::assoc_source_supported(e))) + .then_some((surface, profile, path)) }) .collect::>(); pending.sort_by_key(|(surface, _, _)| surface.value()); 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_source_supported(e)) + })) + .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() +} + +/// Named-objects dictionary key of the drawing's persistent subentity manager. +const SUBENT_MANAGER_KEY: &str = "ACAD_ASSOCPERSSUBENTMANAGER"; + +/// A read dependency of an action on `dependent_on`. +fn read_dependency(dependent_on: Handle, id: i32) -> AssocDependency { + AssocDependency { + class_version: 2, + status: 0, + is_read_dependency: true, + is_write_dependency: false, + is_attached_to_object: true, + is_delegating_to_owning_action: true, + order: 0, + dependent_on, + name: None, + read_dependency: Handle::NULL, + node: Handle::NULL, + dependency_body: Handle::NULL, + dependency_body_id: id, + } +} + +/// An edge action parameter reading `curve` through dependency `read`. +fn edge_action_param(read: Handle, (action_type, subcurve_kind, curve): crate::scene::model::sweep_model::AssocCurve) -> AssociativeData { + use codec::objects::{AssocActionParam, AssocEdgeActionParam, AssocSingleDependencyActionParam}; + 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, + }) +} + +/// The subentity manager's record of a region's edges: one entry per edge +/// id from `first_id`, in the order the region's dependencies read them, +/// each naming the edge (and, between edges, its two vertices) by the +/// region's face and edge numbers. `loops` holds each loop's edge count. +fn region_subent_entries(loops: &[usize], step: i32, first_id: i32) -> Vec { + const BODY: i32 = 1_000_000_000; + const FACE: i32 = 1001; + let mut values = Vec::new(); + let (mut id, mut offset) = (first_id, 0usize); + for &count in loops { + let edge = |index: usize| 51001 + 100_000 * (offset + index % count) as i32; + let vertex = |a: i32, b: i32| [1, 3, 1, BODY, FACE, 1, BODY, a, 1, BODY, b, 0, 0, 0, 900, 0, 900, 0, 0]; + for read in 0..count { + if count == 1 { + values.extend([4, 0, 5, id, step, step, 0, 0, 24, 0, 0, 1, BODY, edge(0), 0, 0, 1, 1, BODY, FACE]); + values.extend([0; 14]); + } else { + // The dependencies start at the loop's second edge. + let index = read + 1; + values.extend([4, 0, 11, id, step, step, 0, 2, 2, 2, 0, 2, 2, 2, 54, 0, 0, 1, BODY, edge(index)]); + values.extend([0, 0, 1, 1, BODY, FACE, 0, 0, 0, 0]); + values.extend(vertex(edge(index), edge(index + count - 1))); + values.extend([0, 0]); + values.extend(vertex(edge(index + 1), edge(index))); + } + id += 1; + } + offset += count; + } + values +} + +/// Records a region read by an action in the drawing's persistent +/// subentity manager (made when the drawing has none): a step for the +/// action and one for the region, then one id per edge. The manager keeps +/// the last id given, its edge entries and the list of every id given. +/// Returns the edges' ids, or `None` when the drawing's manager has a form +/// this cannot extend. +fn register_region_subents(document: &mut CadDocument, loops: &[usize]) -> Option> { + use codec::objects::AssocPersSubentManager; + let root = document.header.named_objects_dict_handle; + let existing = match document.objects.get(&root) { + Some(ObjectType::Dictionary(dictionary)) => dictionary.get(SUBENT_MANAGER_KEY), + _ => return None, + }; + let current = existing.and_then(|handle| match document.objects.get(&handle) { + Some(ObjectType::Associative(AssociativeObject { data: AssociativeData::PersSubentManager(manager), .. })) => { + Some((handle, manager.values.clone())) + } + _ => None, + }); + let (handle, last, count, mut entries) = match current { + Some((handle, values)) => { + let last = *values.first()?; + if values.len() == 4 && values[1..].iter().all(|value| *value == 0) { + (handle, last, 0, Vec::new()) + } else { + let tail = usize::try_from(last).ok()? + 3; + if values.len() < tail + 2 || values[values.len() - tail] != last { + return None; + } + (handle, last, values[1], values[2..values.len() - tail].to_vec()) + } + } + None => { + let handle = document.allocate_handle(); + set_dictionary_entry(document, root, SUBENT_MANAGER_KEY, handle); + (handle, 0, 0, Vec::new()) + } + }; + let edges = loops.iter().sum::() as i32; + let first = last + 3; + entries.extend(region_subent_entries(loops, last + 2, first)); + let last = last + 2 + edges; + let mut values = vec![last, count + edges]; + values.extend(entries); + values.push(last); + values.extend(1..=last); + values.extend([0, 0]); + document.objects.insert(handle, ObjectType::Associative(AssociativeObject { + handle, + owner: root, + reactors: vec![root], + dxf_name: "ACDBASSOCPERSSUBENTMANAGER".to_string(), + cpp_class_name: "AcDbAssocPersSubentManager".to_string(), + data: AssociativeData::PersSubentManager(AssocPersSubentManager { + class_version: 2, + markers: [3, 0, 2], + values, + final_flag: false, + }), + ..Default::default() + })); + Some((first..first + edges).collect()) +} + +/// How an action reads one source through `parameter`: one read dependency +/// on a curve; on a region one geometry dependency per edge, naming the +/// edge by its persistent subentity id and chained on the region. Adds the +/// dependencies (owned by `action`) and edge parameters to `objects` with +/// ids from `first_id`; returns (edge parameter, dependency) pairs. +fn source_reads( + document: &mut CadDocument, + action: Handle, + parameter: Handle, + source: Handle, + first_id: i32, + objects: &mut Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)>, +) -> Option> { + use crate::scene::model::sweep_model; + let entity = document.get_entity(source)?; + let Some(loops) = sweep_model::region_edge_curves(entity) else { + let curve = sweep_model::assoc_edge_curve(entity)?; + let (edge, read) = (document.allocate_handle(), document.allocate_handle()); + objects.push((edge, parameter, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_action_param(read, curve))); + objects.push((read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", + AssociativeData::Dependency(read_dependency(source, first_id)))); + return Some(vec![(edge, read)]); + }; + let sizes = loops.iter().map(Vec::len).collect::>(); + let ids = register_region_subents(document, &sizes)?; + // The region carries the step its edge names belong to; without it (and + // without named edges in its modeler data) the reference cannot find + // the edges again. + crate::scene::view::dispatch::set_entity_xdata(document, source, "ACAD_STEPID", + Some(vec![codec::xdata::XDataValue::Integer32(ids.first()? - 1)])); + let curves = loops.into_iter().flatten().collect::>(); + let pairs = curves.iter().map(|_| (document.allocate_handle(), document.allocate_handle())).collect::>(); + for (index, (curve, (edge, read))) in curves.into_iter().zip(pairs.iter().copied()).enumerate() { + let mut dependency = read_dependency(source, first_id + index as i32); + dependency.read_dependency = index.checked_sub(1).map_or(Handle::NULL, |previous| pairs[previous].1); + dependency.node = pairs.get(index + 1).map_or(Handle::NULL, |next| next.1); + objects.push((edge, parameter, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_action_param(read, curve))); + objects.push((read, action, "ACDBASSOCGEOMDEPENDENCY", "AcDbAssocGeomDependency", + AssociativeData::GeomDependency(AssocGeomDependency { + dependency, + class_version: 0, + enabled: true, + persistent_subent: AssocPersistentSubentId { + class_name: "AcDbAssocAsmBasedEntityPersSubentId".to_string(), + dependent_on_compound_object: false, + class_code: 5, + values: vec![0, 2, ids[index]], + leading_flag: false, + }, + }))); + } + Some(pairs) +} + +/// 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 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", + // One value per cross section: the start and end + // settings at the ends, the defaults between them. + vec![ + ("Continuity", 0, (0..sections.len()).map(|index| short(if index == 0 { continuity.0 } else if index + 1 == sections.len() { continuity.1 } else { 1 })).collect(), false), + ("Bulge", 0, (0..sections.len()).map(|index| real(if index == 0 { bulge.0 } else if index + 1 == sections.len() { bulge.1 } else { 0.5 })).collect(), false), + ], + sections.iter().map(|section| ("CrossSection", Some(*section))) + .chain(std::iter::once(("PathCurve", None))) + .collect(), + ) + } + }; + let document = &mut *allocator.document; + let (action, body, written) = (document.allocate_handle(), document.allocate_handle(), document.allocate_handle()); + let mut objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> = Vec::new(); + let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)]; + let mut dependencies = vec![written]; + let mut owned = Vec::new(); + let mut writes = read_dependency(surface, 1); + (writes.is_read_dependency, writes.is_write_dependency, writes.order) = (false, true, i32::MIN); + objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(writes))); + let mut complete = true; + for (name, source) in params.iter() { + let parameter = document.allocate_handle(); + let mut children = Vec::new(); + if let Some(source) = source { + let Some(reads) = source_reads(document, action, parameter, *source, dependencies.len() as i32 + 1, &mut objects) else { + complete = false; + break; + }; + for (edge, read) in reads { + 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); + } + if !complete { + continue; + } + 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(); @@ -2717,16 +3094,6 @@ fn materialize_swept_surface_actions( 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()?; @@ -2737,27 +3104,7 @@ fn materialize_swept_surface_actions( }; 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 (action, body, written, profile_param, path_param) = (next(), next(), next(), next(), next()); let dependency = |dependent_on: Handle, read: bool, id: i32, order: i32| AssocDependency { class_version: 2, status: 0, @@ -2773,13 +3120,15 @@ fn materialize_swept_surface_actions( 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 objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> = Vec::new(); + // The profile (a region by its edges) and the path, read in that order. + let Some(profile_reads) = source_reads(document, action, profile_param, profile, 2, &mut objects) else { continue }; + let Some(path_reads) = source_reads(document, action, path_param, path, profile_reads.len() as i32 + 2, &mut objects) else { continue }; + let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)]; + reactors.extend(profile_reads.iter().map(|(_, read)| (profile, *read))); + reactors.extend(path_reads.iter().map(|(_, read)| (path, *read))); + let mut dependencies = vec![written]; + dependencies.extend(profile_reads.iter().chain(&path_reads).map(|(_, read)| *read)); let mut value_handles = [Handle::NULL, Handle::NULL]; let mut network_actions = vec![action]; // Scale (slot 0) and twist (slot 1) expressions. @@ -2916,94 +3265,22 @@ fn materialize_swept_surface_actions( 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)), - )); + }))); + objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN)))); + let path_parameter = |name: &str, children: Vec| 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, + }); + objects.push((profile_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam", + path_parameter("SweepProfile", profile_reads.iter().map(|(edge, _)| *edge).collect()))); + objects.push((path_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam", + path_parameter("SweepPath", path_reads.iter().map(|(edge, _)| *edge).collect()))); for (handle, owner, dxf, class, data) in objects { allocator.insert_associative_at(handle, owner, dxf, class, data); } @@ -3180,7 +3457,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; } @@ -3365,18 +3642,14 @@ 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 pending = if materialize_all_parameters { pending_swept_surfaces(allocator.document) } else { Vec::new() }; + 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 { @@ -3537,8 +3810,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 7a8343ecd..46921453f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3732,6 +3732,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 ec86fc686..55c7e27be 100644 --- a/src/scene/model/loft_command_model.rs +++ b/src/scene/model/loft_command_model.rs @@ -7,9 +7,11 @@ use codec::objects::{SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryN use codec::{EntityType, Handle}; use kernel::brep::Body; -fn embedded_section(entity: &EntityType) -> Option { +pub(crate) fn embedded_section(entity: &EntityType) -> Option { match entity { - EntityType::Region(region) => Some(EmbeddedEntity::Region(region.clone())), + // A region as the curve round it, as the reference records it. + EntityType::Region(region) => Some(super::sweep_model::region_boundary_entity(entity) + .map_or_else(|| EmbeddedEntity::Region(region.clone()), |(curve, _)| curve)), EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())), _ => embedded_path(entity), } diff --git a/src/scene/model/mod.rs b/src/scene/model/mod.rs index e2078f10a..57c88c901 100644 --- a/src/scene/model/mod.rs +++ b/src/scene/model/mod.rs @@ -15,6 +15,7 @@ pub mod point_cloud; pub mod presspull_model; pub mod solid_history; pub mod solid_model; +pub mod surface_sources; mod sweep_command_model; pub mod sweep_model; pub mod underlay_vector; diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 1961733e3..dff89da22 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -850,8 +850,11 @@ pub fn extrusion_surface_data(value: &SolidHistorySweep) -> 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 51c3bc356..f159b6906 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -14,13 +14,11 @@ fn embedded_sweep_profile( entity: &EntityType, ) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> { match entity { - EntityType::Region(region) => Some(( - codec::entities::EmbeddedEntity::Region(region.clone()), - glam::DMat4::IDENTITY.to_cols_array(), - )), - EntityType::Polyline3D(value) => { - Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array())) - } + // A region as the curve round it, as the reference records it. + EntityType::Region(region) => super::sweep_model::region_boundary_entity(entity).or_else(|| { + Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array())) + }), + EntityType::Polyline3D(value) => Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array())), _ => embedded_revolve_profile(entity), } } @@ -580,13 +578,7 @@ pub fn resweep_from_sources( /// 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, -)> { +pub fn assoc_edge_curve(entity: &EntityType) -> Option { 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)); @@ -729,17 +721,154 @@ pub fn assoc_edge_curve( } 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 tolerance = if spline.knot_tolerance > 0.0 { spline.knot_tolerance } else { 1e-9 }; + let points = points.iter().map(|p| glam::DVec3::from_array(*p)).collect::>(); + Some(nurbs_curve(curve.degree(), tolerance, &knots, curve.weights(), &points)) + } _ => 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 +/// A NURBS curve's edge parameter values (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. +fn nurbs_curve(degree: usize, tolerance: f64, knots: &[f64], weights: &[f64], points: &[glam::DVec3]) -> AssocCurve { + use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K}; + let rational = 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 mut values = vec![V::Bool(rational), V::Bool(false), V::Int(degree as i32), V::Real(tolerance)]; + header(&mut values, knots.len()); + values.extend(knots.iter().map(|knot| V::Real(*knot))); + let weights = if rational { 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(codec::types::Vector3::new(p.x, p.y, p.z)))); + (42, K::Nurb3d, values) +} + +/// An edge parameter's curve: kind, subcurve kind and values. +pub type AssocCurve = (i32, codec::objects::AssocSubcurveKind, Vec); + +/// A region's edges as an associative action reads them, loop by loop, each +/// loop from its second edge on as the reference lists them: lines (23), +/// circles and arcs (11) measured from the arbitrary axis of the region's +/// normal, elliptical arcs (17) and NURBS curves (42). +pub fn region_edge_curves(entity: &EntityType) -> Option>> { + use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K}; + use kernel::geom2d::Curve; + let EntityType::Region(_) = entity else { return None }; + let Some((plane, loops, true)) = super::presspull_model::profile_geometry(entity) else { return None }; + let at = |uv: [f64; 2]| glam::DVec3::from_array(plane.point_at(uv)); + let normal = glam::DVec3::from_array(plane.x_axis).cross(glam::DVec3::from_array(plane.y_axis)).try_normalize()?; + let axis = arbitrary_x(normal); + let across = normal.cross(axis); + let point = |p: glam::DVec3| V::Point(codec::types::Vector3::new(p.x, p.y, p.z)); + let arc = |centre: glam::DVec3, radius: f64, start: f64, end: f64| (11, K::Arc, vec![ + point(centre), point(normal), point(axis), V::Real(radius), V::Real(start), V::Real(end), V::Real(0.0), + ]); + let curve = |piece: &Curve| -> Option { + match piece { + Curve::Line(line) => { + let (a, b) = (at(line.start), at(line.end)); + Some((23, K::LineSegment3d, vec![point(a), point(b - a)])) + } + Curve::Circle(circle) => Some(arc(at(circle.centre), circle.radius, 0.0, std::f64::consts::TAU)), + Curve::Arc(value) => { + let centre = at(value.centre); + let offset = at([ + value.centre[0] + value.start_angle.cos(), + value.centre[1] + value.start_angle.sin(), + ]) - centre; + let start = offset.dot(across).atan2(offset.dot(axis)); + Some(arc(centre, value.radius, start, start + value.sweep())) + } + Curve::Ellipse(value) => { + let ellipse = value.ellipse; + let centre = at(ellipse.centre); + let tip = [ellipse.centre[0] + ellipse.major_axis[0], ellipse.centre[1] + ellipse.major_axis[1]]; + let major = (at(tip) - centre).try_normalize()?; + let end = if value.end_parameter > value.start_parameter { + value.end_parameter + } else { + value.end_parameter + std::f64::consts::TAU + }; + Some((17, K::Ellipse, vec![ + point(centre), point(major), point(normal.cross(major)), + V::Real(ellipse.major_radius), V::Real(ellipse.minor_radius), + V::Real(value.start_parameter), V::Real(end), V::Real(0.0), + ])) + } + Curve::Nurbs(value) => { + let points = value.control_points().iter().map(|p| at(*p)).collect::>(); + Some(nurbs_curve(value.degree(), 1e-9, value.knots(), value.weights(), &points)) + } + _ => None, + } + }; + loops + .iter() + .map(|pieces| { + let mut curves = pieces.iter().map(curve).collect::>>()?; + if curves.len() > 1 { + curves.rotate_left(1); + } + Some(curves) + }) + .collect() +} + +/// Whether an associative action can read this source: a region's edges +/// or the entity's own curve. +pub fn assoc_source_supported(entity: &EntityType) -> bool { + region_edge_curves(entity).is_some() || assoc_edge_curve(entity).is_some() +} + +/// The arbitrary-axis X direction of an entity normal. +pub(crate) 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() } diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 43692ac79..9d69b6ae1 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -12,11 +12,7 @@ use kernel::geom2d::{Arc, Curve, EllipseArc, Line}; use kernel::space::{PlanarCurve, Plane, Vec3}; pub use super::sweep_command_model::{ - assoc_edge_curve, is_sweep_path, is_sweep_profile, link_sweep_expressions, link_sweep_sources, - resweep_from_sources, scaled_sweep_path_refusal, set_surface_associativity, spatial_profile, - spatial_sweep_record, surface_associativity, surface_sweep_record, sweep_expressions, - sweep_record, sweep_refusal_code, sweep_selection_options, sweep_sources, swept_surface_entity, - swept_with_options, SWEEP_SOURCES_APP, + 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, SWEEP_SOURCES_APP, resweep_from_sources, surface_associativity, set_surface_associativity, assoc_edge_curve, assoc_source_supported, region_edge_curves, AssocCurve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options, }; use crate::entities::curve::entity_curve; @@ -36,6 +32,9 @@ pub struct Profile { /// is handed over as the arcs it is made of rather than as one curve with /// nowhere for the sweep to start. pub fn profile_of(entity: &EntityType) -> Option { + if let Some(profile) = region_loop(entity) { + return (profile.pieces.len() >= 3).then_some(profile); + } let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?; if !planar.curve.is_closed() { return None; @@ -50,6 +49,9 @@ pub fn profile_of(entity: &EntityType) -> Option { /// An entity-level planar curve suitable for either a solid profile or an /// open surface profile. pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> { + if let Some(profile) = region_loop(entity) { + return Some((profile, true)); + } let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?; let closed = planar.curve.is_closed(); let pieces = profile_pieces(&planar.curve, closed)?; @@ -62,6 +64,30 @@ pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> { )) } +/// A region of one boundary loop as that loop, read from the region's exact +/// edges (its display wires hold one wire per edge), a lone closed edge split +/// as a closed curve is. +fn region_loop(entity: &EntityType) -> Option { + let EntityType::Region(region) = entity else { return None }; + if !region.acis_data.has_data() { + return None; + } + let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?; + let [boundary] = loops.as_slice() else { return None }; + let single = boundary.len() == 1; + let pieces = boundary + .iter() + .map(|piece| profile_pieces(piece, single)) + .collect::>>()? + .concat(); + Some(Profile { plane, pieces }) +} + +/// Whether a region has more than one boundary loop. +fn region_with_holes(entity: &EntityType) -> bool { + matches!(entity, EntityType::Region(_)) && region_profiles(entity).is_some_and(|(_, loops)| loops.len() > 1) +} + fn profile_pieces(curve: &Curve, closed: bool) -> Option> { let pieces = match curve { Curve::Polyline(_) => curve.segments(), @@ -164,10 +190,16 @@ fn planar_polygon_entity(entity: &EntityType) -> Option { )) } +/// A region's boundary loops: from its modeler edges when it has them (its +/// display wires then hold one wire per edge), else from its wires. fn region_profiles(entity: &EntityType) -> Option<(Plane, Vec>)> { let EntityType::Region(region) = entity else { return None; }; + if region.acis_data.has_data() { + let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?; + return (!loops.is_empty()).then_some((plane, loops)); + } let plane = planar_polygon_entity(entity)?.plane; let mut profiles = Vec::with_capacity(region.wires.len()); for wire in ®ion.wires { @@ -295,7 +327,7 @@ pub fn extruded_surface( direction: [f64; 3], taper_angle: f64, ) -> Option { - if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) { + if region_with_holes(entity) { return None; } let (profile, _) = extrusion_profile_of(entity)?; @@ -307,7 +339,7 @@ pub fn extruded_along_path( path: &EntityType, taper_angle: f64, ) -> Option { - if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) { + if region_with_holes(entity) { if taper_angle.abs() > 1e-12 { return None; } @@ -464,7 +496,7 @@ pub fn revolve_history( if !angle.is_finite() || angle.abs() <= 1e-12 || !start_angle.is_finite() - || matches!(entity, EntityType::Region(region) if region.wires.len() > 1) + || region_with_holes(entity) { return None; } @@ -627,10 +659,105 @@ pub fn embedded_path(entity: &EntityType) -> Option { } } -fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> { - if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) { +/// A region of one boundary loop as the curve the reference records for it +/// as a profile: a circle (one edge, or arcs of one circle making a whole +/// turn) or whole ellipse as itself, a loop of lines and arcs as a closed +/// polyline in its own coordinate system about the region's normal, read +/// from the region's modeler edges. +pub fn region_boundary_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> { + let EntityType::Region(region) = entity else { return None }; + if !region.acis_data.has_data() { return None; } + let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?; + let [boundary] = loops.as_slice() else { return None }; + let world = |v: [f64; 3]| Vector3::new(v[0], v[1], v[2]); + let normal = world(plane.normal()?); + let identity = glam::DMat4::IDENTITY.to_cols_array(); + match boundary.as_slice() { + [Curve::Circle(circle)] => { + let mut value = codec::entities::Circle::new(); + value.center = world(plane.point_at(circle.centre)); + value.radius = circle.radius; + value.normal = normal; + return Some((EmbeddedEntity::Circle(value), identity)); + } + [Curve::Arc(first), rest @ ..] + if rest.iter().all(|piece| matches!(piece, Curve::Arc(arc) + if (arc.centre[0] - first.centre[0]).hypot(arc.centre[1] - first.centre[1]) <= 1e-9 * (1.0 + first.radius) + && (arc.radius - first.radius).abs() <= 1e-9 * (1.0 + first.radius))) + && (boundary.iter().map(|piece| match piece { Curve::Arc(arc) => arc.sweep(), _ => 0.0 }).sum::() + - std::f64::consts::TAU).abs() <= 1e-9 => + { + let mut value = codec::entities::Circle::new(); + value.center = world(plane.point_at(first.centre)); + value.radius = first.radius; + value.normal = normal; + return Some((EmbeddedEntity::Circle(value), identity)); + } + [Curve::Ellipse(arc)] if (arc.sweep() - std::f64::consts::TAU).abs() <= 1e-9 => { + let ellipse = arc.ellipse; + let centre = Vec3::from(plane.point_at(ellipse.centre)); + let tip = Vec3::from(plane.point_at([ + ellipse.centre[0] + ellipse.major_axis[0] * ellipse.major_radius, + ellipse.centre[1] + ellipse.major_axis[1] * ellipse.major_radius, + ])); + let mut value = codec::entities::Ellipse::new(); + value.center = world(centre.to_array()); + value.major_axis = world((tip - centre).to_array()); + value.minor_axis_ratio = ellipse.minor_radius / ellipse.major_radius; + value.start_parameter = arc.start_parameter; + value.end_parameter = arc.start_parameter + std::f64::consts::TAU; + value.normal = normal; + return Some((EmbeddedEntity::Ellipse(value), identity)); + } + _ => {} + } + // Walk the loop head to tail; an arc run against its direction turns + // clockwise (negative bulge). + let close = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).hypot(a[1] - b[1]) <= 1e-9 * (1.0 + a[0].abs().max(a[1].abs())); + let ends = |piece: &Curve| match piece { + Curve::Line(line) => Some((line.start, line.end, 0.0)), + Curve::Arc(arc) => { + let at = |angle: f64| [arc.centre[0] + arc.radius * angle.cos(), arc.centre[1] + arc.radius * angle.sin()]; + Some((at(arc.start_angle), at(arc.end_angle), (arc.sweep() / 4.0).tan())) + } + _ => None, + }; + let (first_start, first_end, _) = ends(boundary.first()?)?; + let second = boundary.get(1).and_then(ends); + let mut at = if second.is_some_and(|(start, end, _)| close(first_start, start) || close(first_start, end)) { + first_end + } else { + first_start + }; + // The polyline in its object coordinates: the arbitrary axes of the normal. + let axis_normal = glam::DVec3::new(normal.x, normal.y, normal.z); + let axis_x = super::sweep_command_model::arbitrary_x(axis_normal); + let axis_y = axis_normal.cross(axis_x); + let mut polyline = LwPolyline::new(); + polyline.normal = normal; + polyline.elevation = glam::DVec3::from_array(plane.origin).dot(axis_normal); + for piece in boundary { + let (start, end, bulge) = ends(piece)?; + let (from, to, bulge) = if close(start, at) { (start, end, bulge) } else if close(end, at) { (end, start, -bulge) } else { return None }; + let point = glam::DVec3::from_array(plane.point_at(from)); + let mut vertex = LwVertex::new(Vector2::new(point.dot(axis_x), point.dot(axis_y))); + vertex.bulge = bulge; + polyline.vertices.push(vertex); + at = to; + } + polyline.is_closed = true; + Some((EmbeddedEntity::LwPolyline(polyline), identity)) +} + +fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> { + if region_with_holes(entity) { + return None; + } + if let Some(found) = region_boundary_entity(entity) { + return Some(found); + } if !matches!(entity, EntityType::Polyline3D(_)) { if let Some(embedded) = embedded_path(entity) { return Some((embedded, glam::DMat4::IDENTITY.to_cols_array())); diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 66267234f..63ebedc75 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1268,6 +1268,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. @@ -1279,23 +1371,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 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 }; + 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 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 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 Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency) - else { - continue; - }; let mut expressions: [Option; 2] = [None, None]; for value in &action.values { let slot = match value.name.as_str() { @@ -1325,28 +1451,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)); } } @@ -1387,6 +1492,44 @@ impl Scene { &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(