1use super::*;
24use crate::sketch3d::{Sketch3dDoc, Sketch3dSession, WorkPlane};
25use serde_json::{json, Value};
26
27#[derive(Clone)]
30pub struct Sketch3dDrag {
31 pub(super) points: Vec<(Value, [f64; 3], bool)>,
33 pub(super) plane: crate::sketch::PlaneFrame,
35 pub(super) anchor: [f64; 3],
37}
38
39#[derive(Clone, Debug, PartialEq)]
41pub enum Sketch3dCursorHit {
42 SketchPoint(Value),
44 Vertex { solid: String, position: [f64; 3] },
46 Edge { name: String, position: [f64; 3] },
48 Face { name: String, position: [f64; 3] },
50 WorkPlane([f64; 3]),
52}
53
54impl Sketch3dCursorHit {
55 pub fn kind(&self) -> &'static str {
56 match self {
57 Sketch3dCursorHit::SketchPoint(_) => "point",
58 Sketch3dCursorHit::Vertex { .. } => "vertex",
59 Sketch3dCursorHit::Edge { .. } => "edge",
60 Sketch3dCursorHit::Face { .. } => "face",
61 Sketch3dCursorHit::WorkPlane(_) => "workPlane",
62 }
63 }
64}
65
66pub struct Sketch3dEdit {
68 pub session: Sketch3dSession,
69 pub feature_id: String,
70 prev_rollback: usize,
71 is_new: bool,
72 creation_step: Option<u64>,
73 pub(super) drag: Option<Sketch3dDrag>,
74 pub(super) pending: Vec<Value>,
76 pub(super) hover_world: Option<[f64; 3]>,
78 pub(super) undo_stack: Vec<Sketch3dSnapshot>,
79 pub(super) redo_stack: Vec<Sketch3dSnapshot>,
80 pub(super) external_refs: Vec<Value>,
83 pub(super) listed_unlinked: Vec<String>,
86 pub(super) last_hit: Option<&'static str>,
88}
89
90#[derive(Clone)]
91pub(super) struct Sketch3dSnapshot {
92 doc: Sketch3dDoc,
93 selection: Vec<Value>,
94 external_refs: Vec<Value>,
95 work_plane: WorkPlane,
96}
97
98impl Sketch3dEdit {
99 fn snapshot(&self) -> Sketch3dSnapshot {
100 Sketch3dSnapshot {
101 doc: self.session.doc.clone(),
102 selection: self.session.selection.items.clone(),
103 external_refs: self.external_refs.clone(),
104 work_plane: self.session.work_plane.clone(),
105 }
106 }
107
108 pub(super) fn record_undo(&mut self) {
109 let snap = self.snapshot();
110 self.undo_stack.push(snap);
111 self.redo_stack.clear();
112 }
113
114 fn restore(&mut self, snap: Sketch3dSnapshot) {
115 self.session.doc = snap.doc;
116 self.session.selection.items = snap.selection;
117 self.session.selection.hovered = None;
118 self.external_refs = snap.external_refs;
119 self.session.work_plane = snap.work_plane;
120 self.pending.clear();
121 self.drag = None;
122 let _ = self.session.resolve();
123 }
124}
125
126fn reference_record(name: &str, resolved: &brep_kernel::Sketch3dResolvedReference, points: &[Value], geometry: Option<&Value>) -> Value {
130 brep_kernel::sketch3d_reference_record(name, resolved, points, geometry)
131}
132
133impl EngineState {
134 pub fn sketch3d_mode(&self) -> bool {
136 self.sketch3d_edit.is_some()
137 }
138
139 pub fn any_sketch_mode(&self) -> bool {
143 self.sketch_mode() || self.sketch3d_mode()
144 }
145
146 pub fn sketch3d_edit_session(&self) -> Option<&Sketch3dSession> {
147 self.sketch3d_edit.as_ref().map(|edit| &edit.session)
148 }
149
150 pub fn sketch3d_edit_feature_id(&self) -> Option<&str> {
151 self.sketch3d_edit.as_ref().map(|edit| edit.feature_id.as_str())
152 }
153
154 pub fn enter_sketch3d_mode(&mut self, feature_id: &str) -> Result<String, String> {
160 if self.sketch_mode() {
161 return Err("a planar sketch is being edited; finish it first".into());
162 }
163 if self.sketch3d_mode() {
164 return Err("a 3D sketch is already being edited".into());
165 }
166 let index = self
167 .history
168 .index_of(feature_id)
169 .ok_or_else(|| format!("no feature with id '{feature_id}'"))?;
170 let ty = self.history.feature_type(index).unwrap_or_default();
171 if ty != brep_kernel::SKETCH3D_FEATURE_TYPE {
172 return Err(format!("feature '{feature_id}' is not a 3D sketch (type \"S3D\")"));
173 }
174
175 let prev_rollback = self.history.rollback();
178 self.history.set_rollback(index.saturating_sub(1));
179 self.rerun_history();
180
181 let persistent = self.history.feature_persistent_data(index);
182 let doc_value = persistent
183 .as_ref()
184 .and_then(|p| p.get(brep_kernel::SKETCH3D_DOC_KEY))
185 .cloned()
186 .unwrap_or_else(|| json!({ "points": [], "geometries": [], "constraints": [] }));
187 let doc: Sketch3dDoc = serde_json::from_value(doc_value)
188 .map_err(|error| format!("3D sketch doc parse: {error}"))?;
189 let mut session = Sketch3dSession::new(doc)?;
190 session.colors = self.settings.sketch_colors();
191 if let Some(name) = persistent
192 .as_ref()
193 .and_then(|p| p.get("workPlane"))
194 .and_then(Value::as_str)
195 {
196 if let Some(plane) = WorkPlane::new(name, [0.0; 3]) {
197 session.work_plane = plane;
198 }
199 }
200 let external_refs: Vec<Value> = persistent
201 .as_ref()
202 .and_then(|p| p.get(brep_kernel::SKETCH3D_REFS_KEY))
203 .and_then(Value::as_array)
204 .cloned()
205 .unwrap_or_default();
206 let listed = brep_kernel::reference_names(
207 self.history
208 .feature_params(index)
209 .as_ref()
210 .and_then(|p| p.get(brep_kernel::SKETCH3D_REFERENCES_PARAM)),
211 );
212
213 self.clear_selection();
214 self.set_sketch3d_overlay(&session);
215 self.sketch3d_edit = Some(Sketch3dEdit {
216 session,
217 feature_id: feature_id.to_string(),
218 prev_rollback,
219 is_new: false,
220 creation_step: None,
221 drag: None,
222 pending: Vec::new(),
223 hover_world: None,
224 undo_stack: Vec::new(),
225 redo_stack: Vec::new(),
226 external_refs,
227 listed_unlinked: Vec::new(),
228 last_hit: None,
229 });
230 self.sketch3d_sync_references(&listed);
235 self.refresh_committed_sketches();
236 self.refresh_sketch3d_overlay();
237 self.dirty = true;
238 Ok(self.sketch3d_diagnostics_json())
239 }
240
241 pub fn exit_sketch3d_mode(&mut self, commit: bool) -> String {
245 let Some(edit) = self.sketch3d_edit.take() else {
246 return "{}".to_string();
247 };
248 if commit {
249 if let Some(index) = self.history.index_of(&edit.feature_id) {
250 let sketch_value = serde_json::to_value(&edit.session.doc)
251 .unwrap_or_else(|_| json!({ "points": [], "geometries": [], "constraints": [] }));
252 let refs_value = Value::Array(edit.external_refs.clone());
253 let fields = vec![
254 (brep_kernel::SKETCH3D_DOC_KEY, sketch_value),
255 (brep_kernel::SKETCH3D_REFS_KEY, refs_value),
256 ("workPlane", Value::String(edit.session.work_plane.name.clone())),
257 ];
258 match edit.creation_step {
259 Some(step) => {
260 self.history.set_feature_persistent_fields_in_step(index, fields, step);
261 }
262 None => {
263 self.history.set_feature_persistent_fields(index, fields);
264 }
265 }
266 let current = self.history.feature_params(index);
270 let listed = brep_kernel::reference_names(
271 current.as_ref().and_then(|p| p.get(brep_kernel::SKETCH3D_REFERENCES_PARAM)),
272 );
273 let linked: Vec<String> = edit
274 .external_refs
275 .iter()
276 .filter_map(|r| r.get("name").and_then(Value::as_str).map(String::from))
277 .collect();
278 let mut names: Vec<String> = listed
279 .iter()
280 .filter(|n| linked.contains(n) || edit.listed_unlinked.contains(n))
281 .cloned()
282 .collect();
283 for name in &linked {
284 if !names.contains(name) {
285 names.push(name.clone());
286 }
287 }
288 if names != listed {
289 self.history.fold_param_no_undo(
290 &edit.feature_id,
291 brep_kernel::SKETCH3D_REFERENCES_PARAM,
292 Value::Array(names.into_iter().map(Value::String).collect()),
293 );
294 }
295 }
296 } else if edit.is_new {
297 if edit.creation_step.is_some_and(|step| self.history.retract(step)) {
298 self.reinstall_rewound_parts_library();
299 } else if let Some(index) = self.history.index_of(&edit.feature_id) {
300 self.history.remove_feature(index);
301 }
302 }
303 self.history.set_rollback(edit.prev_rollback);
304 self.clear_sketch3d_overlay();
305 self.dirty = true;
306 self.rerun_history()
307 }
308
309 pub fn new_sketch3d(&mut self) -> Result<String, String> {
312 let id = self
313 .history
314 .next_feature_id(&crate::features::feature_short_name(brep_kernel::SKETCH3D_FEATURE_TYPE));
315 let feature = json!({
316 "type": brep_kernel::SKETCH3D_FEATURE_TYPE,
317 "inputParams": { "id": id },
318 "persistentData": {
319 brep_kernel::SKETCH3D_DOC_KEY: { "points": [], "geometries": [], "constraints": [] }
320 }
321 });
322 self.history.push_feature(feature);
323 let creation_step = self.history.undo_top();
324 let diagnostics = self.enter_sketch3d_mode(&id)?;
325 if let Some(edit) = self.sketch3d_edit.as_mut() {
326 edit.is_new = true;
327 edit.creation_step = creation_step;
328 }
329 Ok(diagnostics)
330 }
331
332 fn set_sketch3d_overlay(&mut self, session: &Sketch3dSession) {
335 let wpp = self.camera.world_per_pixel();
336 let _ = self.set_overlay_json(&session.overlay_json(wpp));
337 let _ = self.set_overlay_json(&session.workplane_overlay_json(wpp));
338 self.sketch3d_overlay_wpp = if wpp > 0.0 { wpp } else { f64::MIN_POSITIVE };
339 }
340
341 pub(super) fn refresh_sketch3d_overlay(&mut self) {
342 let wpp = self.camera.world_per_pixel();
343 let (geometry, preview, workplane) = match self.sketch3d_edit.as_ref() {
344 Some(edit) => (
345 edit.session.overlay_json(wpp),
346 edit.session.preview_overlay_json(&edit.pending, edit.hover_world),
347 edit.session.workplane_overlay_json(wpp),
348 ),
349 None => return,
350 };
351 let _ = self.set_overlay_json(&geometry);
352 let _ = self.set_overlay_json(&preview);
353 let _ = self.set_overlay_json(&workplane);
354 self.sketch3d_overlay_wpp = if wpp > 0.0 { wpp } else { f64::MIN_POSITIVE };
355 }
356
357 pub(super) fn ensure_sketch3d_overlay_current(&mut self) {
360 if !self.sketch3d_mode() {
361 self.sketch3d_overlay_wpp = 0.0;
362 return;
363 }
364 let wpp = self.camera.world_per_pixel();
365 if super::overlay_wpp_stale(self.sketch3d_overlay_wpp, wpp) {
366 self.refresh_sketch3d_overlay();
367 }
368 }
369
370 pub fn clear_sketch3d_overlay(&mut self) {
371 let groups: Vec<Value> = crate::sketch3d::OVERLAY_GROUPS
372 .iter()
373 .map(|name| json!({ "name": name }))
374 .collect();
375 let _ = self.set_overlay_json(&json!({ "groups": groups }).to_string());
376 self.sketch3d_overlay_wpp = 0.0;
377 }
378
379 pub fn sketch3d_diagnostics_json(&self) -> String {
382 self.sketch3d_edit
383 .as_ref()
384 .and_then(|edit| serde_json::to_string(&edit.session.diagnostics).ok())
385 .unwrap_or_else(|| "{}".to_string())
386 }
387
388 pub fn sketch3d_state_json(&self) -> String {
391 let Some(edit) = self.sketch3d_edit.as_ref() else {
392 return json!({ "sketch3dMode": false }).to_string();
393 };
394 let session = &edit.session;
395 json!({
396 "sketch3dMode": true,
397 "featureId": edit.feature_id,
398 "dof": session.diagnostics.dof,
399 "status": session.diagnostics.status,
400 "conflicting": session.diagnostics.conflicting,
401 "conflictingConstraints": session.diagnostics.conflicting_constraints,
402 "redundant": session.diagnostics.redundant,
403 "tool": session.tool.clone().unwrap_or_else(|| "select".into()),
404 "workPlane": session.work_plane.name,
405 "workPlaneAnchor": session.work_plane.anchor,
406 "pointCount": session.doc.points.len(),
407 "geometryCount": session.doc.geometries.len(),
408 "constraintCount": session.doc.constraints.len(),
409 "pendingLen": edit.pending.len(),
410 "selection": session.selection.items,
411 "constraintGlyphs": session
412 .constraint_glyphs(self.camera.world_per_pixel())
413 .into_iter()
414 .map(|(id, position, shape)| json!({ "id": id, "position": position, "shape": shape }))
415 .collect::<Vec<_>>(),
416 "hovered": session.selection.hovered,
417 "applicableConstraints": session.applicable_constraints(),
418 "dimensions": session.dimension_labels(),
419 "externalRefs": edit.external_refs,
420 "lostReferences": session.lost_reference_messages(),
421 "lastHit": edit.last_hit,
422 "canUndo": !edit.undo_stack.is_empty(),
423 "canRedo": !edit.redo_stack.is_empty(),
424 "entities": session.entity_rows_json(),
425 })
426 .to_string()
427 }
428
429 pub fn sketch3d_dimension_labels_json(&self) -> String {
431 self.sketch3d_edit
432 .as_ref()
433 .and_then(|edit| serde_json::to_string(&edit.session.dimension_labels()).ok())
434 .unwrap_or_else(|| "[]".to_string())
435 }
436
437 pub fn sketch3d_active_tool(&self) -> Option<&str> {
438 self.sketch3d_edit.as_ref().and_then(|edit| edit.session.tool.as_deref())
439 }
440
441 pub fn sketch3d_selection_count(&self) -> usize {
442 self.sketch3d_edit.as_ref().map_or(0, |edit| edit.session.selection.items.len())
443 }
444
445 pub fn sketch3d_pending_len(&self) -> usize {
446 self.sketch3d_edit.as_ref().map_or(0, |edit| edit.pending.len())
447 }
448
449 pub fn sketch3d_work_plane(&self) -> Option<&str> {
450 self.sketch3d_edit.as_ref().map(|edit| edit.session.work_plane.name.as_str())
451 }
452
453 pub fn sketch3d_set_tool(&mut self, tool: Option<&str>) -> Result<(), String> {
458 let tool = match tool {
459 None | Some("select") | Some("") => None,
460 Some(name) if crate::sketch3d::TOOLS.contains(&name) => Some(name.to_string()),
461 Some(other) => return Err(format!("unknown 3D sketch tool '{other}'")),
462 };
463 let Some(edit) = self.sketch3d_edit.as_mut() else {
464 return Err("not in 3D sketch mode".into());
465 };
466 edit.session.tool = tool;
467 edit.pending.clear();
468 edit.drag = None;
469 self.refresh_sketch3d_overlay();
470 self.dirty = true;
471 Ok(())
472 }
473
474 pub fn sketch3d_tool_cancel(&mut self) {
477 if let Some(edit) = self.sketch3d_edit.as_mut() {
478 edit.pending.clear();
479 }
480 self.refresh_sketch3d_overlay();
481 self.dirty = true;
482 }
483
484 pub fn sketch3d_set_work_plane(&mut self, name: &str) -> Result<(), String> {
487 let Some(edit) = self.sketch3d_edit.as_mut() else {
488 return Err("not in 3D sketch mode".into());
489 };
490 let anchor = edit.session.work_plane.anchor;
491 edit.session.work_plane =
492 WorkPlane::new(name, anchor).ok_or_else(|| format!("unknown working plane '{name}' (expected XY|YZ|XZ)"))?;
493 self.refresh_sketch3d_overlay();
494 self.dirty = true;
495 Ok(())
496 }
497
498 pub fn sketch3d_cycle_work_plane(&mut self) {
500 let next = self
501 .sketch3d_edit
502 .as_ref()
503 .map(|edit| edit.session.work_plane.next_name());
504 if let Some(next) = next {
505 let _ = self.sketch3d_set_work_plane(next);
506 }
507 }
508
509 fn sketch3d_pick_radius_px(&self) -> f64 {
510 f64::from(crate::sketch::tessellate::POINT_SIZE_PX) * 1.5
511 }
512
513 pub fn sketch3d_cursor_hit(&self, x: f64, y: f64) -> Option<Sketch3dCursorHit> {
518 let edit = self.sketch3d_edit.as_ref()?;
519 let radius = self.sketch3d_pick_radius_px();
520 if let Some(entity) = edit.session.pick_entity(&self.camera, x, y, radius) {
521 if entity.get("kind").and_then(Value::as_str) == Some("point") {
522 return entity.get("id").cloned().map(Sketch3dCursorHit::SketchPoint);
523 }
524 }
525 for candidate in crate::pick::pick_unoccluded(&self.scene, &self.context_scene, &self.camera, x, y, &self.pick_options()) {
526 let Some(solid) = self.scene.solids().iter().find(|s| s.name == candidate.solid) else {
527 continue;
528 };
529 if solid.is_sketch || !solid.visible {
530 continue;
531 }
532 match candidate.kind {
533 crate::pick::PickKind::Vertex => {
534 return Some(Sketch3dCursorHit::Vertex {
535 solid: candidate.solid,
536 position: candidate.position,
537 });
538 }
539 crate::pick::PickKind::Edge if !candidate.name.is_empty() => {
540 return Some(Sketch3dCursorHit::Edge {
541 name: candidate.name,
542 position: candidate.position,
543 });
544 }
545 crate::pick::PickKind::Face if !candidate.name.is_empty() => {
546 if self.scene.face_is_planar(&candidate.name) {
547 return Some(Sketch3dCursorHit::Face {
548 name: candidate.name,
549 position: candidate.position,
550 });
551 }
552 break;
555 }
556 _ => continue,
557 }
558 }
559 let ray = self.camera.pick_ray(x, y);
560 let frame = edit.session.work_plane.frame();
561 let (u, v) = crate::sketch::ray_plane_uv(&frame, ray.origin, ray.dir)?;
562 Some(Sketch3dCursorHit::WorkPlane(frame.to_world(u, v)))
563 }
564
565 fn sketch3d_hit_world(&self, hit: &Sketch3dCursorHit) -> Option<[f64; 3]> {
567 match hit {
568 Sketch3dCursorHit::SketchPoint(id) => self.sketch3d_edit.as_ref()?.session.point_world(id),
569 Sketch3dCursorHit::Vertex { position, .. }
570 | Sketch3dCursorHit::Edge { position, .. }
571 | Sketch3dCursorHit::Face { position, .. }
572 | Sketch3dCursorHit::WorkPlane(position) => Some(*position),
573 }
574 }
575
576 pub fn sketch3d_cursor_world(&self, x: f64, y: f64) -> Option<[f64; 3]> {
579 let hit = self.sketch3d_cursor_hit(x, y)?;
580 self.sketch3d_hit_world(&hit)
581 }
582
583 fn sketch3d_place_point(&mut self, hit: Sketch3dCursorHit) -> Option<Value> {
589 let world = self.sketch3d_hit_world(&hit)?;
590 let id = match &hit {
591 Sketch3dCursorHit::SketchPoint(id) => id.clone(),
592 _ => {
593 let edit = self.sketch3d_edit.as_mut()?;
594 let radius = 0.0;
595 edit.session.doc.snap_or_add_point(brep_kernel::Vec3::new(world[0], world[1], world[2]), radius)
596 }
597 };
598 match &hit {
599 Sketch3dCursorHit::Vertex { solid, position } => {
600 let name = format!("{solid}@{},{},{}", fmt_coord(position[0]), fmt_coord(position[1]), fmt_coord(position[2]));
601 if let Some(ids) = self.sketch3d_link_reference_silent(&name) {
602 if let Some(edit) = self.sketch3d_edit.as_mut() {
603 if let Some(ref_point) = ids.first() {
604 edit.session.doc.add_constraint("coincident", vec![id.clone(), ref_point.clone()], None);
605 }
606 }
607 }
608 }
609 Sketch3dCursorHit::Edge { name, .. } => {
610 if let Some(ids) = self.sketch3d_link_reference_silent(name) {
611 if let Some(edit) = self.sketch3d_edit.as_mut() {
612 match ids.len() {
613 2 => {
616 let circle = edit.session.doc.geometries.iter().any(|g| {
617 g.points == ids && g.extra.get("circle").and_then(Value::as_bool) == Some(true)
618 });
619 let ctype = if circle { "onCircle" } else { "onLine" };
620 edit.session.doc.add_constraint(ctype, vec![id.clone(), ids[0].clone(), ids[1].clone()], None);
621 }
622 3 => {
624 edit.session.doc.add_constraint("onArc", vec![id.clone(), ids[0].clone(), ids[1].clone(), ids[2].clone()], None);
625 }
626 _ => {}
627 }
628 }
629 }
630 }
631 Sketch3dCursorHit::Face { name, .. } => {
632 if let Some(ids) = self.sketch3d_link_reference_silent(name) {
633 if let Some(edit) = self.sketch3d_edit.as_mut() {
634 if ids.len() == 2 {
635 edit.session.doc.add_constraint("onPlane", vec![id.clone(), ids[0].clone(), ids[1].clone()], None);
636 }
637 }
638 }
639 }
640 _ => {}
641 }
642 if let Some(edit) = self.sketch3d_edit.as_mut() {
643 edit.session.work_plane.anchor = world;
644 edit.last_hit = Some(hit.kind());
645 }
646 Some(id)
647 }
648
649 pub fn sketch3d_tool_click_at(&mut self, x: f64, y: f64) {
654 let Some(tool) = self.sketch3d_active_tool().map(String::from) else {
655 return;
656 };
657 if tool == "pickRef" {
658 self.sketch3d_pick_reference_at(x, y);
659 return;
660 }
661 let Some(hit) = self.sketch3d_cursor_hit(x, y) else {
662 return;
663 };
664 if let Some(edit) = self.sketch3d_edit.as_mut() {
665 edit.record_undo();
666 }
667 let Some(id) = self.sketch3d_place_point(hit) else {
668 return;
669 };
670 let Some(edit) = self.sketch3d_edit.as_mut() else {
671 return;
672 };
673 if edit.pending.last().map_or(false, |last| brep_kernel::sketch_id_key(last) == brep_kernel::sketch_id_key(&id)) {
675 edit.undo_stack.pop();
676 return;
677 }
678 edit.pending.push(id);
679 match tool.as_str() {
680 "point" => {
681 let id = edit.pending.pop().unwrap();
682 edit.session.doc.add_geometry("point", vec![id], false);
683 }
684 "line" if edit.pending.len() >= 2 => {
685 let b = edit.pending.pop().unwrap();
686 let a = edit.pending.pop().unwrap();
687 edit.session.doc.add_geometry("line", vec![a, b.clone()], false);
688 edit.pending.push(b);
689 }
690 "arc" if edit.pending.len() >= 3 => {
691 let pts = std::mem::take(&mut edit.pending);
692 edit.session.doc.add_geometry("arc", pts, false);
693 }
694 "bezier" if edit.pending.len() >= 4 => {
695 let pts = std::mem::take(&mut edit.pending);
696 edit.session.doc.add_geometry("bezier", pts, false);
697 }
698 _ => {}
699 }
700 let _ = edit.session.resolve();
701 self.refresh_sketch3d_overlay();
705 self.dirty = true;
706 }
707
708 pub fn sketch3d_pick_reference_at(&mut self, x: f64, y: f64) -> bool {
714 let Some(name) = self.sketch3d_model_reference_at(x, y) else { return false };
715 self.sketch3d_link_reference(&name)
716 }
717
718 fn sketch3d_model_reference_at(&self, x: f64, y: f64) -> Option<String> {
721 let candidates = self.pick_candidates_at(x, y);
722 let mut name: Option<String> = None;
723 for candidate in candidates {
724 match candidate.kind {
725 crate::pick::PickKind::Vertex => {
726 if self.scene.solids().iter().any(|s| s.name == candidate.solid && !s.is_sketch) {
727 let p = candidate.position;
728 name = Some(format!("{}@{},{},{}", candidate.solid, fmt_coord(p[0]), fmt_coord(p[1]), fmt_coord(p[2])));
729 break;
730 }
731 }
732 crate::pick::PickKind::Edge | crate::pick::PickKind::Face | crate::pick::PickKind::Plane
733 if !candidate.name.is_empty() =>
734 {
735 if candidate.kind == crate::pick::PickKind::Plane
736 || self.scene.solids().iter().any(|s| s.name == candidate.solid && !s.is_sketch)
737 {
738 name = Some(candidate.name);
739 break;
740 }
741 }
742 _ => {}
743 }
744 }
745 name
746 }
747
748 fn sketch3d_link_model_at(&mut self, x: f64, y: f64) -> Option<Value> {
752 let name = self.sketch3d_model_reference_at(x, y)?;
753 let record_of = |refs: &[Value]| refs.iter().find(|r| r.get("name").and_then(Value::as_str) == Some(name.as_str())).cloned();
754 let known = record_of(&self.sketch3d_edit.as_ref()?.external_refs).is_some();
755 if !known && !self.sketch3d_link_reference(&name) {
756 return None;
757 }
758 let record = record_of(&self.sketch3d_edit.as_ref()?.external_refs)?;
759 if let Some(geometry) = record.get("geometry").filter(|g| !g.is_null()) {
760 return Some(crate::sketch3d::entity_geometry_ref(geometry));
761 }
762 record.get("points")?.get(0).map(crate::sketch3d::entity_point_ref)
763 }
764
765 pub fn sketch3d_link_reference(&mut self, name: &str) -> bool {
770 if let Some(edit) = self.sketch3d_edit.as_mut() {
771 edit.record_undo();
772 }
773 let linked = self.sketch3d_link_reference_silent(name).is_some();
774 if !linked {
775 if let Some(edit) = self.sketch3d_edit.as_mut() {
776 edit.undo_stack.pop();
777 }
778 }
779 if let Some(edit) = self.sketch3d_edit.as_mut() {
780 let _ = edit.session.resolve();
781 }
782 self.refresh_sketch3d_overlay();
783 self.dirty = true;
784 linked
785 }
786
787 fn sketch3d_link_reference_silent(&mut self, name: &str) -> Option<Vec<Value>> {
790 let prior = self.sketch3d_edit.as_ref().and_then(|edit| {
791 edit.external_refs
792 .iter()
793 .find(|r| r.get("name").and_then(Value::as_str) == Some(name))
794 .cloned()
795 });
796 let anchors: Option<Vec<String>> = prior
797 .as_ref()
798 .and_then(|r| r.get(brep_kernel::SKETCH3D_ANCHOR_EDGES_KEY))
799 .and_then(|a| serde_json::from_value(a.clone()).ok());
800 let resolved = match &anchors {
803 Some(edges) => brep_kernel::Sketch3dResolvedReference::Point(self.sketch3d_anchored_vertex(edges)?),
804 None => self.sketch3d_resolve_reference_from_display(name)?,
805 };
806 let resolved = match prior.as_ref().and_then(|r| r.get("kind")).and_then(Value::as_str) {
807 Some(kind) => resolved.as_recorded(kind),
808 None => resolved,
809 };
810 let new_anchors = match (&resolved, &anchors) {
811 (brep_kernel::Sketch3dResolvedReference::Point(p), None) => self.sketch3d_vertex_anchor_edges(name, [p.x, p.y, p.z]),
812 _ => None,
813 };
814 let edit = self.sketch3d_edit.as_mut()?;
815 let positions = resolved.positions();
816 if let Some(record) = edit
818 .external_refs
819 .iter()
820 .find(|r| r.get("name").and_then(Value::as_str) == Some(name))
821 .cloned()
822 {
823 let ids: Vec<Value> = record.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
824 if record.get("kind").and_then(Value::as_str) == Some(resolved.kind()) && ids.len() == positions.len() {
825 for (id, p) in ids.iter().zip(&positions) {
826 if let Some(point) = edit.session.doc.point_mut(id) {
827 point.x = p.x;
828 point.y = p.y;
829 point.z = p.z;
830 point.fixed = true;
831 point.construction = true;
832 point.external_reference = true;
833 }
834 }
835 if matches!(resolved, brep_kernel::Sketch3dResolvedReference::CircleAxis(..)) {
836 if let Some(g) = record.get("geometry").and_then(|id| edit.session.doc.geometry_mut(id)) {
837 g.extra.insert("circle".into(), Value::Bool(true));
838 }
839 }
840 if let Some(anchor) = new_anchors {
843 if let Some(map) = edit
844 .external_refs
845 .iter_mut()
846 .find(|r| r.get("name").and_then(Value::as_str) == Some(name))
847 .and_then(Value::as_object_mut)
848 {
849 map.insert(brep_kernel::SKETCH3D_ANCHOR_EDGES_KEY.into(), json!(anchor));
850 }
851 }
852 return Some(ids);
853 }
854 let held = edit.session.doc.constraints.iter().any(|c| {
857 c.points().iter().any(|p| ids.iter().any(|id| brep_kernel::sketch_id_key(id) == brep_kernel::sketch_id_key(p)))
858 });
859 if held {
860 return None;
861 }
862 edit.external_refs.retain(|r| r.get("name").and_then(Value::as_str) != Some(name));
864 for id in &ids {
865 edit.session.doc.points.retain(|p| brep_kernel::sketch_id_key(&p.id) != brep_kernel::sketch_id_key(id));
866 }
867 if let Some(gid) = record.get("geometry").filter(|g| !g.is_null()) {
868 edit.session.doc.geometries.retain(|g| brep_kernel::sketch_id_key(&g.id) != brep_kernel::sketch_id_key(gid));
869 }
870 }
871 let mut ids: Vec<Value> = Vec::with_capacity(positions.len());
872 for p in &positions {
873 let id = edit.session.doc.add_point(*p);
874 if let Some(point) = edit.session.doc.point_mut(&id) {
875 point.fixed = true;
876 point.construction = true;
877 point.external_reference = true;
878 }
879 ids.push(id);
880 }
881 let geometry = resolved
882 .geometry_type()
883 .map(|ty| edit.session.doc.add_geometry(ty, ids.clone(), true));
884 if matches!(resolved, brep_kernel::Sketch3dResolvedReference::CircleAxis(..)) {
885 if let Some(g) = geometry.as_ref().and_then(|id| edit.session.doc.geometry_mut(id)) {
886 g.extra.insert("circle".into(), Value::Bool(true));
887 }
888 }
889 let mut record = reference_record(name, &resolved, &ids, geometry.as_ref());
890 if let (Some(anchor), Some(map)) = (new_anchors, record.as_object_mut()) {
891 map.insert(brep_kernel::SKETCH3D_ANCHOR_EDGES_KEY.into(), json!(anchor));
892 }
893 edit.external_refs.push(record);
894 Some(ids)
895 }
896
897 fn sketch3d_display_edge_ends(&self, name: &str) -> Option<(brep_kernel::Vec3, brep_kernel::Vec3)> {
899 let polyline = self.scene.edge_polyline_world(name)?;
900 let v = |a: [f64; 3]| brep_kernel::Vec3::new(a[0], a[1], a[2]);
901 Some((v(*polyline.first()?), v(*polyline.last()?)))
902 }
903
904 fn sketch3d_anchored_vertex(&self, edges: &[String]) -> Option<brep_kernel::Vec3> {
906 let a = self.sketch3d_display_edge_ends(edges.first()?)?;
907 let b = self.sketch3d_display_edge_ends(edges.get(1)?)?;
908 brep_kernel::sketch3d_common_edge_end(a, b)
909 }
910
911 fn sketch3d_vertex_anchor_edges(&self, name: &str, position: [f64; 3]) -> Option<Vec<String>> {
915 let (solid_name, _) = name.rsplit_once('@')?;
916 let solid = self.scene.solids().iter().find(|s| s.name == solid_name)?;
917 let tol = 1e-4 * (1.0 + position.iter().fold(0.0f64, |m, c| m.max(c.abs())));
918 let at_vertex: Vec<&str> = solid
919 .edges
920 .iter()
921 .filter(|edge| !edge.aux && !edge.name.is_empty())
922 .filter(|edge| {
923 let touches = |p: Option<&[f32; 3]>| p.map_or(false, |p| dist([p[0] as f64, p[1] as f64, p[2] as f64], position) <= tol);
924 touches(edge.polyline.first()) || touches(edge.polyline.last())
925 })
926 .map(|edge| edge.name.as_str())
927 .collect();
928 for (i, first) in at_vertex.iter().enumerate() {
929 for second in &at_vertex[i + 1..] {
930 let (Some(a), Some(b)) = (self.sketch3d_display_edge_ends(first), self.sketch3d_display_edge_ends(second)) else {
931 continue;
932 };
933 if brep_kernel::sketch3d_common_edge_end(a, b).is_some() {
934 return Some(vec![first.to_string(), second.to_string()]);
935 }
936 }
937 }
938 None
939 }
940
941 fn sketch3d_resolve_reference_from_display(&self, name: &str) -> Option<brep_kernel::Sketch3dResolvedReference> {
945 use brep_kernel::{Sketch3dResolvedReference as R, Vec3, SKETCH3D_REFERENCE_TIP_LENGTH as TIP};
946 let v = |a: [f64; 3]| Vec3::new(a[0], a[1], a[2]);
947 if let Some((_, frame)) = self.construction_frames.iter().find(|(n, _)| n == name) {
948 return Some(R::Plane(frame.origin, frame.origin.add(frame.z_axis.scale(TIP))));
949 }
950 if let Some((_, axis)) = self.sketch_axes.iter().find(|(n, _)| n == name) {
951 return Some(R::Line(axis.point, axis.point.add(axis.direction.scale(TIP))));
952 }
953 if let Some((_, point)) = self.sketch_points.iter().find(|(n, _)| n == name) {
954 return Some(R::Point(point.position));
955 }
956 if let Some((centroid, normal)) = self.scene.face_plane_world(name) {
957 if !self.scene.face_is_planar(name) {
958 return None;
959 }
960 return Some(R::Plane(v(centroid), v(centroid).add(v(normal).scale(TIP))));
961 }
962 if let Some(polyline) = self.scene.edge_polyline_world(name) {
963 return classify_edge_polyline(&polyline);
964 }
965 if let Some((solid_name, coords)) = name.rsplit_once('@') {
966 let mut parts = coords.split(',').map(|p| p.trim().parse::<f64>().ok());
967 let position = [parts.next()??, parts.next()??, parts.next()??];
968 let solid = self.scene.solids().iter().find(|s| s.name == solid_name)?;
969 let best = solid
970 .vertices
971 .iter()
972 .map(|vertex| vertex.position)
973 .min_by(|a, b| dist(*a, position).total_cmp(&dist(*b, position)))?;
974 if dist(best, position) > 1e-4 * (1.0 + best.iter().fold(0.0f64, |m, c| m.max(c.abs()))) {
975 return None;
976 }
977 return Some(R::Point(v(best)));
978 }
979 None
980 }
981
982 fn sketch3d_sync_references(&mut self, listed: &[String]) {
991 let names: Vec<String> = self
992 .sketch3d_edit
993 .as_ref()
994 .map(|edit| {
995 edit.external_refs
996 .iter()
997 .filter_map(|r| r.get("name").and_then(Value::as_str).map(String::from))
998 .collect()
999 })
1000 .unwrap_or_default();
1001 for name in &names {
1002 if !listed.contains(name) {
1003 self.sketch3d_unlink_reference(name);
1004 if let Some(edit) = self.sketch3d_edit.as_mut() {
1005 edit.undo_stack.clear();
1007 edit.redo_stack.clear();
1008 }
1009 }
1010 }
1011 let names: Vec<String> = names.into_iter().filter(|n| listed.contains(n)).collect();
1012 let mut lost: Vec<String> = Vec::new();
1013 for name in &names {
1014 if self.sketch3d_link_reference_silent(name).is_none() {
1015 lost.push(name.clone());
1016 }
1017 }
1018 let mut unlinked = Vec::new();
1019 for name in listed {
1020 if names.contains(name) {
1021 continue;
1022 }
1023 if self.sketch3d_link_reference_silent(name).is_none() {
1024 unlinked.push(name.clone());
1025 }
1026 }
1027 if let Some(edit) = self.sketch3d_edit.as_mut() {
1028 edit.listed_unlinked = unlinked;
1029 let _ = edit.session.resolve();
1030 edit.session.lost_references = lost
1031 .iter()
1032 .filter_map(|name| {
1033 let record = edit.external_refs.iter().find(|r| r.get("name").and_then(Value::as_str) == Some(name.as_str()))?;
1034 let ids: Vec<Value> = record.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
1035 Some((name.clone(), ids))
1036 })
1037 .collect();
1038 }
1039 let notices: Vec<String> = self
1040 .sketch3d_edit
1041 .as_ref()
1042 .map(|edit| edit.session.lost_reference_messages())
1043 .unwrap_or_default();
1044 for message in notices {
1045 self.push_notice(message);
1046 }
1047 }
1048
1049 pub fn sketch3d_unlink_reference(&mut self, name: &str) -> bool {
1052 let Some(edit) = self.sketch3d_edit.as_mut() else { return false };
1053 let Some(record) = edit
1054 .external_refs
1055 .iter()
1056 .find(|r| r.get("name").and_then(Value::as_str) == Some(name))
1057 .cloned()
1058 else {
1059 return false;
1060 };
1061 edit.record_undo();
1062 edit.external_refs.retain(|r| r.get("name").and_then(Value::as_str) != Some(name));
1063 let ids: Vec<Value> = record.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
1064 let gid = record.get("geometry").filter(|g| !g.is_null()).cloned();
1065 let mut refs: Vec<Value> = ids.iter().map(crate::sketch3d::entity_point_ref).collect();
1066 if let Some(gid) = gid {
1067 refs.push(crate::sketch3d::entity_geometry_ref(&gid));
1068 }
1069 edit.session.selection.items = refs;
1070 self.sketch3d_delete_selection();
1071 true
1072 }
1073
1074 pub fn sketch3d_hover_at(&mut self, x: f64, y: f64) -> bool {
1077 if self.sketch3d_edit.is_none() {
1078 return false;
1079 }
1080 let world = self.sketch3d_cursor_world(x, y);
1081 let radius = self.sketch3d_pick_radius_px();
1082 let new_hover = self
1083 .sketch3d_edit
1084 .as_ref()
1085 .and_then(|edit| edit.session.pick_entity(&self.camera, x, y, radius));
1086 let (changed, preview_live) = match self.sketch3d_edit.as_mut() {
1087 Some(edit) => {
1088 let changed = !crate::sketch::session::entity_ref_eq(edit.session.selection.hovered.as_ref(), new_hover.as_ref());
1089 edit.session.selection.hovered = new_hover;
1090 let moved = edit.hover_world != world;
1091 edit.hover_world = world;
1092 (changed, edit.session.tool.is_some() && moved)
1093 }
1094 None => (false, false),
1095 };
1096 if changed || preview_live {
1097 self.refresh_sketch3d_overlay();
1098 self.dirty = true;
1099 }
1100 changed
1101 }
1102
1103 pub fn sketch3d_clear_hover(&mut self) -> bool {
1104 let changed = self
1105 .sketch3d_edit
1106 .as_mut()
1107 .map_or(false, |edit| {
1108 let had = edit.session.selection.hovered.take().is_some() || edit.hover_world.take().is_some();
1109 had
1110 });
1111 if changed {
1112 self.refresh_sketch3d_overlay();
1113 self.dirty = true;
1114 }
1115 changed
1116 }
1117
1118 pub fn sketch3d_click_at(&mut self, x: f64, y: f64, additive: bool) {
1121 let toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
1122 let radius = self.sketch3d_pick_radius_px();
1123 let hit = self
1124 .sketch3d_edit
1125 .as_ref()
1126 .and_then(|edit| edit.session.pick_entity(&self.camera, x, y, radius));
1127 let linked = if hit.is_none() { self.sketch3d_link_model_at(x, y) } else { None };
1130 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1131 if let Some(entity) = linked {
1132 edit.session.toggle_selection(entity);
1133 self.refresh_sketch3d_overlay();
1134 self.dirty = true;
1135 return;
1136 }
1137 match hit {
1138 None => edit.session.clear_selection(),
1139 Some(entity) => {
1140 if additive || toggles {
1141 edit.session.toggle_selection(entity);
1142 } else {
1143 edit.session.clear_selection();
1144 edit.session.toggle_selection(entity);
1145 }
1146 }
1147 }
1148 self.refresh_sketch3d_overlay();
1149 self.dirty = true;
1150 }
1151
1152 pub fn sketch3d_select_entity(&mut self, entity_ref: Value, additive: bool) {
1154 if let Some(edit) = self.sketch3d_edit.as_mut() {
1155 if !additive {
1156 edit.session.clear_selection();
1157 }
1158 edit.session.toggle_selection(entity_ref);
1159 }
1160 self.refresh_sketch3d_overlay();
1161 self.dirty = true;
1162 }
1163
1164 pub fn sketch3d_clear_selection(&mut self) {
1165 if let Some(edit) = self.sketch3d_edit.as_mut() {
1166 edit.session.clear_selection();
1167 }
1168 self.refresh_sketch3d_overlay();
1169 self.dirty = true;
1170 }
1171
1172 pub fn sketch3d_drag_begin(&mut self, x: f64, y: f64) -> bool {
1175 let radius = self.sketch3d_pick_radius_px();
1176 let points = self.sketch3d_edit.as_ref().and_then(|edit| match edit.session.selection.hovered.as_ref() {
1177 Some(entity) => edit.session.drag_points_from_ref(entity),
1178 None => edit
1179 .session
1180 .pick_draggable_point(&self.camera, x, y, radius)
1181 .and_then(|(id, fixed)| edit.session.point_world(&id).map(|p| vec![(id, p, fixed)])),
1182 });
1183 let Some(points) = points else { return false };
1184 let (_, _, forward) = self.camera.basis();
1186 let plane = crate::sketch::PlaneFrame::from_normal(points[0].1, [-forward[0], -forward[1], -forward[2]]);
1187 let ray = self.camera.pick_ray(x, y);
1188 let Some((u, v)) = crate::sketch::ray_plane_uv(&plane, ray.origin, ray.dir) else {
1189 return false;
1190 };
1191 let anchor = plane.to_world(u, v);
1192 if let Some(edit) = self.sketch3d_edit.as_mut() {
1193 edit.record_undo();
1194 edit.drag = Some(Sketch3dDrag { points, plane, anchor });
1195 }
1196 true
1197 }
1198
1199 pub fn sketch3d_drag_to(&mut self, x: f64, y: f64) {
1203 let ray = self.camera.pick_ray(x, y);
1204 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1205 let Some(drag) = edit.drag.clone() else { return };
1206 let Some((u, v)) = crate::sketch::ray_plane_uv(&drag.plane, ray.origin, ray.dir) else {
1207 return;
1208 };
1209 let here = drag.plane.to_world(u, v);
1210 let delta = [here[0] - drag.anchor[0], here[1] - drag.anchor[1], here[2] - drag.anchor[2]];
1211 let session = &mut edit.session;
1212 for (id, orig, _) in &drag.points {
1213 if let Some(p) = session.doc.point_mut(id) {
1214 p.x = orig[0] + delta[0];
1215 p.y = orig[1] + delta[1];
1216 p.z = orig[2] + delta[2];
1217 p.fixed = true;
1218 }
1219 }
1220 match session.resolve() {
1221 Ok(()) => {
1222 for (id, _, fixed) in &drag.points {
1223 if let Some(p) = session.doc.point_mut(id) {
1224 p.fixed = *fixed;
1225 }
1226 }
1227 }
1228 Err(_) => {
1229 for (id, orig, fixed) in &drag.points {
1230 if let Some(p) = session.doc.point_mut(id) {
1231 p.x = orig[0];
1232 p.y = orig[1];
1233 p.z = orig[2];
1234 p.fixed = *fixed;
1235 }
1236 }
1237 }
1238 }
1239 self.refresh_sketch3d_overlay();
1240 self.dirty = true;
1241 }
1242
1243 pub fn sketch3d_drag_end(&mut self) {
1244 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1245 if edit.drag.take().is_none() {
1246 return;
1247 }
1248 let _ = edit.session.resolve();
1249 if edit.undo_stack.last().map_or(false, |snap| snap.doc == edit.session.doc) {
1250 edit.undo_stack.pop();
1251 }
1252 self.refresh_sketch3d_overlay();
1253 self.dirty = true;
1254 }
1255
1256 pub fn sketch3d_add_constraint(&mut self, ctype: &str) -> Result<Value, String> {
1262 let Some(edit) = self.sketch3d_edit.as_mut() else {
1263 return Err("not in 3D sketch mode".into());
1264 };
1265 let offer = edit
1266 .session
1267 .applicable_constraints()
1268 .into_iter()
1269 .find(|c| c.ctype == ctype)
1270 .ok_or_else(|| format!("constraint '{ctype}' does not apply to the current selection"))?;
1271 edit.record_undo();
1272 if ctype == "fixed" {
1273 for id in &offer.points {
1274 if let Some(p) = edit.session.doc.point_mut(id) {
1275 p.fixed = true;
1276 }
1277 }
1278 let _ = edit.session.resolve();
1279 self.refresh_sketch3d_overlay();
1280 self.dirty = true;
1281 return Ok(Value::Null);
1282 }
1283 let value = if offer.dimensional {
1284 edit.session.measure(ctype, &offer.points)
1285 } else {
1286 None
1287 };
1288 let id = edit.session.doc.add_constraint(ctype, offer.points, value);
1289 if let Err(error) = edit.session.resolve() {
1290 if let Some(snap) = edit.undo_stack.pop() {
1292 edit.restore(snap);
1293 }
1294 return Err(error);
1295 }
1296 self.refresh_sketch3d_overlay();
1297 self.dirty = true;
1298 Ok(id)
1299 }
1300
1301 pub fn sketch3d_add_constraint_over(&mut self, ctype: &str, points: Vec<Value>, value: Option<f64>) -> Result<Value, String> {
1303 let Some(edit) = self.sketch3d_edit.as_mut() else {
1304 return Err("not in 3D sketch mode".into());
1305 };
1306 edit.record_undo();
1307 let id = edit.session.doc.add_constraint(ctype, points, value);
1308 if let Err(error) = edit.session.resolve() {
1309 if let Some(snap) = edit.undo_stack.pop() {
1310 edit.restore(snap);
1311 }
1312 return Err(error);
1313 }
1314 self.refresh_sketch3d_overlay();
1315 self.dirty = true;
1316 Ok(id)
1317 }
1318
1319 pub fn sketch3d_applicable_constraints_json(&self) -> String {
1321 self.sketch3d_edit
1322 .as_ref()
1323 .and_then(|edit| serde_json::to_string(&edit.session.applicable_constraints()).ok())
1324 .unwrap_or_else(|| "[]".to_string())
1325 }
1326
1327 pub fn sketch3d_set_dimension_value(&mut self, id: &Value, value: &str) -> Result<(), String> {
1331 let trimmed = value.trim();
1332 let number: f64 = match trimmed.parse::<f64>() {
1333 Ok(n) => n,
1334 Err(_) => brep_kernel::eval_expression(
1335 &self.history.expressions(),
1336 &self.history.configurator(),
1337 trimmed,
1338 )
1339 .map_err(|e| format!("dimension `{trimmed}`: {e}"))?,
1340 };
1341 if !number.is_finite() {
1342 return Err(format!("dimension `{trimmed}` is not finite"));
1343 }
1344 let Some(edit) = self.sketch3d_edit.as_mut() else {
1345 return Err("not in 3D sketch mode".into());
1346 };
1347 edit.record_undo();
1348 {
1349 let key = brep_kernel::sketch_id_key(id);
1350 let Some(constraint) = edit
1351 .session
1352 .doc
1353 .constraints
1354 .iter_mut()
1355 .find(|c| c.id().map(brep_kernel::sketch_id_key) == Some(key.clone()))
1356 else {
1357 edit.undo_stack.pop();
1358 return Err(format!("no constraint with id {key}"));
1359 };
1360 constraint.raw.insert("value".into(), Value::from(number));
1361 if trimmed.parse::<f64>().is_ok() {
1362 constraint.raw.remove("valueExpr");
1363 } else {
1364 constraint.raw.insert("valueExpr".into(), Value::String(trimmed.to_string()));
1365 }
1366 }
1367 if let Err(error) = edit.session.resolve() {
1368 if let Some(snap) = edit.undo_stack.pop() {
1369 edit.restore(snap);
1370 }
1371 return Err(error);
1372 }
1373 self.refresh_sketch3d_overlay();
1374 self.dirty = true;
1375 Ok(())
1376 }
1377
1378 pub fn sketch3d_toggle_construction(&mut self) {
1380 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1381 let (_, geometries, _) = edit.session.selection_by_kind();
1382 if geometries.is_empty() {
1383 return;
1384 }
1385 edit.record_undo();
1386 for gid in &geometries {
1387 if let Some(g) = edit.session.doc.geometry_mut(gid) {
1388 if g.geom_type == "plane" {
1389 continue;
1390 }
1391 let now = g.construction();
1392 g.extra.insert("construction".into(), Value::Bool(!now));
1393 }
1394 }
1395 self.refresh_sketch3d_overlay();
1396 self.dirty = true;
1397 }
1398
1399 pub fn sketch3d_toggle_fixed(&mut self) {
1402 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1403 let (points, _, _) = edit.session.selection_by_kind();
1404 if points.is_empty() {
1405 return;
1406 }
1407 edit.record_undo();
1408 for pid in &points {
1409 if let Some(p) = edit.session.doc.point_mut(pid) {
1410 if p.external_reference {
1411 continue;
1412 }
1413 p.fixed = !p.fixed;
1414 }
1415 }
1416 let _ = edit.session.resolve();
1417 self.refresh_sketch3d_overlay();
1418 self.dirty = true;
1419 }
1420
1421 pub fn sketch3d_delete_selection(&mut self) {
1426 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1427 let (mut points, mut geometries, constraints) = edit.session.selection_by_kind();
1428 if points.is_empty() && geometries.is_empty() && constraints.is_empty() {
1429 return;
1430 }
1431 edit.record_undo();
1432 let key = brep_kernel::sketch_id_key;
1433 let mut taken: Vec<Value> = Vec::new();
1438 for record in &edit.external_refs {
1439 let ids: Vec<Value> = record.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
1440 let gid = record.get("geometry").filter(|g| !g.is_null()).cloned();
1441 let hit = ids.iter().any(|id| points.iter().any(|p| key(p) == key(id)))
1442 || gid.as_ref().map_or(false, |g| geometries.iter().any(|s| key(s) == key(g)));
1443 if hit {
1444 for id in ids {
1445 if !points.iter().any(|p| key(p) == key(&id)) {
1446 points.push(id);
1447 }
1448 }
1449 if let Some(g) = gid {
1450 if !geometries.iter().any(|s| key(s) == key(&g)) {
1451 geometries.push(g);
1452 }
1453 }
1454 taken.push(record.clone());
1455 }
1456 }
1457 edit.external_refs.retain(|r| !taken.iter().any(|t| t == r));
1458 let doc = &mut edit.session.doc;
1459 let ckeys: Vec<String> = constraints.iter().map(key).collect();
1460 doc.constraints.retain(|c| c.id().map_or(true, |id| !ckeys.contains(&key(id))));
1461 let gkeys: Vec<String> = geometries.iter().map(key).collect();
1465 let mut pkeys: Vec<String> = points.iter().map(key).collect();
1466 doc.geometries.retain(|g| !gkeys.contains(&key(&g.id)));
1467 doc.geometries.retain(|g| !g.points.iter().any(|pid| pkeys.contains(&key(pid))));
1469 doc.points.retain(|p| !pkeys.contains(&key(&p.id)));
1470 let used: std::collections::HashSet<String> = doc
1474 .geometries
1475 .iter()
1476 .flat_map(|g| g.points.iter().map(key))
1477 .collect();
1478 let orphans: Vec<String> = doc
1479 .points
1480 .iter()
1481 .filter(|p| !p.external_reference && !used.contains(&key(&p.id)))
1482 .map(|p| key(&p.id))
1483 .collect();
1484 doc.points.retain(|p| !orphans.contains(&key(&p.id)));
1485 pkeys.extend(orphans);
1486 doc.constraints.retain(|c| !c.points().iter().any(|pid| pkeys.contains(&key(pid))));
1488 edit.external_refs.retain(|r| {
1490 let ids: Vec<Value> = r.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
1491 let gid = r.get("geometry").filter(|g| !g.is_null());
1492 let points_alive = ids.iter().all(|id| doc.point(id).is_some());
1493 let geometry_alive = gid.map_or(true, |g| doc.geometry(g).is_some());
1494 points_alive && geometry_alive
1495 });
1496 edit.session.selection.items.clear();
1497 edit.session.selection.hovered = None;
1498 edit.pending.clear();
1499 let _ = edit.session.resolve();
1500 self.refresh_sketch3d_overlay();
1501 self.dirty = true;
1502 }
1503
1504 pub fn sketch3d_undo(&mut self) -> bool {
1505 let Some(edit) = self.sketch3d_edit.as_mut() else { return false };
1506 let Some(snap) = edit.undo_stack.pop() else { return false };
1507 let current = edit.snapshot();
1508 edit.redo_stack.push(current);
1509 edit.restore(snap);
1510 self.refresh_sketch3d_overlay();
1511 self.dirty = true;
1512 true
1513 }
1514
1515 pub fn sketch3d_redo(&mut self) -> bool {
1516 let Some(edit) = self.sketch3d_edit.as_mut() else { return false };
1517 let Some(snap) = edit.redo_stack.pop() else { return false };
1518 let current = edit.snapshot();
1519 edit.undo_stack.push(current);
1520 edit.restore(snap);
1521 self.refresh_sketch3d_overlay();
1522 self.dirty = true;
1523 true
1524 }
1525
1526 pub fn sketch3d_can_undo(&self) -> bool {
1527 self.sketch3d_edit.as_ref().map_or(false, |edit| !edit.undo_stack.is_empty())
1528 }
1529
1530 pub fn sketch3d_can_redo(&self) -> bool {
1531 self.sketch3d_edit.as_ref().map_or(false, |edit| !edit.redo_stack.is_empty())
1532 }
1533}
1534
1535fn fmt_coord(value: f64) -> String {
1536 let text = format!("{value:.6}");
1537 let text = text.trim_end_matches('0').trim_end_matches('.').to_string();
1538 if text.is_empty() || text == "-0" {
1539 "0".to_string()
1540 } else {
1541 text
1542 }
1543}
1544
1545fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
1546 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
1547}
1548
1549fn classify_edge_polyline(polyline: &[[f64; 3]]) -> Option<brep_kernel::Sketch3dResolvedReference> {
1554 use brep_kernel::{Sketch3dResolvedReference as R, Vec3};
1555 let v = |a: [f64; 3]| Vec3::new(a[0], a[1], a[2]);
1556 if polyline.len() < 2 {
1557 return None;
1558 }
1559 let start = v(polyline[0]);
1560 let end = v(*polyline.last()?);
1561 let extent = polyline
1562 .iter()
1563 .map(|p| v(*p).sub(start).length())
1564 .fold(0.0f64, f64::max)
1565 .max(1e-12);
1566 let chord = end.sub(start);
1567 if chord.length() > 1e-9 * extent {
1569 let d = chord.normalized().ok()?;
1570 let off = polyline
1571 .iter()
1572 .map(|p| {
1573 let w = v(*p).sub(start);
1574 w.sub(d.scale(d.dot(w))).length()
1575 })
1576 .fold(0.0f64, f64::max);
1577 if off <= 1e-6 * extent {
1578 return Some(R::Segment(start, end));
1579 }
1580 }
1581 if polyline.len() >= 3 {
1584 let a = start;
1585 let b = v(polyline[polyline.len() / 2]);
1586 let c = if chord.length() > 1e-9 * extent { end } else { v(polyline[polyline.len() / 4]) };
1587 if let Some((center, radius, axis)) = circle_through(a, b, c) {
1588 let worst = polyline
1589 .iter()
1590 .map(|p| (v(*p).sub(center).length() - radius).abs())
1591 .fold(0.0f64, f64::max);
1592 let planar = polyline
1593 .iter()
1594 .map(|p| v(*p).sub(center).dot(axis).abs())
1595 .fold(0.0f64, f64::max);
1596 if worst <= 1e-4 * radius && planar <= 1e-4 * radius {
1597 if chord.length() <= 1e-6 * extent {
1598 return Some(R::CircleAxis(center, center.add(axis.scale(radius))));
1599 }
1600 return Some(R::Arc(center, start, end));
1601 }
1602 }
1603 }
1604 let samples: Vec<Vec3> = polyline.iter().map(|p| v(*p)).collect();
1605 match brep_kernel::sketch3d_curve_reference_controls(&samples) {
1606 Some(controls) => Some(R::Curve(controls)),
1607 None => (chord.length() > 1e-9 * extent).then_some(R::Segment(start, end)),
1608 }
1609}
1610
1611fn circle_through(a: brep_kernel::Vec3, b: brep_kernel::Vec3, c: brep_kernel::Vec3) -> Option<(brep_kernel::Vec3, f64, brep_kernel::Vec3)> {
1613 let ab = b.sub(a);
1614 let ac = c.sub(a);
1615 let n = ab.cross(ac);
1616 let n2 = n.dot(n);
1617 if n2 <= 1e-24 {
1618 return None;
1619 }
1620 let term = n.cross(ab.scale(ac.dot(ac)).sub(ac.scale(ab.dot(ab)))).scale(1.0 / (2.0 * n2));
1622 let center = a.add(term);
1623 let radius = a.sub(center).length();
1624 let axis = n.normalized().ok()?;
1625 Some((center, radius, axis))
1626}