1use geop_core_editor::view::{drawn, icons};
28use geop_core_editor::{
29 edit_form::{Edit, EditForm},
30 editing::{
31 Action, CanvasEvent, Gizmo, InHand, ListItem, Number, Pointer, Shape, Style, Tone, Unit,
32 Value, Visual,
33 hit::{Scene, hit_visuals, no_instances},
34 },
35 selectable,
36};
37use geop_core_math::{scalars::Scalar, vector::Vector3};
38use geop_core_program::reader::Reader;
39use geop_core_sketch::{
40 ConstraintId, CurveId, PointId,
41 space::{Arc3, Constraint3d, Coordinate, CurveKind3d, End, Solve3dReport},
42};
43use geop_ops_model::sketch::fields::constraint_status;
44
45const AXIS_SNAP: f64 = 2.0;
49
50use crate::{AddSketch3dArgs, Reference3d, Target};
51use geop_ops_model::sketch3d::{CurveSource, LineSource, PointSource};
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum Tool {
56 Select,
58 Point,
59 Line,
60 Arc,
61 Spline,
62 Helix,
63}
64
65impl Tool {
66 const ALL: [(
70 Tool,
71 &'static str,
72 &'static str,
73 &'static str,
74 Option<&'static str>,
75 ); 6] = [
76 (Tool::Select, "select", "Select", "Escape", None),
77 (Tool::Point, "point", "Point", "p", Some(icons::POINT)),
78 (Tool::Line, "line", "Line", "l", Some(icons::LINE)),
79 (Tool::Arc, "arc", "3-point arc", "a", Some(icons::ARC)),
80 (Tool::Spline, "spline", "Spline", "s", Some(icons::SPLINE)),
81 (
82 Tool::Helix,
83 "helix",
84 "Helix",
85 "h",
86 Some(crate::icons::HELIX),
87 ),
88 ];
89
90 fn draws(self) -> bool {
91 self != Tool::Select
92 }
93}
94
95#[derive(Clone, Debug, Default)]
97pub struct Sketch3dSession {
98 tool: Option<Tool>,
101 placed: Vec<PointId>,
104 fresh: Vec<PointId>,
107 hover: Option<Located>,
109 typed: [f64; 3],
111 turns: Option<f64>,
114 left: bool,
115}
116
117const TURNS: f64 = 5.0;
119
120impl Sketch3dSession {
121 pub fn tool(&self, args: &AddSketch3dArgs) -> Tool {
123 self.tool.unwrap_or_else(|| {
124 if args.sketch.curves.values().any(|c| c.is_drawn()) {
125 Tool::Select
126 } else {
127 Tool::Line
128 }
129 })
130 }
131}
132
133#[derive(Default)]
135struct Picks {
136 points: Vec<PointId>,
137 curves: Vec<CurveId>,
138}
139
140fn moved(args: &AddSketch3dArgs, picks: &Picks) -> Vec<PointId> {
143 let sketch = &args.sketch;
144 let mut points: Vec<PointId> = picks.points.clone();
145 for c in &picks.curves {
146 points.extend(sketch.curves[c].points());
147 }
148 let mut seen = std::collections::BTreeSet::new();
149 points.retain(|p| seen.insert(*p) && !sketch.points[p].fixed);
150 points
151}
152
153fn picked(args: &AddSketch3dArgs, selection: &[String]) -> Picks {
154 let sketch = &args.sketch;
155 let mut picks = Picks::default();
156 for key in selection {
157 if let Some(p) = key.parse().ok().filter(|p| sketch.points.contains_key(p)) {
158 picks.points.push(p);
159 } else if let Some(c) = key.parse().ok().filter(|c| sketch.curves.contains_key(c)) {
160 picks.curves.push(c);
161 }
162 }
163 picks
164}
165
166fn checked(
169 before: &Reader,
170 args: &AddSketch3dArgs,
171) -> Result<(AddSketch3dArgs, Solve3dReport), String> {
172 let mut args = args.clone();
173 args.resolve(before)
174 .map_err(|e| e.root_message().to_string())?;
175 let report = args
176 .sketch
177 .check()
178 .map_err(|e| e.root_message().to_string())?;
179 Ok((args, report))
180}
181
182fn solve(before: &Reader, args: &mut AddSketch3dArgs, drags: &[(PointId, Vector3)]) {
186 args.tidy();
187 if args.resolve(before).is_ok() {
188 let _ = args.sketch.solve_with_drag(drags);
189 }
190}
191
192#[derive(Clone, Debug)]
196enum Located {
197 Point(PointId),
199 Fixed(PointSource, Vector3),
201 OnEdge(CurveSource, Vector3),
203 Along(Coordinate, Vector3),
206 Free(Vector3),
208}
209
210impl Located {
211 fn at(&self, args: &AddSketch3dArgs) -> Option<Vector3> {
215 match self {
216 Located::Point(p) => args.sketch.points.get(p).map(|p| p.at),
217 Located::Fixed(_, at)
218 | Located::OnEdge(_, at)
219 | Located::Along(_, at)
220 | Located::Free(at) => Some(*at),
221 }
222 }
223}
224
225fn along_axis(from: &Vector3, pointer: &Pointer) -> Option<Located> {
229 Coordinate::ALL
230 .into_iter()
231 .filter_map(|axis| {
232 let d = Vector3::axis(axis.index());
233 let at = from.add(&d.prod_scalar(pointer.ray.line_parameter(from, &d)?));
234 let (dist, t) = pointer.ray.distance_to_point(&at);
235 let on_screen = dist.to_f64() / pointer.reach_at(1.0, t).to_f64();
238 pointer
239 .within(dist, t, AXIS_SNAP)
240 .then_some((on_screen, axis, at))
241 })
242 .min_by(|a, b| a.0.total_cmp(&b.0))
243 .map(|(_, axis, at)| Located::Along(axis, at))
244}
245
246fn locate(
250 before: &Reader,
251 scene: Option<&dyn Scene>,
252 args: &AddSketch3dArgs,
253 s: &Sketch3dSession,
254 tool: Tool,
255 pointer: &Pointer,
256 shift: bool,
257) -> Option<Located> {
258 if !shift {
259 let points: Vec<Visual> = args
260 .sketch
261 .points
262 .iter()
263 .map(|(id, p)| Visual::new(id.to_string(), Shape::Point { at: p.at }, Style::Free))
264 .collect();
265 if let Some(hit) = hit_visuals(&points, pointer, no_instances, |_| true) {
266 return Some(Located::Point(hit.visual.key.parse().ok()?));
267 }
268 if let Some(scene) = scene {
269 if let Some((point, _)) = selectable::pick::<PointSource>(scene, before, pointer) {
270 let at = point.resolve(before).ok()?;
271 return Some(Located::Fixed(point, at));
272 }
273 if let Some((curve, at)) = selectable::pick::<CurveSource>(scene, before, pointer) {
274 return Some(Located::OnEdge(curve, at));
275 }
276 }
277 let from = (s.placed.last()).and_then(|p| args.sketch.points.get(p).map(|p| p.at));
278 if tool == Tool::Line
279 && let Some(along) = from.and_then(|from| along_axis(&from, pointer))
280 {
281 return Some(along);
282 }
283 }
284 if let Some(scene) = scene
285 && let Some(hit) = scene.pick(pointer, &[drawn::FACE.into(), drawn::DATUM.into()])
286 {
287 return Some(Located::Free(hit.point));
288 }
289 let anchor = (s.placed.last())
292 .and_then(|p| args.sketch.points.get(p).map(|p| p.at))
293 .unwrap_or_else(Vector3::zero);
294 let (_, at) = pointer.ray.intersect_plane(&anchor, pointer.ray.dir())?;
295 Some(Located::Free(at))
296}
297
298fn place(args: &mut AddSketch3dArgs, s: &mut Sketch3dSession, located: Located) -> PointId {
301 let sketch = &mut args.sketch;
302 match located {
303 Located::Point(p) => p,
304 Located::Fixed(source, at) => {
305 let known = args.references.iter().find_map(|r| match r {
306 Reference3d::Point { point, from } if *from == source => Some(*point),
307 _ => None,
308 });
309 known.unwrap_or_else(|| {
310 let point = sketch.add_fixed_point(at);
311 args.references.push(Reference3d::Point {
312 point,
313 from: source,
314 });
315 s.fresh.push(point);
316 point
317 })
318 }
319 Located::OnEdge(source, at) => {
320 let known = args.references.iter().find_map(|r| match r {
321 Reference3d::Curve { curve, from } if *from == source => Some(*curve),
322 _ => None,
323 });
324 let curve = known.unwrap_or_else(|| {
325 let curve = sketch.add_curve(CurveKind3d::Reference);
326 args.references.push(Reference3d::Curve {
327 curve,
328 from: source,
329 });
330 curve
331 });
332 let point = sketch.add_point(at);
333 sketch.constrain(Constraint3d::OnCurve { point, curve });
334 s.fresh.push(point);
335 point
336 }
337 Located::Along(_, at) | Located::Free(at) => {
338 let point = sketch.add_point(at);
339 s.fresh.push(point);
340 point
341 }
342 }
343}
344
345fn same(args: &AddSketch3dArgs, a: PointId, b: PointId) -> bool {
347 let class = args.sketch.point_classes();
348 class[&a] == class[&b]
349}
350
351fn smooth_joints(args: &mut AddSketch3dArgs, curve: CurveId) {
355 let sketch = &args.sketch;
356 let is_spline = |c: CurveId| matches!(sketch.curves[&c].kind, CurveKind3d::Spline { .. });
357 let others: Vec<CurveId> = sketch
358 .curves
359 .iter()
360 .filter(|&(&c, k)| c != curve && k.is_drawn() && !k.construction)
361 .map(|(&c, _)| c)
362 .collect();
363 let mut tangents = Vec::new();
364 for &other in &others {
365 if !(is_spline(curve) || is_spline(other)) {
366 continue;
367 }
368 let Ok(Some((end, _))) = sketch.shared_end(curve, other) else {
369 continue;
370 };
371 let meeting = others
373 .iter()
374 .filter(|&&c| {
375 sketch
376 .shared_end(curve, c)
377 .ok()
378 .flatten()
379 .is_some_and(|(e, _)| e == end)
380 })
381 .count();
382 if meeting == 1 {
383 tangents.push(Constraint3d::Tangent { a: other, b: curve });
384 }
385 }
386 for tangent in tangents {
387 let id = args.sketch.constrain(tangent);
388 if args.sketch.validate().is_err() {
389 args.sketch.remove(&[], &[], &[id]);
390 }
391 }
392}
393
394fn click(
396 before: &Reader,
397 args: &mut AddSketch3dArgs,
398 s: &mut Sketch3dSession,
399 tool: Tool,
400 located: Located,
401) {
402 let along = match located {
403 Located::Along(axis, _) => Some(axis),
404 _ => None,
405 };
406 let p = place(args, s, located);
407 match tool {
408 Tool::Select => {}
409 Tool::Point => {
410 s.fresh.clear();
411 }
412 Tool::Line => {
413 if let Some(&last) = s.placed.last()
414 && !same(args, last, p)
415 {
416 let line = args.sketch.add_line(last, p);
417 if let Some(axis) = along {
418 args.sketch.constrain(Constraint3d::ParallelTo {
419 line,
420 direction: Vector3::axis(axis.index()),
421 });
422 }
423 smooth_joints(args, line);
424 s.fresh.clear();
425 }
426 s.placed = vec![p];
427 }
428 Tool::Arc => {
429 if s.placed.last().is_none_or(|&last| !same(args, last, p)) {
430 s.placed.push(p);
431 }
432 if let [start, through, end] = s.placed[..] {
433 if !same(args, start, end) {
434 let arc = args.sketch.add_arc(start, through, end);
435 smooth_joints(args, arc);
436 s.fresh.clear();
437 }
438 s.placed = vec![end];
439 }
440 }
441 Tool::Spline => {
442 if s.placed.last().is_none_or(|&last| !same(args, last, p)) {
443 s.placed.push(p);
444 }
445 }
446 Tool::Helix => {
447 if s.placed.iter().all(|&placed| !same(args, placed, p)) {
448 s.placed.push(p);
449 }
450 if let [base, top, start] = s.placed[..] {
451 let turns = Scalar::from_f64(s.turns.unwrap_or(TURNS));
452 if args
453 .sketch
454 .add_helix(start, base, top, turns, s.left)
455 .is_ok()
456 {
457 s.fresh.clear();
458 }
459 s.placed.clear();
460 }
461 }
462 }
463 solve(before, args, &[]);
464}
465
466fn finish(before: &Reader, args: &mut AddSketch3dArgs, s: &mut Sketch3dSession) {
469 if s.tool(args) == Tool::Spline && s.placed.len() >= 2 {
470 let spline = args.sketch.add_spline(std::mem::take(&mut s.placed));
471 smooth_joints(args, spline);
472 s.fresh.clear();
473 }
474 s.placed.clear();
475 let used = |p: &PointId| {
476 args.sketch.curves.values().any(|c| c.points().contains(p))
477 || args
478 .sketch
479 .constraints
480 .values()
481 .any(|k| !matches!(k, Constraint3d::OnCurve { .. }) && k.points().contains(p))
482 };
483 let unused: Vec<PointId> = s.fresh.iter().copied().filter(|p| !used(p)).collect();
484 s.fresh.clear();
485 if !unused.is_empty() {
486 args.sketch.remove(&unused, &[], &[]);
487 }
488 solve(before, args, &[]);
489}
490
491fn take_up(before: &Reader, args: &mut AddSketch3dArgs, s: &mut Sketch3dSession, tool: Tool) {
493 finish(before, args, s);
494 s.tool = Some(tool);
495 s.hover = None;
496}
497
498fn remove_selected(before: &Reader, args: &mut AddSketch3dArgs, selection: &mut Vec<String>) {
500 let picks = picked(args, selection);
501 args.sketch.remove(&picks.points, &picks.curves, &[]);
502 selection.clear();
503 solve(before, args, &[]);
504}
505
506pub(crate) fn event(
510 before: &Reader,
511 scene: Option<&dyn Scene>,
512 edit: Edit<'_, AddSketch3dArgs, Sketch3dSession>,
513 event: &CanvasEvent,
514) {
515 let Edit {
516 args,
517 session: s,
518 selection,
519 ..
520 } = edit;
521 let tool = s.tool(args);
522 s.tool = Some(tool);
523 match event {
524 CanvasEvent::Hover { pointer, shift } => {
525 s.hover = tool
526 .draws()
527 .then(|| locate(before, scene, args, s, tool, pointer, *shift))
528 .flatten();
529 }
530 CanvasEvent::Leave => s.hover = None,
531 CanvasEvent::Click { double: true, .. } if tool.draws() => finish(before, args, s),
532 CanvasEvent::Click { pointer, shift, .. } if tool.draws() => {
533 if let Some(located) = locate(before, scene, args, s, tool, pointer, *shift) {
534 click(before, args, s, tool, located);
535 }
536 s.hover = None;
537 }
538 CanvasEvent::Confirm => finish(before, args, s),
539 CanvasEvent::Cancel if !s.placed.is_empty() => finish(before, args, s),
540 CanvasEvent::Cancel => take_up(before, args, s, Tool::Select),
541 CanvasEvent::Delete => remove_selected(before, args, selection),
542 CanvasEvent::Move { key, to, .. } => {
543 let point = key.parse().ok();
544 let free = |p: &PointId| args.sketch.points.get(p).is_some_and(|p| !p.fixed);
545 if let Some(p) = point.filter(free) {
546 solve(before, args, &[(p, *to)]);
547 }
548 }
549 CanvasEvent::Gizmo { drag, .. } => {
552 let points = moved(args, &picked(args, selection));
553 let drags: Vec<(PointId, Vector3)> = points
554 .into_iter()
555 .map(|p| (p, drag.apply(&args.sketch.points[&p].at)))
556 .collect();
557 solve(before, args, &drags);
558 }
559 CanvasEvent::Key { key } => {
560 if let Some(&(tool, ..)) = Tool::ALL.iter().find(|t| t.3 == key) {
561 take_up(before, args, s, tool);
562 }
563 }
564 _ => {}
565 }
566}
567
568fn hint(tool: Tool, placed: usize) -> &'static str {
572 match (tool, placed) {
573 (Tool::Select, _) => {
574 "Select points and curves to constrain them or move them with the gizmo, or drag points"
575 }
576 (Tool::Point, _) => "Click to place a point: on the part, or in space",
577 (Tool::Line, 0) => "Click where the line starts",
578 (Tool::Line, _) => "Click the next point · Esc ends the chain",
579 (Tool::Arc, 0) => "Click where the arc starts",
580 (Tool::Arc, 1) => "Click a point it passes through",
581 (Tool::Arc, _) => "Click where it ends",
582 (Tool::Spline, _) => "Click the points it passes through · double-click or Enter finishes",
583 (Tool::Helix, 0) => "Click the bottom of its axis",
584 (Tool::Helix, 1) => "Click the top of its axis: how far it rises",
585 (Tool::Helix, _) => "Click where it starts: how far from the axis says its radius",
586 }
587}
588
589fn axis_of(direction: &Vector3) -> Option<Coordinate> {
591 Coordinate::ALL.into_iter().find(|axis| {
592 (0..3).all(|k| {
593 let want = if k == axis.index() { 1.0 } else { 0.0 };
594 direction[k].to_f64() == want
595 })
596 })
597}
598
599fn describe(args: &AddSketch3dArgs, id: ConstraintId, c: &Constraint3d) -> String {
601 use Constraint3d::*;
602 let direction =
603 |direction: &Vector3| match args.reference_of(Target::Direction { constraint: id }) {
604 Some(r) => r.source().label(),
605 None => match axis_of(direction) {
606 Some(axis) => axis.name().to_string(),
607 None => "a direction".to_string(),
608 },
609 };
610 let end = |e: End| match e {
611 End::Start => "start",
612 End::End => "end",
613 };
614 match c {
615 Coincident { a, b } => format!("Coincident {a} {b}"),
616 Coordinate { point, axis, .. } => format!("{} of {point}", axis.name()),
617 Distance { a, b, .. } => format!("Distance {a} {b}"),
618 Length { line, .. } => format!("Length {line}"),
619 Radius { arc, .. } => format!("Radius {arc}"),
620 Parallel { a, b } => format!("Parallel {a} {b}"),
621 ParallelTo { line, direction: d } => format!("{line} along {}", direction(d)),
622 TangentTo {
623 curve,
624 end: e,
625 direction: d,
626 } => format!("{curve} {} along {}", end(*e), direction(d)),
627 OnCurve { point, curve } => match args.reference_of(Target::Curve { curve: *curve }) {
628 Some(r) => format!("{point} on {}", r.source().label()),
629 None => format!("{point} on {curve}"),
630 },
631 Tangent { a, b } => format!("Tangent {a} {b}"),
632 HelixEnd { helix } => format!("End of {helix}"),
633 }
634}
635
636fn constraints_for(
639 args: &AddSketch3dArgs,
640 picks: &Picks,
641) -> Vec<(&'static str, &'static str, Vec<Constraint3d>)> {
642 let sketch = &args.sketch;
643 let kind = |c: &CurveId| &sketch.curves[c].kind;
644 let lines: Vec<CurveId> = picks
645 .curves
646 .iter()
647 .copied()
648 .filter(|c| matches!(kind(c), CurveKind3d::Line { .. }))
649 .collect();
650 let at = |p: &PointId| sketch.points[p].at;
651 let dist = |a: &Vector3, b: &Vector3| b.sub(a).norm();
652 let (np, nc) = (picks.points.len(), picks.curves.len());
653 let mut out = Vec::new();
654 if np == 2 && nc == 0 {
655 let (a, b) = (picks.points[0], picks.points[1]);
656 out.push((
657 "coincident",
658 "Coincident",
659 vec![Constraint3d::Coincident { a, b }],
660 ));
661 out.push((
662 "distance",
663 "Distance",
664 vec![Constraint3d::Distance {
665 a,
666 b,
667 value: dist(&at(&a), &at(&b)),
668 }],
669 ));
670 }
671 if np >= 1 && nc == 0 {
672 let fixes = picks
673 .points
674 .iter()
675 .filter(|p| !sketch.points[p].fixed)
676 .flat_map(|&point| {
677 Coordinate::ALL.map(|axis| Constraint3d::Coordinate {
678 point,
679 axis,
680 value: at(&point)[axis.index()],
681 })
682 })
683 .collect::<Vec<_>>();
684 if !fixes.is_empty() {
685 out.push(("fix", "Fix", fixes));
686 }
687 }
688 if np == 0 && nc == 1 {
689 let c = picks.curves[0];
690 match kind(&c) {
691 CurveKind3d::Line { start, end } => out.push((
692 "length",
693 "Length",
694 vec![Constraint3d::Length {
695 line: c,
696 value: dist(&at(start), &at(end)),
697 }],
698 )),
699 CurveKind3d::Arc {
700 start,
701 through,
702 end,
703 } => {
704 let arc = Arc3 {
705 s: at(start),
706 m: at(through),
707 e: at(end),
708 };
709 if let Ok(value) = arc.radius() {
710 out.push((
711 "radius",
712 "Radius",
713 vec![Constraint3d::Radius { arc: c, value }],
714 ));
715 }
716 }
717 _ => {}
718 }
719 }
720 if np == 0 && lines.len() == 2 {
721 out.push((
722 "parallel",
723 "Parallel",
724 vec![Constraint3d::Parallel {
725 a: lines[0],
726 b: lines[1],
727 }],
728 ));
729 }
730 if np == 0 && nc == 2 {
731 let (a, b) = (picks.curves[0], picks.curves[1]);
732 if sketch.shared_end(a, b).ok().flatten().is_some() {
733 out.push(("tangent", "Tangent", vec![Constraint3d::Tangent { a, b }]));
734 }
735 }
736 if np == 1
737 && nc == 1
738 && matches!(
739 kind(&picks.curves[0]),
740 CurveKind3d::Line { .. } | CurveKind3d::Arc { .. }
741 )
742 {
743 out.push((
744 "on_curve",
745 "On curve",
746 vec![Constraint3d::OnCurve {
747 point: picks.points[0],
748 curve: picks.curves[0],
749 }],
750 ));
751 }
752 for (name, label, axis) in [
753 ("along_x", "Along x", Coordinate::X),
754 ("along_y", "Along y", Coordinate::Y),
755 ("along_z", "Along z", Coordinate::Z),
756 ] {
757 if let Some(c) = along(args, picks, Vector3::axis(axis.index())) {
758 out.push((name, label, vec![c]));
759 }
760 }
761 out
762}
763
764fn along(args: &AddSketch3dArgs, picks: &Picks, direction: Vector3) -> Option<Constraint3d> {
768 let sketch = &args.sketch;
769 let [curve] = picks.curves[..] else {
770 return None;
771 };
772 match (&sketch.curves[&curve].kind, &picks.points[..]) {
773 (CurveKind3d::Line { .. }, []) => Some(Constraint3d::ParallelTo {
774 line: curve,
775 direction,
776 }),
777 (CurveKind3d::Arc { .. } | CurveKind3d::Spline { .. }, [p]) => {
778 let (start, end) = sketch.curves[&curve].endpoints()?;
779 let end = if same(args, *p, start) {
780 End::Start
781 } else if same(args, *p, end) {
782 End::End
783 } else {
784 return None;
785 };
786 Some(Constraint3d::TangentTo {
787 curve,
788 end,
789 direction,
790 })
791 }
792 _ => None,
793 }
794}
795
796fn status(checked: &Result<(AddSketch3dArgs, Solve3dReport), String>) -> (String, Tone) {
798 let (args, report) = match checked {
799 Ok(checked) => checked,
800 Err(e) => return (e.clone(), Tone::Error),
801 };
802 let chains = match args.sketch.chains() {
803 Ok(chains) if chains.is_empty() => "nothing drawn yet".to_string(),
804 Ok(chains) => format!(
805 "{} chain{}",
806 chains.len(),
807 if chains.len() == 1 { "" } else { "s" }
808 ),
809 Err(e) => e.root_message().to_string(),
810 };
811 let conflicts = report.failed_constraints.len();
812 constraint_status(report.converged, conflicts, report.dof, &chains)
813}
814
815fn visuals(
818 args: &AddSketch3dArgs,
819 s: &Sketch3dSession,
820 report: Option<&Solve3dReport>,
821) -> Vec<Visual> {
822 let sketch = &args.sketch;
823 let failed: Vec<&Constraint3d> = report
824 .map(|r| {
825 r.failed_constraints
826 .iter()
827 .filter_map(|k| sketch.constraints.get(k))
828 .collect()
829 })
830 .unwrap_or_default();
831 let free = |p: &PointId| report.is_none_or(|r| r.free_points.get(p).copied().unwrap_or(true));
832 let mut out = Vec::new();
833 for (&id, curve) in sketch.curves.iter().filter(|(_, c)| c.is_drawn()) {
834 let Ok(polyline) = sketch.curve_polyline(id) else {
835 continue;
836 };
837 let style = if failed.iter().any(|k| k.curves().contains(&id)) {
838 Style::Failed
839 } else if curve.construction {
840 Style::Construction
841 } else if curve.points().iter().any(free) {
842 Style::Free
843 } else {
844 Style::Fixed
845 };
846 out.push(
847 Visual::new(
848 id.to_string(),
849 Shape::Polyline {
850 points: polyline.to_vec(),
851 },
852 style,
853 )
854 .selectable(),
855 );
856 }
857 for (&id, p) in &sketch.points {
858 let style = if p.fixed {
859 Style::Reference
860 } else if failed.iter().any(|k| k.points().contains(&id)) {
861 Style::Failed
862 } else if free(&id) {
863 Style::Free
864 } else {
865 Style::Fixed
866 };
867 let visual = Visual::new(id.to_string(), Shape::Point { at: p.at }, style).selectable();
868 out.push(if p.fixed { visual } else { visual.draggable() });
869 }
870 let at = |p: &PointId| sketch.points.get(p).map(|p| p.at);
871 let last = s.placed.last().and_then(at);
872 if let Some(located) = &s.hover
873 && let Some(hover) = located.at(args)
874 {
875 let mut draft: Vec<Vector3> = match s.tool(args) {
876 Tool::Spline | Tool::Arc | Tool::Helix => s.placed.iter().filter_map(at).collect(),
877 _ => last.into_iter().collect(),
878 };
879 draft.push(hover);
880 if draft.len() > 1 {
881 out.push(Visual::new(
882 "draft",
883 Shape::Polyline {
884 points: draft.to_vec(),
885 },
886 Style::Draft,
887 ));
888 }
889 if let (Located::Along(axis, _), Some(from)) = (located, last) {
890 out.push(Visual::new(
893 "snap_axis",
894 Shape::Polyline {
895 points: vec![from, hover],
896 },
897 Style::Snap,
898 ));
899 out.push(Visual::new(
900 "snap_label",
901 Shape::Label {
902 at: hover,
903 text: axis.name().to_string(),
904 offset: Vector3::from_array([Scalar::ONE, Scalar::ONE, Scalar::ZERO]),
905 },
906 Style::Snap,
907 ));
908 }
909 let style = match located {
910 Located::Free(_) => Style::Draft,
911 _ => Style::Snap,
912 };
913 out.push(Visual::new("snap", Shape::Point { at: hover }, style));
914 }
915 let anchor = last.or_else(|| s.hover.as_ref().and_then(|l| l.at(args)));
918 if let Some(at) = anchor.filter(|_| s.tool(args).draws()) {
919 out.push(Visual::new(
920 "triad",
921 Shape::Triad {
922 at,
923 axes: [0, 1, 2].map(Vector3::axis),
924 },
925 Style::Guide,
926 ));
927 }
928 out
929}
930
931pub(crate) fn form<'a>(
934 before: &'a Reader<'a>,
935 args: &AddSketch3dArgs,
936 s: &Sketch3dSession,
937 selection: &[String],
938) -> EditForm<'a, AddSketch3dArgs, Sketch3dSession> {
939 let mut f = EditForm::default();
940 let tool = s.tool(args);
941 f.form.tool = if tool.draws() {
942 InHand::Clicks
943 } else {
944 InHand::Nothing
945 };
946 let mut tools: Vec<Action> = Tool::ALL
947 .iter()
948 .map(|&(t, name, label, _, icon)| {
949 Action::new(name, label)
950 .group("Draw")
951 .active(t == tool)
952 .icon(icon)
953 })
954 .collect();
955 let finishing = if tool == Tool::Spline {
956 "Finish the spline"
957 } else {
958 "End what is drawn"
959 };
960 tools.push(
961 Action::new("finish", "Finish")
962 .title(finishing)
963 .group("Draw"),
964 );
965 f.actions("tools", tools, move |edit, value| {
966 let Edit { args, session, .. } = edit;
967 match value {
968 "finish" => finish(before, args, session),
969 name => {
970 if let Some(&(t, ..)) = Tool::ALL.iter().find(|t| t.1 == name) {
971 take_up(before, args, session, t);
972 }
973 }
974 }
975 });
976 (f.form.dialog).text("hint", hint(tool, s.placed.len()), Tone::Hint);
977
978 let picks = picked(args, selection);
981 match (&picks.points[..], &picks.curves[..]) {
982 ([p], []) if !args.sketch.points[p].fixed => {
983 let at = args.sketch.points[p].at;
984 let p = *p;
985 for axis in Coordinate::ALL {
986 f.number(
987 axis.name(),
988 Number::new(axis.name(), at[axis.index()].to_f64(), Unit::Length),
989 move |Edit { args, .. }, v| {
990 let mut to = args.sketch.points[&p].at;
991 to[axis.index()] = Scalar::from_f64(v);
992 solve(before, args, &[(p, to)]);
993 },
994 );
995 }
996 }
997 _ if tool.draws() => {
998 for axis in Coordinate::ALL {
999 let number = Number::new(axis.name(), s.typed[axis.index()], Unit::Length);
1000 f.number(axis.name(), number, move |edit, v| {
1001 edit.session.typed[axis.index()] = v;
1002 });
1003 }
1004 f.actions(
1005 "place",
1006 vec![
1007 Action::new("place", "Place point")
1008 .title("Place a point at the coordinates typed"),
1009 ],
1010 move |edit, _| {
1011 let Edit { args, session, .. } = edit;
1012 let at = Vector3::from_array(session.typed.map(Scalar::from_f64));
1013 let tool = session.tool(args);
1014 click(before, args, session, tool, Located::Free(at));
1015 },
1016 );
1017 }
1018 _ => {}
1019 }
1020
1021 let selected = match picks.curves[..] {
1024 [c] if picks.points.is_empty() => match args.sketch.curves[&c].kind {
1025 CurveKind3d::Helix { turns, left, .. } => Some((c, turns.to_f64(), left)),
1026 _ => None,
1027 },
1028 _ => None,
1029 };
1030 let helix = selected
1031 .map(|(_, turns, left)| (turns, left))
1032 .or_else(|| (tool == Tool::Helix).then(|| (s.turns.unwrap_or(TURNS), s.left)));
1033 if let Some((turns, left)) = helix {
1034 let curve = selected.map(|(c, ..)| c);
1035 let set = move |edit: Edit<'_, AddSketch3dArgs, Sketch3dSession>,
1036 to_turns: Option<f64>,
1037 to_left: Option<bool>| {
1038 let Edit { args, session, .. } = edit;
1039 match curve {
1040 Some(c) => {
1041 if let Some(CurveKind3d::Helix { turns, left, .. }) =
1042 args.sketch.curves.get_mut(&c).map(|c| &mut c.kind)
1043 {
1044 *turns = to_turns.map_or(*turns, Scalar::from_f64);
1045 *left = to_left.unwrap_or(*left);
1046 }
1047 solve(before, args, &[]);
1048 }
1049 None => {
1050 session.turns = to_turns.or(session.turns);
1051 session.left = to_left.unwrap_or(session.left);
1052 }
1053 }
1054 };
1055 let number = Number::new("turns", turns, Unit::Count).range(0.25, 1000.0);
1056 f.number("helix_turns", number, move |edit, v| {
1057 set(edit, Some(v), None)
1058 });
1059 f.checkbox("helix_left", "Left-handed", left, move |edit, b| {
1060 set(edit, None, Some(b));
1061 });
1062 }
1063
1064 let available = constraints_for(args, &picks);
1066 if !available.is_empty() {
1067 let actions = available
1068 .iter()
1069 .map(|(name, label, _)| Action::new(*name, *label).group("Constrain"))
1070 .collect();
1071 f.actions("constrain", actions, move |edit, value| {
1072 let Edit {
1073 args, selection, ..
1074 } = edit;
1075 let picks = picked(args, selection);
1076 if let Some((_, _, constraints)) = constraints_for(args, &picks)
1077 .into_iter()
1078 .find(|(name, ..)| *name == value)
1079 {
1080 for c in constraints {
1081 args.sketch.constrain(c);
1082 }
1083 selection.clear();
1084 solve(before, args, &[]);
1085 }
1086 });
1087 }
1088 if along(args, &picks, Vector3::from_array([Scalar::ONE; 3])).is_some() {
1089 f.pick(
1090 "along",
1091 "Along an axis or edge",
1092 before,
1093 &[] as &[LineSource],
1094 false,
1095 move |edit, lines| {
1096 let Edit {
1097 args, selection, ..
1098 } = edit;
1099 let [line] = &lines[..] else {
1100 return;
1101 };
1102 let Ok(axis) = line.resolve(before) else {
1103 return;
1104 };
1105 let picks = picked(args, selection);
1106 if let Some(c) = along(args, &picks, axis.direction) {
1107 let constraint = args.sketch.constrain(c);
1108 args.references.push(Reference3d::Direction {
1109 constraint,
1110 from: line.clone(),
1111 });
1112 selection.clear();
1113 solve(before, args, &[]);
1114 }
1115 },
1116 );
1117 f.form.dialog.optional("along");
1118 }
1119
1120 let checked = checked(before, args);
1121 let (text, tone) = status(&checked);
1122 f.form.dialog.text("status", text, tone);
1123
1124 let items = args
1125 .sketch
1126 .constraints
1127 .iter()
1128 .map(|(&id, c)| {
1129 let mut item = ListItem::new(id.to_string(), describe(args, id, c));
1130 item.removable = true;
1131 item.value = c.value().map(|v| v.to_f64());
1132 if let Ok((_, report)) = &checked
1133 && report.failed_constraints.contains(&id)
1134 {
1135 item.tone = Tone::Error;
1136 }
1137 item
1138 })
1139 .collect();
1140 (f.form.dialog).list("constraints", items, "No constraints yet");
1141 for &id in args.sketch.constraints.keys() {
1142 f.on(id.to_string(), move |edit, value| {
1143 let args = edit.args;
1144 match value {
1145 Value::Remove => {
1146 args.sketch.remove(&[], &[], &[id]);
1147 solve(before, args, &[]);
1148 }
1149 Value::Number(v) => {
1150 if let Some(c) = args.sketch.constraints.get_mut(&id) {
1151 c.set_value(Scalar::from_f64(v));
1152 solve(before, args, &[]);
1153 }
1154 }
1155 _ => {}
1156 }
1157 });
1158 }
1159 f.form.visuals = visuals(args, s, checked.as_ref().ok().map(|(_, r)| r));
1160 f.form.selection = selection.to_vec();
1161 let moving = moved(args, &picks);
1164 if tool == Tool::Select && !moving.is_empty() {
1165 let sum = moving
1166 .iter()
1167 .fold(Vector3::zero(), |sum, p| sum.add(&args.sketch.points[p].at));
1168 let n = Scalar::from_f64(moving.len() as f64);
1169 if let Ok(inverse) = Scalar::ONE.div(n) {
1170 let middle = sum.prod_scalar(inverse);
1171 f.form.gizmo = Some(Gizmo::new(middle).translate());
1172 }
1173 }
1174 f
1175}