1use crate::feature_pipeline::features::common;
45use crate::feature_pipeline::{Axis, FeatureContext, FeatureResult, SceneMap, ScenePoint};
46use crate::{
47 make_arc, make_line, resolve_edge_selection, resolve_face_selection,
48 resolve_vertex_selection, NurbsCurve, SelectionGeometry, Sketch3dDoc, Sketch3dSolveOptions,
49 Vec3, Vec4,
50};
51use serde_json::{json, Map, Value};
52
53pub const SKETCH3D_FEATURE_TYPE: &str = "S3D";
55
56pub const SKETCH3D_DOC_KEY: &str = "sketch3d";
58
59pub const SKETCH3D_REFS_KEY: &str = "externalRefs";
61
62pub const SKETCH3D_REFERENCES_PARAM: &str = "references";
64
65pub const SKETCH3D_EDIT_BUTTON: &str = "editSketch3d";
67
68pub const REFERENCE_TIP_LENGTH: f64 = 1.0;
73
74pub const REFERENCE_ANCHOR_EDGES_KEY: &str = "anchorEdges";
78
79const ANCHOR_END_REL: f64 = 1e-5;
82
83const CHAIN_JOIN_REL: f64 = 1e-7;
86
87pub fn execute(ctx: &FeatureContext) -> FeatureResult {
88 match build(ctx) {
89 Ok(output) => {
90 let mut result = FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
91 for entry in output.per_geometry {
92 result.paths.push(entry);
93 }
94 if let Some((chain, names)) = output.chain {
95 result.paths.push((ctx.id.clone(), chain));
96 result.path_segment_names.push((ctx.id.clone(), names));
97 }
98 result.axes = output.axes;
99 result.points = output.points;
100 result.unresolved = output.unresolved;
101 result.notes.extend(output.notes);
102 result
103 }
104 Err(error) => ctx.fail(error),
105 }
106}
107
108pub fn context_applicable(_probe: &crate::feature_pipeline::SelectionProbe) -> bool {
109 false
110}
111
112pub fn schema() -> Value {
113 json!({
114 "type": SKETCH3D_FEATURE_TYPE,
115 "shortName": SKETCH3D_FEATURE_TYPE,
116 "longName": "3D Sketch",
117 "ribbonPath": "Home/Reference/3D Sketch",
118 "commandSize": "Large",
119 "displayBuilder": true,
120 "inputParamsSchema": {
121 "id": {
122 "type": "string",
123 "default_value": null,
124 "hint": "unique identifier for the 3D sketch feature"
125 },
126 SKETCH3D_REFERENCES_PARAM: {
127 "type": "reference_selection",
128 "selectionFilter": ["VERTEX", "EDGE", "FACE", "PLANE"],
129 "multiple": true,
130 "default_value": null,
131 "label": "References",
132 "hint": "Model geometry the sketch is linked to: a vertex becomes a fixed point, an edge a fixed line or arc, a face or datum plane a reference plane, a cylinder its axis. Linked in the editor with the Link reference tool, or listed here; re-resolved on every regenerate."
133 },
134 SKETCH3D_EDIT_BUTTON: {
135 "type": "button",
136 "label": "Edit 3D Sketch",
137 "default_value": null,
138 "hint": "open the 3D sketch editor in the viewport"
139 }
140 }
141 })
142}
143
144struct Output {
149 per_geometry: Vec<(String, Vec<NurbsCurve>)>,
150 chain: Option<(Vec<NurbsCurve>, Vec<Option<String>>)>,
151 axes: Vec<(String, Axis)>,
152 points: Vec<(String, ScenePoint)>,
153 unresolved: Vec<String>,
154 notes: Vec<String>,
155}
156
157fn build(ctx: &FeatureContext) -> Result<Output, String> {
158 let mut unresolved: Vec<String> = Vec::new();
159 let mut notes: Vec<String> = Vec::new();
160
161 let mut sketch = match ctx.persistent.get(SKETCH3D_DOC_KEY) {
164 Some(value) if value.is_object() => value.clone(),
165 Some(value) => {
166 return Err(format!(
167 "3D sketch: `persistentData.{SKETCH3D_DOC_KEY}` must be an object, got {value}"
168 ));
169 }
170 None => {
171 for name in common::reference_names(ctx.param(SKETCH3D_REFERENCES_PARAM)) {
174 if resolve_reference(ctx.scene, &name).is_none() {
175 unresolved.push(name);
176 }
177 }
178 return Ok(Output {
179 per_geometry: Vec::new(),
180 chain: None,
181 axes: Vec::new(),
182 points: Vec::new(),
183 unresolved,
184 notes,
185 });
186 }
187 };
188
189 pre_evaluate_expressions(ctx, &mut sketch)?;
190 let mut doc: Sketch3dDoc = serde_json::from_value(sketch)
191 .map_err(|error| format!("3D sketch: invalid document: {error}"))?;
192
193 let refs = ctx
196 .persistent
197 .get(SKETCH3D_REFS_KEY)
198 .and_then(Value::as_array)
199 .cloned()
200 .unwrap_or_default();
201 let mut materialized: std::collections::HashSet<String> = std::collections::HashSet::new();
202 for entry in &refs {
203 let Some(name) = entry.get("name").and_then(Value::as_str) else {
204 continue;
205 };
206 materialized.insert(name.to_string());
207 let kind = entry.get("kind").and_then(Value::as_str).unwrap_or("");
208 let ids: Vec<Value> = entry
209 .get("points")
210 .and_then(Value::as_array)
211 .cloned()
212 .unwrap_or_default();
213 let resolved = match anchor_edge_names(entry) {
218 Some(anchors) => resolve_anchored_vertex(ctx.scene, &anchors).map(ResolvedReference::Point),
219 None => resolve_reference(ctx.scene, name),
220 };
221 let resolved = resolved.map(|r| r.as_recorded(kind));
222 match resolved {
223 Some(resolved) if resolved.kind() == kind && resolved.positions().len() == ids.len() => {
224 let positions = resolved.positions();
233 let positions = match resolved {
234 ResolvedReference::Plane(origin, tip) | ResolvedReference::Line(origin, tip) => {
235 let direction = tip.sub(origin);
236 let editor_origin = ids
237 .first()
238 .and_then(|id| doc.point(id))
239 .map(|p| p.position())
240 .unwrap_or(origin);
241 let kept = match direction.normalized() {
242 Ok(unit) => {
243 let offset = editor_origin.sub(origin);
244 if kind == "plane" {
245 editor_origin.sub(unit.scale(offset.dot(unit)))
246 } else {
247 origin.add(unit.scale(offset.dot(unit)))
248 }
249 }
250 Err(_) => origin,
251 };
252 vec![kept, kept.add(direction)]
253 }
254 _ => positions,
255 };
256 for (id, position) in ids.iter().zip(positions) {
257 if let Some(point) = doc.point_mut(id) {
258 point.x = position.x;
259 point.y = position.y;
260 point.z = position.z;
261 point.fixed = true;
262 }
263 }
264 }
265 _ => {
269 notes.push(lost_reference_note(&doc, name, &ids));
270 unresolved.push(name.to_string());
271 }
272 }
273 }
274 for name in common::reference_names(ctx.param(SKETCH3D_REFERENCES_PARAM)) {
277 if materialized.contains(&name) {
278 continue;
279 }
280 if resolve_reference(ctx.scene, &name).is_none() {
281 unresolved.push(name);
282 }
283 }
284
285 let diagnostics = crate::solve_sketch3d_doc(&mut doc, &Sketch3dSolveOptions::default())?;
287 if diagnostics.conflicting {
288 return Err(format!(
289 "3D sketch: conflicting constraints ({}): the sketch has no solution near its current geometry",
290 diagnostics.conflicting_constraints.join(", ")
291 ));
292 }
293
294 let scale = doc
295 .points
296 .iter()
297 .fold(1.0f64, |acc, p| acc.max(p.x.abs()).max(p.y.abs()).max(p.z.abs()));
298
299 let mut points: Vec<(String, ScenePoint)> = Vec::with_capacity(doc.points.len());
301 for point in &doc.points {
302 points.push((
303 format!("{}:P{}", ctx.id, id_display(&point.id)),
304 ScenePoint {
305 position: point.position(),
306 construction: point.construction,
307 },
308 ));
309 }
310
311 let mut per_geometry: Vec<(String, Vec<NurbsCurve>)> = Vec::new();
313 let mut axes: Vec<(String, Axis)> = Vec::new();
314 let mut segments: Vec<Segment> = Vec::new();
315 for geometry in &doc.geometries {
316 let name = format!("{}:G{}", ctx.id, id_display(&geometry.id));
317 let Some(positions) = doc.geometry_positions(geometry) else {
318 return Err(format!(
319 "3D sketch: geometry {} references a missing point",
320 id_display(&geometry.id)
321 ));
322 };
323 if geometry.geom_type == "line" && positions.len() == 2 {
324 if let Ok(direction) = positions[1].sub(positions[0]).normalized() {
325 axes.push((name.clone(), Axis { point: positions[0], direction }));
326 }
327 }
328 if geometry.construction() {
329 continue;
330 }
331 let curve = match (geometry.geom_type.as_str(), positions.len()) {
332 ("point", _) => continue,
333 ("line", 2) => {
334 if positions[1].sub(positions[0]).length() <= 1e-9 * scale {
335 return Err(format!("3D sketch: line {} has zero length", id_display(&geometry.id)));
336 }
337 make_line(positions[0], positions[1])?
338 }
339 ("arc", 3) => arc_curve(positions[0], positions[1], positions[2], scale)
340 .map_err(|error| format!("3D sketch: arc {}: {error}", id_display(&geometry.id)))?,
341 ("bezier", n) if n >= 4 && (n - 1) % 3 == 0 => bezier_chain_curve(&positions)?,
342 (other, n) => {
343 return Err(format!(
344 "3D sketch: geometry {} is a '{other}' with {n} points, which is not a line, arc or bezier",
345 id_display(&geometry.id)
346 ));
347 }
348 };
349 let [t0, t1] = curve.domain()?;
350 segments.push(Segment {
351 name: name.clone(),
352 start: curve.evaluate(t0)?,
353 end: curve.evaluate(t1)?,
354 curve: curve.clone(),
355 });
356 per_geometry.push((name, vec![curve]));
357 }
358
359 let chain = longest_chain(&segments, CHAIN_JOIN_REL * scale);
360
361 Ok(Output {
362 per_geometry,
363 chain,
364 axes,
365 points,
366 unresolved,
367 notes,
368 })
369}
370
371fn lost_reference_note(doc: &Sketch3dDoc, name: &str, ids: &[Value]) -> String {
374 let needed: Vec<String> = doc
375 .constraints
376 .iter()
377 .filter(|c| c.points().iter().any(|p| ids.iter().any(|id| crate::sketch_id_key(id) == crate::sketch_id_key(p))))
378 .map(|c| format!("{} {}", c.ctype().unwrap_or("?"), c.id().map(id_display).unwrap_or_else(|| "?".into())))
379 .collect();
380 if needed.is_empty() {
381 format!("3D sketch: reference '{name}' no longer resolves; it keeps its last position")
382 } else {
383 format!(
384 "3D sketch: reference '{name}' no longer resolves; it keeps its last position, needed by constraint {}",
385 needed.join(", ")
386 )
387 }
388}
389
390fn anchor_edge_names(entry: &Value) -> Option<Vec<String>> {
391 let names: Vec<String> = entry
392 .get(REFERENCE_ANCHOR_EDGES_KEY)?
393 .as_array()?
394 .iter()
395 .filter_map(|n| n.as_str().map(String::from))
396 .collect();
397 (names.len() == 2).then_some(names)
398}
399
400pub fn common_edge_end(a: (Vec3, Vec3), b: (Vec3, Vec3)) -> Option<Vec3> {
403 let scale = [a.0, a.1, b.0, b.1]
404 .iter()
405 .fold(1.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()).max(p.z.abs()));
406 let tol = ANCHOR_END_REL * scale;
407 let mut shared: Vec<Vec3> = Vec::new();
408 for end in [a.0, a.1] {
409 if [b.0, b.1].iter().any(|other| end.sub(*other).length() <= tol) {
410 shared.push(end);
411 }
412 }
413 match shared.as_slice() {
414 [only] => Some(*only),
415 [first, second] if first.sub(*second).length() <= tol => Some(*first),
416 _ => None,
417 }
418}
419
420fn resolve_anchored_vertex(scene: &SceneMap, anchors: &[String]) -> Option<Vec3> {
421 let ends = |name: &String| -> Option<(Vec3, Vec3)> {
422 let curve = common::edge_curve(scene.resolve_edge(name)?).ok()?;
423 let [t0, t1] = curve.domain().ok()?;
424 Some((curve.evaluate(t0).ok()?, curve.evaluate(t1).ok()?))
425 };
426 common_edge_end(ends(anchors.first()?)?, ends(anchors.get(1)?)?)
427}
428
429fn id_display(id: &Value) -> String {
432 match id {
433 Value::String(text) => text.clone(),
434 other => crate::sketch_id_key(other),
435 }
436}
437
438fn eval_expr(ctx: &FeatureContext, what: &str, value: &Value) -> Result<Option<f64>, String> {
443 let Value::String(source) = value else {
444 return Ok(None);
445 };
446 if source.trim().is_empty() {
447 return Ok(None);
448 }
449 let number = ctx
450 .env
451 .eval(source)
452 .map_err(|error| format!("3D sketch: {what} = `{source}`: {error}"))?;
453 if !number.is_finite() {
454 return Err(format!("3D sketch: {what} = `{source}` evaluated to {number}"));
455 }
456 Ok(Some(number))
457}
458
459fn pre_evaluate_expressions(ctx: &FeatureContext, sketch: &mut Value) -> Result<(), String> {
462 if let Some(points) = sketch.get_mut("points").and_then(Value::as_array_mut) {
463 for point in points {
464 let id = point.get("id").map(|id| id.to_string()).unwrap_or_else(|| "?".into());
465 for key in ["x", "y", "z"] {
466 let Some(value) = point.get(key).cloned() else { continue };
467 if let Some(number) = eval_expr(ctx, &format!("point {id} `{key}`"), &value)? {
468 point[key] = Value::from(number);
469 }
470 }
471 }
472 }
473 if let Some(constraints) = sketch.get_mut("constraints").and_then(Value::as_array_mut) {
474 for constraint in constraints {
475 let id = constraint.get("id").map(|id| id.to_string()).unwrap_or_else(|| "?".into());
476 let expr = constraint
477 .get("valueExpr")
478 .cloned()
479 .map(|value| eval_expr(ctx, &format!("constraint {id} `valueExpr`"), &value))
480 .transpose()?
481 .flatten();
482 let evaluated = match expr {
483 Some(number) => Some(number),
484 None => constraint
485 .get("value")
486 .cloned()
487 .map(|value| eval_expr(ctx, &format!("constraint {id} `value`"), &value))
488 .transpose()?
489 .flatten(),
490 };
491 if let Some(number) = evaluated {
492 constraint["value"] = Value::from(number);
493 }
494 }
495 }
496 Ok(())
497}
498
499#[derive(Clone, Debug)]
505pub enum ResolvedReference {
506 Point(Vec3),
508 Line(Vec3, Vec3),
511 Segment(Vec3, Vec3),
513 CircleAxis(Vec3, Vec3),
516 Arc(Vec3, Vec3, Vec3),
518 Plane(Vec3, Vec3),
520 Curve(Vec<Vec3>),
523}
524
525const REFERENCE_CURVE_SAMPLES: usize = 128;
527
528pub const REFERENCE_CURVE_SPANS: usize = 8;
531
532pub fn curve_reference_controls(samples: &[Vec3]) -> Option<Vec<Vec3>> {
536 if samples.len() < 2 {
537 return None;
538 }
539 let mut cumulative = Vec::with_capacity(samples.len());
540 let mut total = 0.0;
541 cumulative.push(0.0);
542 for pair in samples.windows(2) {
543 total += pair[1].sub(pair[0]).length();
544 cumulative.push(total);
545 }
546 if total <= 0.0 {
547 return None;
548 }
549 let spans = REFERENCE_CURVE_SPANS;
550 let mut segment = 0;
551 let anchors: Vec<Vec3> = (0..=spans)
552 .map(|i| {
553 let at = total * i as f64 / spans as f64;
554 while segment + 2 < samples.len() && cumulative[segment + 1] < at {
555 segment += 1;
556 }
557 let length = cumulative[segment + 1] - cumulative[segment];
558 let t = if length > 0.0 { ((at - cumulative[segment]) / length).clamp(0.0, 1.0) } else { 0.0 };
559 samples[segment].add(samples[segment + 1].sub(samples[segment]).scale(t))
560 })
561 .collect();
562 let closed = samples[0].sub(samples[samples.len() - 1]).length() <= 1e-9 * total;
563 let tangent = |i: usize| -> Vec3 {
564 let (before, after) = match i {
565 0 if closed => (anchors[spans - 1], anchors[1]),
566 0 => (anchors[0], anchors[1]),
567 n if n == spans && closed => (anchors[spans - 1], anchors[1]),
568 n if n == spans => (anchors[spans - 1], anchors[spans]),
569 n => (anchors[n - 1], anchors[n + 1]),
570 };
571 let scale = if !closed && (i == 0 || i == spans) { 1.0 / 3.0 } else { 1.0 / 6.0 };
572 after.sub(before).scale(scale)
573 };
574 let mut controls = Vec::with_capacity(3 * spans + 1);
575 for i in 0..spans {
576 controls.push(anchors[i]);
577 controls.push(anchors[i].add(tangent(i)));
578 controls.push(anchors[i + 1].sub(tangent(i + 1)));
579 }
580 controls.push(anchors[spans]);
581 Some(controls)
582}
583
584impl ResolvedReference {
585 pub fn kind(&self) -> &'static str {
587 match self {
588 ResolvedReference::Point(..) => "point",
589 ResolvedReference::Line(..) | ResolvedReference::Segment(..) | ResolvedReference::CircleAxis(..) => "line",
592 ResolvedReference::Arc(..) => "arc",
593 ResolvedReference::Plane(..) => "plane",
594 ResolvedReference::Curve(..) => "curve",
595 }
596 }
597
598 pub fn as_recorded(self, kind: &str) -> ResolvedReference {
602 match self {
603 ResolvedReference::Curve(controls) if kind == "line" && controls.len() >= 2 => {
604 ResolvedReference::Segment(controls[0], controls[controls.len() - 1])
605 }
606 other => other,
607 }
608 }
609
610 pub fn positions(&self) -> Vec<Vec3> {
612 match *self {
613 ResolvedReference::Point(p) => vec![p],
614 ResolvedReference::Line(a, b) | ResolvedReference::Segment(a, b) | ResolvedReference::CircleAxis(a, b) => vec![a, b],
615 ResolvedReference::Arc(c, s, e) => vec![c, s, e],
616 ResolvedReference::Plane(o, n) => vec![o, n],
617 ResolvedReference::Curve(ref controls) => controls.clone(),
618 }
619 }
620
621 pub fn geometry_type(&self) -> Option<&'static str> {
624 match self {
625 ResolvedReference::Point(..) => None,
626 ResolvedReference::Line(..) | ResolvedReference::Segment(..) | ResolvedReference::CircleAxis(..) => Some("line"),
627 ResolvedReference::Arc(..) => Some("arc"),
628 ResolvedReference::Plane(..) => Some("plane"),
629 ResolvedReference::Curve(..) => Some("bezier"),
630 }
631 }
632}
633
634pub fn resolve_reference(scene: &SceneMap, name: &str) -> Option<ResolvedReference> {
641 if let Some(point) = scene.resolve_point(name) {
642 return Some(ResolvedReference::Point(point));
643 }
644 if let Some(frame) = scene.resolve_frame(name) {
645 return Some(ResolvedReference::Plane(
646 frame.origin,
647 frame.origin.add(frame.z_axis.scale(REFERENCE_TIP_LENGTH)),
648 ));
649 }
650 if let Some(axis) = scene.resolve_axis(name) {
651 return Some(ResolvedReference::Line(
652 axis.point,
653 axis.point.add(axis.direction.scale(REFERENCE_TIP_LENGTH)),
654 ));
655 }
656 if let Some(face) = scene.resolve_face(name) {
657 let geometry = crate::with_registered_solid_str(face.handle, |solid| {
658 resolve_face_selection(solid, face.face_id).map_err(|e| format!("{e:?}"))
659 })
660 .ok()?;
661 return Some(match geometry {
662 SelectionGeometry::Plane { origin, normal } => {
663 ResolvedReference::Plane(origin, origin.add(normal.scale(REFERENCE_TIP_LENGTH)))
664 }
665 SelectionGeometry::Axis { origin, direction, .. } => {
666 ResolvedReference::Line(origin, origin.add(direction.scale(REFERENCE_TIP_LENGTH)))
667 }
668 SelectionGeometry::Sphere { center, .. } => ResolvedReference::Point(center),
669 _ => return None,
670 });
671 }
672 if let Some(edge) = scene.resolve_edge(name) {
673 let curve = common::edge_curve(edge).ok()?;
674 let [t0, t1] = curve.domain().ok()?;
675 let start = curve.evaluate(t0).ok()?;
676 let end = curve.evaluate(t1).ok()?;
677 let geometry = crate::with_registered_solid_str(edge.handle, |solid| {
678 resolve_edge_selection(solid, edge.edge_id).map_err(|e| format!("{e:?}"))
679 });
680 return Some(match geometry {
681 Ok(SelectionGeometry::Circle { center, axis, radius }) => {
682 if end.sub(start).length() <= 1e-9 * radius.max(1.0) {
683 ResolvedReference::CircleAxis(center, center.add(axis.scale(radius)))
685 } else {
686 ResolvedReference::Arc(center, start, end)
687 }
688 }
689 Ok(SelectionGeometry::Line { .. }) => ResolvedReference::Segment(start, end),
690 _ => {
693 let samples: Option<Vec<Vec3>> = (0..=REFERENCE_CURVE_SAMPLES)
694 .map(|i| curve.evaluate(t0 + (t1 - t0) * i as f64 / REFERENCE_CURVE_SAMPLES as f64).ok())
695 .collect();
696 match samples.as_deref().and_then(curve_reference_controls) {
697 Some(controls) => ResolvedReference::Curve(controls),
698 None => ResolvedReference::Segment(start, end),
699 }
700 }
701 });
702 }
703 if let Some((solid_name, coords)) = name.rsplit_once('@') {
704 let mut parts = coords.split(',').map(|p| p.trim().parse::<f64>().ok());
705 let position = Vec3::new(parts.next()??, parts.next()??, parts.next()??);
706 if parts.next().is_some() {
707 return None;
708 }
709 let handle = scene.resolve_solid(solid_name)?;
710 let geometry = crate::with_registered_solid_str(handle, |solid| {
711 resolve_vertex_selection(solid, position).map_err(|e| format!("{e:?}"))
712 })
713 .ok()?;
714 if let SelectionGeometry::Point { position } = geometry {
715 return Some(ResolvedReference::Point(position));
716 }
717 }
718 None
719}
720
721pub fn arc_curve(center: Vec3, start: Vec3, end: Vec3, scale: f64) -> Result<NurbsCurve, String> {
729 let rs = start.sub(center);
730 let re = end.sub(center);
731 let radius = rs.length();
732 if radius <= 1e-9 * scale {
733 return Err("zero radius".into());
734 }
735 let normal = rs.cross(re);
736 if normal.length() <= 1e-9 * radius * radius {
737 return Err("center, start and end are collinear (no arc plane)".into());
738 }
739 let x_axis = rs.normalized()?;
740 let y_axis = normal.normalized()?.cross(x_axis).normalized()?;
741 let sweep = re.dot(y_axis).atan2(re.dot(x_axis));
744 let sweep = if sweep <= 0.0 { sweep + std::f64::consts::TAU } else { sweep };
745 make_arc(center, x_axis, y_axis, radius, 0.0, sweep)
746}
747
748pub fn bezier_chain_curve(controls: &[Vec3]) -> Result<NurbsCurve, String> {
752 let span_count = (controls.len().saturating_sub(1)) / 3;
753 if span_count == 0 || controls.len() != span_count * 3 + 1 {
754 return Err(format!(
755 "3D sketch: bezier needs 3n+1 control points, got {}",
756 controls.len()
757 ));
758 }
759 let mut knots = vec![0.0; 4];
760 for join in 1..span_count {
761 knots.extend_from_slice(&[join as f64; 3]);
762 }
763 knots.extend_from_slice(&[span_count as f64; 4]);
764 let points = controls.iter().map(|p| Vec4::from_point(*p, 1.0)).collect();
765 NurbsCurve::new(3, knots, points)
766}
767
768struct Segment {
773 name: String,
774 start: Vec3,
775 end: Vec3,
776 curve: NurbsCurve,
777}
778
779fn longest_chain(segments: &[Segment], tol: f64) -> Option<(Vec<NurbsCurve>, Vec<Option<String>>)> {
785 if segments.is_empty() {
786 return None;
787 }
788 let close = |a: Vec3, b: Vec3| a.sub(b).length() <= tol;
789 let degree_at = |p: Vec3| -> usize {
791 segments
792 .iter()
793 .map(|s| usize::from(close(s.start, p)) + usize::from(close(s.end, p)))
794 .sum()
795 };
796 let mut best: Option<Vec<(usize, bool)>> = None;
797 for (seed, _) in segments.iter().enumerate() {
799 for seed_reversed in [false, true] {
800 let mut chain: Vec<(usize, bool)> = vec![(seed, seed_reversed)];
801 let mut used = vec![false; segments.len()];
802 used[seed] = true;
803 loop {
805 let (last, reversed) = *chain.last().unwrap();
806 let tail = if reversed { segments[last].start } else { segments[last].end };
807 if degree_at(tail) != 2 {
808 break;
809 }
810 let next = segments.iter().enumerate().find_map(|(i, s)| {
811 if used[i] {
812 return None;
813 }
814 if close(s.start, tail) {
815 Some((i, false))
816 } else if close(s.end, tail) {
817 Some((i, true))
818 } else {
819 None
820 }
821 });
822 match next {
823 Some((i, reversed)) => {
824 used[i] = true;
825 chain.push((i, reversed));
826 }
827 None => break,
828 }
829 }
830 loop {
832 let (first, reversed) = chain[0];
833 let head = if reversed { segments[first].end } else { segments[first].start };
834 if degree_at(head) != 2 {
835 break;
836 }
837 let prev = segments.iter().enumerate().find_map(|(i, s)| {
838 if used[i] {
839 return None;
840 }
841 if close(s.end, head) {
842 Some((i, false))
843 } else if close(s.start, head) {
844 Some((i, true))
845 } else {
846 None
847 }
848 });
849 match prev {
850 Some((i, reversed)) => {
851 used[i] = true;
852 chain.insert(0, (i, reversed));
853 }
854 None => break,
855 }
856 }
857 if best.as_ref().map_or(true, |b| chain.len() > b.len()) {
858 best = Some(chain);
859 }
860 }
861 }
862 let chain = best?;
863 let mut curves = Vec::with_capacity(chain.len());
864 let mut names = Vec::with_capacity(chain.len());
865 for (i, reversed) in chain {
866 let curve = if reversed {
867 segments[i].curve.reversed().ok()?
868 } else {
869 segments[i].curve.clone()
870 };
871 curves.push(curve);
872 names.push(Some(segments[i].name.clone()));
873 }
874 Some((curves, names))
875}
876
877pub fn reference_record(name: &str, resolved: &ResolvedReference, point_ids: &[Value], geometry_id: Option<&Value>) -> Value {
881 let mut map = Map::new();
882 map.insert("name".into(), Value::String(name.to_string()));
883 map.insert("kind".into(), Value::String(resolved.kind().to_string()));
884 map.insert("points".into(), Value::Array(point_ids.to_vec()));
885 map.insert(
886 "geometry".into(),
887 geometry_id.cloned().unwrap_or(Value::Null),
888 );
889 Value::Object(map)
890}
891