diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index d3074f691..87376b4b7 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -2648,8 +2648,8 @@ 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())) + && 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::>(); @@ -2676,7 +2676,7 @@ fn pending_source_surfaces( 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()) + document.get_entity(*handle).is_some_and(|e| sweep_model::assoc_source_supported(e)) })) .then_some((surface, sources)) }) @@ -2690,6 +2690,191 @@ 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)?; + 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 @@ -2703,7 +2888,6 @@ fn materialize_source_surface_actions( 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() { @@ -2748,9 +2932,11 @@ fn materialize_source_surface_actions( AssocSurfaceActionKind::Lofted, "AcDbAssocLoftedSurfaceActionBody", "ACDBASSOCLOFTEDSURFACEACTIONBODY", + // One value per cross section: the start and end + // settings at the ends, the defaults between them. vec![ - ("Continuity", 0, vec![short(continuity.0), short(continuity.1)], false), - ("Bulge", 0, vec![real(bulge.0), real(bulge.1)], false), + ("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))) @@ -2758,61 +2944,29 @@ fn materialize_source_surface_actions( ) } }; - 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 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(); - 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 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), 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); + 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 { @@ -2827,6 +2981,9 @@ fn materialize_source_surface_actions( }))); owned.push(parameter); } + if !complete { + continue; + } objects.push((action, network, "ACDBASSOCACTION", "AcDbAssocAction", AssociativeData::Action(AssocAction { class_version: 2, geometry_status: 0, @@ -2915,10 +3072,6 @@ fn materialize_swept_surface_actions( let codec::entities::SurfaceData::Swept { options, .. } = &surface_value.surface_data else { continue }; let (rotation, scale, twist) = (options.align_angle, options.scale_factor, options.twist_angle); let expressions = sweep_model::sweep_expressions(surface_entity).unwrap_or([None, None]); - let (Some(profile_curve), Some(path_curve)) = ( - document.get_entity(profile).and_then(sweep_model::assoc_edge_curve), - document.get_entity(path).and_then(sweep_model::assoc_edge_curve), - ) else { continue }; let evaluate = |expression: &str| { let mut table = parameters.clone(); table.set("sweepExpression", expression).ok()?; @@ -2926,8 +3079,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, @@ -2943,9 +3095,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. @@ -3029,35 +3187,20 @@ fn materialize_swept_surface_actions( ..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 { + 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: vec![child], + parameters: children, 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((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); } diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs index 6847c980c..fd21e3e66 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -452,7 +452,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)); @@ -564,25 +564,112 @@ pub fn assoc_edge_curve(entity: &EntityType) -> Option<(i32, codec::objects::Ass } 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)) + let points = points.iter().map(|p| glam::DVec3::from_array(*p)).collect::>(); + Some(nurbs_curve(curve.degree(), tolerance, &knots, curve.weights(), &points)) } _ => None, } } +/// 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. 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 }; diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index fb5f12072..ac1300ad2 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -13,7 +13,7 @@ use codec::EntityType; use crate::entities::curve::entity_curve; pub use super::sweep_command_model::{ - is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, link_sweep_sources, sweep_sources, SWEEP_SOURCES_APP, resweep_from_sources, surface_associativity, set_surface_associativity, assoc_edge_curve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options, + 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, }; /// A drawn profile, as the kernel wants it: the plane it lies in and the