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
74const CHAIN_JOIN_REL: f64 = 1e-7;
77
78pub fn execute(ctx: &FeatureContext) -> FeatureResult {
79 match build(ctx) {
80 Ok(output) => {
81 let mut result = FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
82 for entry in output.per_geometry {
83 result.paths.push(entry);
84 }
85 if let Some((chain, names)) = output.chain {
86 result.paths.push((ctx.id.clone(), chain));
87 result.path_segment_names.push((ctx.id.clone(), names));
88 }
89 result.axes = output.axes;
90 result.points = output.points;
91 result.unresolved = output.unresolved;
92 result
93 }
94 Err(error) => ctx.fail(error),
95 }
96}
97
98pub fn context_applicable(_probe: &crate::feature_pipeline::SelectionProbe) -> bool {
99 false
100}
101
102pub fn schema() -> Value {
103 json!({
104 "type": SKETCH3D_FEATURE_TYPE,
105 "shortName": SKETCH3D_FEATURE_TYPE,
106 "longName": "3D Sketch",
107 "ribbonPath": "Home/Reference/3D Sketch",
108 "commandSize": "Large",
109 "displayBuilder": true,
110 "inputParamsSchema": {
111 "id": {
112 "type": "string",
113 "default_value": null,
114 "hint": "unique identifier for the 3D sketch feature"
115 },
116 SKETCH3D_REFERENCES_PARAM: {
117 "type": "reference_selection",
118 "selectionFilter": ["VERTEX", "EDGE", "FACE", "PLANE"],
119 "multiple": true,
120 "default_value": null,
121 "label": "References",
122 "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."
123 },
124 SKETCH3D_EDIT_BUTTON: {
125 "type": "button",
126 "label": "Edit 3D Sketch",
127 "default_value": null,
128 "hint": "open the 3D sketch editor in the viewport"
129 }
130 }
131 })
132}
133
134struct Output {
139 per_geometry: Vec<(String, Vec<NurbsCurve>)>,
140 chain: Option<(Vec<NurbsCurve>, Vec<Option<String>>)>,
141 axes: Vec<(String, Axis)>,
142 points: Vec<(String, ScenePoint)>,
143 unresolved: Vec<String>,
144}
145
146fn build(ctx: &FeatureContext) -> Result<Output, String> {
147 let mut unresolved: Vec<String> = Vec::new();
148
149 let mut sketch = match ctx.persistent.get(SKETCH3D_DOC_KEY) {
152 Some(value) if value.is_object() => value.clone(),
153 Some(value) => {
154 return Err(format!(
155 "3D sketch: `persistentData.{SKETCH3D_DOC_KEY}` must be an object, got {value}"
156 ));
157 }
158 None => {
159 for name in common::reference_names(ctx.param(SKETCH3D_REFERENCES_PARAM)) {
162 if resolve_reference(ctx.scene, &name).is_none() {
163 unresolved.push(name);
164 }
165 }
166 return Ok(Output {
167 per_geometry: Vec::new(),
168 chain: None,
169 axes: Vec::new(),
170 points: Vec::new(),
171 unresolved,
172 });
173 }
174 };
175
176 pre_evaluate_expressions(ctx, &mut sketch)?;
177 let mut doc: Sketch3dDoc = serde_json::from_value(sketch)
178 .map_err(|error| format!("3D sketch: invalid document: {error}"))?;
179
180 let refs = ctx
183 .persistent
184 .get(SKETCH3D_REFS_KEY)
185 .and_then(Value::as_array)
186 .cloned()
187 .unwrap_or_default();
188 let mut materialized: std::collections::HashSet<String> = std::collections::HashSet::new();
189 for entry in &refs {
190 let Some(name) = entry.get("name").and_then(Value::as_str) else {
191 continue;
192 };
193 materialized.insert(name.to_string());
194 let kind = entry.get("kind").and_then(Value::as_str).unwrap_or("");
195 let ids: Vec<Value> = entry
196 .get("points")
197 .and_then(Value::as_array)
198 .cloned()
199 .unwrap_or_default();
200 match resolve_reference(ctx.scene, name) {
201 Some(resolved) if resolved.kind() == kind && resolved.positions().len() == ids.len() => {
202 let positions = resolved.positions();
209 let positions = match resolved {
210 ResolvedReference::Plane(origin, tip) | ResolvedReference::Line(origin, tip) => {
211 let direction = tip.sub(origin);
212 let editor_origin = ids
213 .first()
214 .and_then(|id| doc.point(id))
215 .map(|p| p.position())
216 .unwrap_or(origin);
217 let kept = match direction.normalized() {
218 Ok(unit) => {
219 let offset = editor_origin.sub(origin);
220 if kind == "plane" {
221 editor_origin.sub(unit.scale(offset.dot(unit)))
222 } else {
223 origin.add(unit.scale(offset.dot(unit)))
224 }
225 }
226 Err(_) => origin,
227 };
228 vec![kept, kept.add(direction)]
229 }
230 _ => positions,
231 };
232 for (id, position) in ids.iter().zip(positions) {
233 if let Some(point) = doc.point_mut(id) {
234 point.x = position.x;
235 point.y = position.y;
236 point.z = position.z;
237 point.fixed = true;
238 }
239 }
240 }
241 _ => unresolved.push(name.to_string()),
245 }
246 }
247 for name in common::reference_names(ctx.param(SKETCH3D_REFERENCES_PARAM)) {
250 if materialized.contains(&name) {
251 continue;
252 }
253 if resolve_reference(ctx.scene, &name).is_none() {
254 unresolved.push(name);
255 }
256 }
257
258 let diagnostics = crate::solve_sketch3d_doc(&mut doc, &Sketch3dSolveOptions::default())?;
260 if diagnostics.conflicting {
261 return Err(format!(
262 "3D sketch: conflicting constraints ({}): the sketch has no solution near its current geometry",
263 diagnostics.conflicting_constraints.join(", ")
264 ));
265 }
266
267 let scale = doc
268 .points
269 .iter()
270 .fold(1.0f64, |acc, p| acc.max(p.x.abs()).max(p.y.abs()).max(p.z.abs()));
271
272 let mut points: Vec<(String, ScenePoint)> = Vec::with_capacity(doc.points.len());
274 for point in &doc.points {
275 points.push((
276 format!("{}:P{}", ctx.id, id_display(&point.id)),
277 ScenePoint {
278 position: point.position(),
279 construction: point.construction,
280 },
281 ));
282 }
283
284 let mut per_geometry: Vec<(String, Vec<NurbsCurve>)> = Vec::new();
286 let mut axes: Vec<(String, Axis)> = Vec::new();
287 let mut segments: Vec<Segment> = Vec::new();
288 for geometry in &doc.geometries {
289 let name = format!("{}:G{}", ctx.id, id_display(&geometry.id));
290 let Some(positions) = doc.geometry_positions(geometry) else {
291 return Err(format!(
292 "3D sketch: geometry {} references a missing point",
293 id_display(&geometry.id)
294 ));
295 };
296 if geometry.geom_type == "line" && positions.len() == 2 {
297 if let Ok(direction) = positions[1].sub(positions[0]).normalized() {
298 axes.push((name.clone(), Axis { point: positions[0], direction }));
299 }
300 }
301 if geometry.construction() {
302 continue;
303 }
304 let curve = match (geometry.geom_type.as_str(), positions.len()) {
305 ("point", _) => continue,
306 ("line", 2) => {
307 if positions[1].sub(positions[0]).length() <= 1e-9 * scale {
308 return Err(format!("3D sketch: line {} has zero length", id_display(&geometry.id)));
309 }
310 make_line(positions[0], positions[1])?
311 }
312 ("arc", 3) => arc_curve(positions[0], positions[1], positions[2], scale)
313 .map_err(|error| format!("3D sketch: arc {}: {error}", id_display(&geometry.id)))?,
314 ("bezier", n) if n >= 4 && (n - 1) % 3 == 0 => bezier_chain_curve(&positions)?,
315 (other, n) => {
316 return Err(format!(
317 "3D sketch: geometry {} is a '{other}' with {n} points, which is not a line, arc or bezier",
318 id_display(&geometry.id)
319 ));
320 }
321 };
322 let [t0, t1] = curve.domain()?;
323 segments.push(Segment {
324 name: name.clone(),
325 start: curve.evaluate(t0)?,
326 end: curve.evaluate(t1)?,
327 curve: curve.clone(),
328 });
329 per_geometry.push((name, vec![curve]));
330 }
331
332 let chain = longest_chain(&segments, CHAIN_JOIN_REL * scale);
333
334 Ok(Output {
335 per_geometry,
336 chain,
337 axes,
338 points,
339 unresolved,
340 })
341}
342
343fn id_display(id: &Value) -> String {
346 match id {
347 Value::String(text) => text.clone(),
348 other => crate::sketch_id_key(other),
349 }
350}
351
352fn eval_expr(ctx: &FeatureContext, what: &str, value: &Value) -> Result<Option<f64>, String> {
357 let Value::String(source) = value else {
358 return Ok(None);
359 };
360 if source.trim().is_empty() {
361 return Ok(None);
362 }
363 let number = ctx
364 .env
365 .eval(source)
366 .map_err(|error| format!("3D sketch: {what} = `{source}`: {error}"))?;
367 if !number.is_finite() {
368 return Err(format!("3D sketch: {what} = `{source}` evaluated to {number}"));
369 }
370 Ok(Some(number))
371}
372
373fn pre_evaluate_expressions(ctx: &FeatureContext, sketch: &mut Value) -> Result<(), String> {
376 if let Some(points) = sketch.get_mut("points").and_then(Value::as_array_mut) {
377 for point in points {
378 let id = point.get("id").map(|id| id.to_string()).unwrap_or_else(|| "?".into());
379 for key in ["x", "y", "z"] {
380 let Some(value) = point.get(key).cloned() else { continue };
381 if let Some(number) = eval_expr(ctx, &format!("point {id} `{key}`"), &value)? {
382 point[key] = Value::from(number);
383 }
384 }
385 }
386 }
387 if let Some(constraints) = sketch.get_mut("constraints").and_then(Value::as_array_mut) {
388 for constraint in constraints {
389 let id = constraint.get("id").map(|id| id.to_string()).unwrap_or_else(|| "?".into());
390 let expr = constraint
391 .get("valueExpr")
392 .cloned()
393 .map(|value| eval_expr(ctx, &format!("constraint {id} `valueExpr`"), &value))
394 .transpose()?
395 .flatten();
396 let evaluated = match expr {
397 Some(number) => Some(number),
398 None => constraint
399 .get("value")
400 .cloned()
401 .map(|value| eval_expr(ctx, &format!("constraint {id} `value`"), &value))
402 .transpose()?
403 .flatten(),
404 };
405 if let Some(number) = evaluated {
406 constraint["value"] = Value::from(number);
407 }
408 }
409 }
410 Ok(())
411}
412
413#[derive(Clone, Debug)]
419pub enum ResolvedReference {
420 Point(Vec3),
422 Line(Vec3, Vec3),
425 Arc(Vec3, Vec3, Vec3),
427 Plane(Vec3, Vec3),
429}
430
431impl ResolvedReference {
432 pub fn kind(&self) -> &'static str {
434 match self {
435 ResolvedReference::Point(..) => "point",
436 ResolvedReference::Line(..) => "line",
437 ResolvedReference::Arc(..) => "arc",
438 ResolvedReference::Plane(..) => "plane",
439 }
440 }
441
442 pub fn positions(&self) -> Vec<Vec3> {
444 match *self {
445 ResolvedReference::Point(p) => vec![p],
446 ResolvedReference::Line(a, b) => vec![a, b],
447 ResolvedReference::Arc(c, s, e) => vec![c, s, e],
448 ResolvedReference::Plane(o, n) => vec![o, n],
449 }
450 }
451
452 pub fn geometry_type(&self) -> Option<&'static str> {
455 match self {
456 ResolvedReference::Point(..) => None,
457 ResolvedReference::Line(..) => Some("line"),
458 ResolvedReference::Arc(..) => Some("arc"),
459 ResolvedReference::Plane(..) => Some("plane"),
460 }
461 }
462}
463
464pub fn resolve_reference(scene: &SceneMap, name: &str) -> Option<ResolvedReference> {
471 if let Some(point) = scene.resolve_point(name) {
472 return Some(ResolvedReference::Point(point));
473 }
474 if let Some(frame) = scene.resolve_frame(name) {
475 return Some(ResolvedReference::Plane(
476 frame.origin,
477 frame.origin.add(frame.z_axis.scale(REFERENCE_TIP_LENGTH)),
478 ));
479 }
480 if let Some(axis) = scene.resolve_axis(name) {
481 return Some(ResolvedReference::Line(
482 axis.point,
483 axis.point.add(axis.direction.scale(REFERENCE_TIP_LENGTH)),
484 ));
485 }
486 if let Some(face) = scene.resolve_face(name) {
487 let geometry = crate::with_registered_solid_str(face.handle, |solid| {
488 resolve_face_selection(solid, face.face_id).map_err(|e| format!("{e:?}"))
489 })
490 .ok()?;
491 return Some(match geometry {
492 SelectionGeometry::Plane { origin, normal } => {
493 ResolvedReference::Plane(origin, origin.add(normal.scale(REFERENCE_TIP_LENGTH)))
494 }
495 SelectionGeometry::Axis { origin, direction, .. } => {
496 ResolvedReference::Line(origin, origin.add(direction.scale(REFERENCE_TIP_LENGTH)))
497 }
498 SelectionGeometry::Sphere { center, .. } => ResolvedReference::Point(center),
499 _ => return None,
500 });
501 }
502 if let Some(edge) = scene.resolve_edge(name) {
503 let curve = common::edge_curve(edge).ok()?;
504 let [t0, t1] = curve.domain().ok()?;
505 let start = curve.evaluate(t0).ok()?;
506 let end = curve.evaluate(t1).ok()?;
507 let geometry = crate::with_registered_solid_str(edge.handle, |solid| {
508 resolve_edge_selection(solid, edge.edge_id).map_err(|e| format!("{e:?}"))
509 });
510 return Some(match geometry {
511 Ok(SelectionGeometry::Circle { center, axis, radius }) => {
512 if end.sub(start).length() <= 1e-9 * radius.max(1.0) {
513 ResolvedReference::Line(center, center.add(axis.scale(radius.max(REFERENCE_TIP_LENGTH))))
515 } else {
516 ResolvedReference::Arc(center, start, end)
517 }
518 }
519 Ok(SelectionGeometry::Line { .. }) => ResolvedReference::Line(start, end),
520 _ => ResolvedReference::Line(start, end),
523 });
524 }
525 if let Some((solid_name, coords)) = name.rsplit_once('@') {
526 let mut parts = coords.split(',').map(|p| p.trim().parse::<f64>().ok());
527 let position = Vec3::new(parts.next()??, parts.next()??, parts.next()??);
528 if parts.next().is_some() {
529 return None;
530 }
531 let handle = scene.resolve_solid(solid_name)?;
532 let geometry = crate::with_registered_solid_str(handle, |solid| {
533 resolve_vertex_selection(solid, position).map_err(|e| format!("{e:?}"))
534 })
535 .ok()?;
536 if let SelectionGeometry::Point { position } = geometry {
537 return Some(ResolvedReference::Point(position));
538 }
539 }
540 None
541}
542
543pub fn arc_curve(center: Vec3, start: Vec3, end: Vec3, scale: f64) -> Result<NurbsCurve, String> {
551 let rs = start.sub(center);
552 let re = end.sub(center);
553 let radius = rs.length();
554 if radius <= 1e-9 * scale {
555 return Err("zero radius".into());
556 }
557 let normal = rs.cross(re);
558 if normal.length() <= 1e-9 * radius * radius {
559 return Err("center, start and end are collinear (no arc plane)".into());
560 }
561 let x_axis = rs.normalized()?;
562 let y_axis = normal.normalized()?.cross(x_axis).normalized()?;
563 let sweep = re.dot(y_axis).atan2(re.dot(x_axis));
566 let sweep = if sweep <= 0.0 { sweep + std::f64::consts::TAU } else { sweep };
567 make_arc(center, x_axis, y_axis, radius, 0.0, sweep)
568}
569
570pub fn bezier_chain_curve(controls: &[Vec3]) -> Result<NurbsCurve, String> {
574 let span_count = (controls.len().saturating_sub(1)) / 3;
575 if span_count == 0 || controls.len() != span_count * 3 + 1 {
576 return Err(format!(
577 "3D sketch: bezier needs 3n+1 control points, got {}",
578 controls.len()
579 ));
580 }
581 let mut knots = vec![0.0; 4];
582 for join in 1..span_count {
583 knots.extend_from_slice(&[join as f64; 3]);
584 }
585 knots.extend_from_slice(&[span_count as f64; 4]);
586 let points = controls.iter().map(|p| Vec4::from_point(*p, 1.0)).collect();
587 NurbsCurve::new(3, knots, points)
588}
589
590struct Segment {
595 name: String,
596 start: Vec3,
597 end: Vec3,
598 curve: NurbsCurve,
599}
600
601fn longest_chain(segments: &[Segment], tol: f64) -> Option<(Vec<NurbsCurve>, Vec<Option<String>>)> {
607 if segments.is_empty() {
608 return None;
609 }
610 let close = |a: Vec3, b: Vec3| a.sub(b).length() <= tol;
611 let degree_at = |p: Vec3| -> usize {
613 segments
614 .iter()
615 .map(|s| usize::from(close(s.start, p)) + usize::from(close(s.end, p)))
616 .sum()
617 };
618 let mut best: Option<Vec<(usize, bool)>> = None;
619 for (seed, _) in segments.iter().enumerate() {
621 for seed_reversed in [false, true] {
622 let mut chain: Vec<(usize, bool)> = vec![(seed, seed_reversed)];
623 let mut used = vec![false; segments.len()];
624 used[seed] = true;
625 loop {
627 let (last, reversed) = *chain.last().unwrap();
628 let tail = if reversed { segments[last].start } else { segments[last].end };
629 if degree_at(tail) != 2 {
630 break;
631 }
632 let next = segments.iter().enumerate().find_map(|(i, s)| {
633 if used[i] {
634 return None;
635 }
636 if close(s.start, tail) {
637 Some((i, false))
638 } else if close(s.end, tail) {
639 Some((i, true))
640 } else {
641 None
642 }
643 });
644 match next {
645 Some((i, reversed)) => {
646 used[i] = true;
647 chain.push((i, reversed));
648 }
649 None => break,
650 }
651 }
652 loop {
654 let (first, reversed) = chain[0];
655 let head = if reversed { segments[first].end } else { segments[first].start };
656 if degree_at(head) != 2 {
657 break;
658 }
659 let prev = segments.iter().enumerate().find_map(|(i, s)| {
660 if used[i] {
661 return None;
662 }
663 if close(s.end, head) {
664 Some((i, false))
665 } else if close(s.start, head) {
666 Some((i, true))
667 } else {
668 None
669 }
670 });
671 match prev {
672 Some((i, reversed)) => {
673 used[i] = true;
674 chain.insert(0, (i, reversed));
675 }
676 None => break,
677 }
678 }
679 if best.as_ref().map_or(true, |b| chain.len() > b.len()) {
680 best = Some(chain);
681 }
682 }
683 }
684 let chain = best?;
685 let mut curves = Vec::with_capacity(chain.len());
686 let mut names = Vec::with_capacity(chain.len());
687 for (i, reversed) in chain {
688 let curve = if reversed {
689 segments[i].curve.reversed().ok()?
690 } else {
691 segments[i].curve.clone()
692 };
693 curves.push(curve);
694 names.push(Some(segments[i].name.clone()));
695 }
696 Some((curves, names))
697}
698
699pub fn reference_record(name: &str, resolved: &ResolvedReference, point_ids: &[Value], geometry_id: Option<&Value>) -> Value {
703 let mut map = Map::new();
704 map.insert("name".into(), Value::String(name.to_string()));
705 map.insert("kind".into(), Value::String(resolved.kind().to_string()));
706 map.insert("points".into(), Value::Array(point_ids.to_vec()));
707 map.insert(
708 "geometry".into(),
709 geometry_id.cloned().unwrap_or(Value::Null),
710 );
711 Value::Object(map)
712}
713