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.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 = snap.selection;
117 self.session.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,
411 "hovered": session.hovered,
412 "applicableConstraints": session.applicable_constraints(),
413 "dimensions": session.dimension_labels(),
414 "externalRefs": edit.external_refs,
415 "lastHit": edit.last_hit,
416 "canUndo": !edit.undo_stack.is_empty(),
417 "canRedo": !edit.redo_stack.is_empty(),
418 "entities": session.entity_rows_json(),
419 })
420 .to_string()
421 }
422
423 pub fn sketch3d_dimension_labels_json(&self) -> String {
425 self.sketch3d_edit
426 .as_ref()
427 .and_then(|edit| serde_json::to_string(&edit.session.dimension_labels()).ok())
428 .unwrap_or_else(|| "[]".to_string())
429 }
430
431 pub fn sketch3d_active_tool(&self) -> Option<&str> {
432 self.sketch3d_edit.as_ref().and_then(|edit| edit.session.tool.as_deref())
433 }
434
435 pub fn sketch3d_selection_count(&self) -> usize {
436 self.sketch3d_edit.as_ref().map_or(0, |edit| edit.session.selection.len())
437 }
438
439 pub fn sketch3d_pending_len(&self) -> usize {
440 self.sketch3d_edit.as_ref().map_or(0, |edit| edit.pending.len())
441 }
442
443 pub fn sketch3d_work_plane(&self) -> Option<&str> {
444 self.sketch3d_edit.as_ref().map(|edit| edit.session.work_plane.name.as_str())
445 }
446
447 pub fn sketch3d_set_tool(&mut self, tool: Option<&str>) -> Result<(), String> {
452 let tool = match tool {
453 None | Some("select") | Some("") => None,
454 Some(name) if crate::sketch3d::TOOLS.contains(&name) => Some(name.to_string()),
455 Some(other) => return Err(format!("unknown 3D sketch tool '{other}'")),
456 };
457 let Some(edit) = self.sketch3d_edit.as_mut() else {
458 return Err("not in 3D sketch mode".into());
459 };
460 edit.session.tool = tool;
461 edit.pending.clear();
462 edit.drag = None;
463 self.refresh_sketch3d_overlay();
464 self.dirty = true;
465 Ok(())
466 }
467
468 pub fn sketch3d_tool_cancel(&mut self) {
471 if let Some(edit) = self.sketch3d_edit.as_mut() {
472 edit.pending.clear();
473 }
474 self.refresh_sketch3d_overlay();
475 self.dirty = true;
476 }
477
478 pub fn sketch3d_set_work_plane(&mut self, name: &str) -> Result<(), String> {
481 let Some(edit) = self.sketch3d_edit.as_mut() else {
482 return Err("not in 3D sketch mode".into());
483 };
484 let anchor = edit.session.work_plane.anchor;
485 edit.session.work_plane =
486 WorkPlane::new(name, anchor).ok_or_else(|| format!("unknown working plane '{name}' (expected XY|YZ|XZ)"))?;
487 self.refresh_sketch3d_overlay();
488 self.dirty = true;
489 Ok(())
490 }
491
492 pub fn sketch3d_cycle_work_plane(&mut self) {
494 let next = self
495 .sketch3d_edit
496 .as_ref()
497 .map(|edit| edit.session.work_plane.next_name());
498 if let Some(next) = next {
499 let _ = self.sketch3d_set_work_plane(next);
500 }
501 }
502
503 fn sketch3d_pick_radius_px(&self) -> f64 {
504 f64::from(crate::sketch::tessellate::POINT_SIZE_PX) * 1.5
505 }
506
507 pub fn sketch3d_cursor_hit(&self, x: f64, y: f64) -> Option<Sketch3dCursorHit> {
512 let edit = self.sketch3d_edit.as_ref()?;
513 let radius = self.sketch3d_pick_radius_px();
514 if let Some(entity) = edit.session.pick_entity(&self.camera, x, y, radius) {
515 if entity.get("kind").and_then(Value::as_str) == Some("point") {
516 return entity.get("id").cloned().map(Sketch3dCursorHit::SketchPoint);
517 }
518 }
519 for candidate in crate::pick::pick_unoccluded(&self.scene, &self.context_scene, &self.camera, x, y, &self.pick_options()) {
520 let Some(solid) = self.scene.solids().iter().find(|s| s.name == candidate.solid) else {
521 continue;
522 };
523 if solid.is_sketch || !solid.visible {
524 continue;
525 }
526 match candidate.kind {
527 crate::pick::PickKind::Vertex => {
528 return Some(Sketch3dCursorHit::Vertex {
529 solid: candidate.solid,
530 position: candidate.position,
531 });
532 }
533 crate::pick::PickKind::Edge if !candidate.name.is_empty() => {
534 return Some(Sketch3dCursorHit::Edge {
535 name: candidate.name,
536 position: candidate.position,
537 });
538 }
539 crate::pick::PickKind::Face if !candidate.name.is_empty() => {
540 if self.scene.face_is_planar(&candidate.name) {
541 return Some(Sketch3dCursorHit::Face {
542 name: candidate.name,
543 position: candidate.position,
544 });
545 }
546 break;
549 }
550 _ => continue,
551 }
552 }
553 let ray = self.camera.pick_ray(x, y);
554 let frame = edit.session.work_plane.frame();
555 let (u, v) = crate::sketch::ray_plane_uv(&frame, ray.origin, ray.dir)?;
556 Some(Sketch3dCursorHit::WorkPlane(frame.to_world(u, v)))
557 }
558
559 fn sketch3d_hit_world(&self, hit: &Sketch3dCursorHit) -> Option<[f64; 3]> {
561 match hit {
562 Sketch3dCursorHit::SketchPoint(id) => self.sketch3d_edit.as_ref()?.session.point_world(id),
563 Sketch3dCursorHit::Vertex { position, .. }
564 | Sketch3dCursorHit::Edge { position, .. }
565 | Sketch3dCursorHit::Face { position, .. }
566 | Sketch3dCursorHit::WorkPlane(position) => Some(*position),
567 }
568 }
569
570 pub fn sketch3d_cursor_world(&self, x: f64, y: f64) -> Option<[f64; 3]> {
573 let hit = self.sketch3d_cursor_hit(x, y)?;
574 self.sketch3d_hit_world(&hit)
575 }
576
577 fn sketch3d_place_point(&mut self, hit: Sketch3dCursorHit) -> Option<Value> {
583 let world = self.sketch3d_hit_world(&hit)?;
584 let id = match &hit {
585 Sketch3dCursorHit::SketchPoint(id) => id.clone(),
586 _ => {
587 let edit = self.sketch3d_edit.as_mut()?;
588 let radius = 0.0;
589 edit.session.doc.snap_or_add_point(brep_kernel::Vec3::new(world[0], world[1], world[2]), radius)
590 }
591 };
592 match &hit {
593 Sketch3dCursorHit::Vertex { solid, position } => {
594 let name = format!("{solid}@{},{},{}", fmt_coord(position[0]), fmt_coord(position[1]), fmt_coord(position[2]));
595 if let Some(ids) = self.sketch3d_link_reference_silent(&name) {
596 if let Some(edit) = self.sketch3d_edit.as_mut() {
597 if let Some(ref_point) = ids.first() {
598 edit.session.doc.add_constraint("coincident", vec![id.clone(), ref_point.clone()], None);
599 }
600 }
601 }
602 }
603 Sketch3dCursorHit::Edge { name, .. } => {
604 if let Some(ids) = self.sketch3d_link_reference_silent(name) {
605 if let Some(edit) = self.sketch3d_edit.as_mut() {
606 match ids.len() {
607 2 => {
609 edit.session.doc.add_constraint("onLine", vec![id.clone(), ids[0].clone(), ids[1].clone()], None);
610 }
611 3 => {
615 let radius = edit
616 .session
617 .measure("distance", &[ids[0].clone(), ids[1].clone()])
618 .unwrap_or(0.0);
619 edit.session.doc.add_constraint("distance", vec![id.clone(), ids[0].clone()], Some(radius));
620 }
621 _ => {}
622 }
623 }
624 }
625 }
626 Sketch3dCursorHit::Face { name, .. } => {
627 if let Some(ids) = self.sketch3d_link_reference_silent(name) {
628 if let Some(edit) = self.sketch3d_edit.as_mut() {
629 if ids.len() == 2 {
630 edit.session.doc.add_constraint("onPlane", vec![id.clone(), ids[0].clone(), ids[1].clone()], None);
631 }
632 }
633 }
634 }
635 _ => {}
636 }
637 if let Some(edit) = self.sketch3d_edit.as_mut() {
638 edit.session.work_plane.anchor = world;
639 edit.last_hit = Some(hit.kind());
640 }
641 Some(id)
642 }
643
644 pub fn sketch3d_tool_click_at(&mut self, x: f64, y: f64) {
649 let Some(tool) = self.sketch3d_active_tool().map(String::from) else {
650 return;
651 };
652 if tool == "pickRef" {
653 self.sketch3d_pick_reference_at(x, y);
654 return;
655 }
656 let Some(hit) = self.sketch3d_cursor_hit(x, y) else {
657 return;
658 };
659 if let Some(edit) = self.sketch3d_edit.as_mut() {
660 edit.record_undo();
661 }
662 let Some(id) = self.sketch3d_place_point(hit) else {
663 return;
664 };
665 let Some(edit) = self.sketch3d_edit.as_mut() else {
666 return;
667 };
668 if edit.pending.last().map_or(false, |last| brep_kernel::sketch_id_key(last) == brep_kernel::sketch_id_key(&id)) {
670 edit.undo_stack.pop();
671 return;
672 }
673 edit.pending.push(id);
674 match tool.as_str() {
675 "point" => {
676 let id = edit.pending.pop().unwrap();
677 edit.session.doc.add_geometry("point", vec![id], false);
678 }
679 "line" if edit.pending.len() >= 2 => {
680 let b = edit.pending.pop().unwrap();
681 let a = edit.pending.pop().unwrap();
682 edit.session.doc.add_geometry("line", vec![a, b.clone()], false);
683 edit.pending.push(b);
684 }
685 "arc" if edit.pending.len() >= 3 => {
686 let pts = std::mem::take(&mut edit.pending);
687 edit.session.doc.add_geometry("arc", pts, false);
688 }
689 "bezier" if edit.pending.len() >= 4 => {
690 let pts = std::mem::take(&mut edit.pending);
691 edit.session.doc.add_geometry("bezier", pts, false);
692 }
693 _ => {}
694 }
695 let _ = edit.session.resolve();
696 self.refresh_sketch3d_overlay();
700 self.dirty = true;
701 }
702
703 pub fn sketch3d_pick_reference_at(&mut self, x: f64, y: f64) -> bool {
709 let candidates = self.pick_candidates_at(x, y);
710 let mut name: Option<String> = None;
711 for candidate in candidates {
712 match candidate.kind {
713 crate::pick::PickKind::Vertex => {
714 if self.scene.solids().iter().any(|s| s.name == candidate.solid && !s.is_sketch) {
715 let p = candidate.position;
716 name = Some(format!("{}@{},{},{}", candidate.solid, fmt_coord(p[0]), fmt_coord(p[1]), fmt_coord(p[2])));
717 break;
718 }
719 }
720 crate::pick::PickKind::Edge | crate::pick::PickKind::Face | crate::pick::PickKind::Plane
721 if !candidate.name.is_empty() =>
722 {
723 if candidate.kind == crate::pick::PickKind::Plane
724 || self.scene.solids().iter().any(|s| s.name == candidate.solid && !s.is_sketch)
725 {
726 name = Some(candidate.name);
727 break;
728 }
729 }
730 _ => {}
731 }
732 }
733 let Some(name) = name else { return false };
734 self.sketch3d_link_reference(&name)
735 }
736
737 pub fn sketch3d_link_reference(&mut self, name: &str) -> bool {
742 if let Some(edit) = self.sketch3d_edit.as_mut() {
743 edit.record_undo();
744 }
745 let linked = self.sketch3d_link_reference_silent(name).is_some();
746 if !linked {
747 if let Some(edit) = self.sketch3d_edit.as_mut() {
748 edit.undo_stack.pop();
749 }
750 }
751 if let Some(edit) = self.sketch3d_edit.as_mut() {
752 let _ = edit.session.resolve();
753 }
754 self.refresh_sketch3d_overlay();
755 self.dirty = true;
756 linked
757 }
758
759 fn sketch3d_link_reference_silent(&mut self, name: &str) -> Option<Vec<Value>> {
762 let resolved = self.sketch3d_resolve_reference_from_display(name)?;
763 let edit = self.sketch3d_edit.as_mut()?;
764 let positions = resolved.positions();
765 if let Some(record) = edit
767 .external_refs
768 .iter()
769 .find(|r| r.get("name").and_then(Value::as_str) == Some(name))
770 .cloned()
771 {
772 let ids: Vec<Value> = record.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
773 if record.get("kind").and_then(Value::as_str) == Some(resolved.kind()) && ids.len() == positions.len() {
774 for (id, p) in ids.iter().zip(&positions) {
775 if let Some(point) = edit.session.doc.point_mut(id) {
776 point.x = p.x;
777 point.y = p.y;
778 point.z = p.z;
779 point.fixed = true;
780 point.construction = true;
781 point.external_reference = true;
782 }
783 }
784 return Some(ids);
785 }
786 edit.external_refs.retain(|r| r.get("name").and_then(Value::as_str) != Some(name));
788 for id in &ids {
789 edit.session.doc.points.retain(|p| brep_kernel::sketch_id_key(&p.id) != brep_kernel::sketch_id_key(id));
790 }
791 if let Some(gid) = record.get("geometry").filter(|g| !g.is_null()) {
792 edit.session.doc.geometries.retain(|g| brep_kernel::sketch_id_key(&g.id) != brep_kernel::sketch_id_key(gid));
793 }
794 }
795 let mut ids: Vec<Value> = Vec::with_capacity(positions.len());
796 for p in &positions {
797 let id = edit.session.doc.add_point(*p);
798 if let Some(point) = edit.session.doc.point_mut(&id) {
799 point.fixed = true;
800 point.construction = true;
801 point.external_reference = true;
802 }
803 ids.push(id);
804 }
805 let geometry = resolved
806 .geometry_type()
807 .map(|ty| edit.session.doc.add_geometry(ty, ids.clone(), true));
808 edit.external_refs.push(reference_record(name, &resolved, &ids, geometry.as_ref()));
809 Some(ids)
810 }
811
812 fn sketch3d_resolve_reference_from_display(&self, name: &str) -> Option<brep_kernel::Sketch3dResolvedReference> {
816 use brep_kernel::{Sketch3dResolvedReference as R, Vec3, SKETCH3D_REFERENCE_TIP_LENGTH as TIP};
817 let v = |a: [f64; 3]| Vec3::new(a[0], a[1], a[2]);
818 if let Some((_, frame)) = self.construction_frames.iter().find(|(n, _)| n == name) {
819 return Some(R::Plane(frame.origin, frame.origin.add(frame.z_axis.scale(TIP))));
820 }
821 if let Some((_, axis)) = self.sketch_axes.iter().find(|(n, _)| n == name) {
822 return Some(R::Line(axis.point, axis.point.add(axis.direction.scale(TIP))));
823 }
824 if let Some((_, point)) = self.sketch_points.iter().find(|(n, _)| n == name) {
825 return Some(R::Point(point.position));
826 }
827 if let Some((centroid, normal)) = self.scene.face_plane_world(name) {
828 if !self.scene.face_is_planar(name) {
829 return None;
830 }
831 return Some(R::Plane(v(centroid), v(centroid).add(v(normal).scale(TIP))));
832 }
833 if let Some(polyline) = self.scene.edge_polyline_world(name) {
834 return classify_edge_polyline(&polyline);
835 }
836 if let Some((solid_name, coords)) = name.rsplit_once('@') {
837 let mut parts = coords.split(',').map(|p| p.trim().parse::<f64>().ok());
838 let position = [parts.next()??, parts.next()??, parts.next()??];
839 let solid = self.scene.solids().iter().find(|s| s.name == solid_name)?;
840 let best = solid
841 .vertices
842 .iter()
843 .map(|vertex| vertex.position)
844 .min_by(|a, b| dist(*a, position).total_cmp(&dist(*b, position)))?;
845 if dist(best, position) > 1e-4 * (1.0 + best.iter().fold(0.0f64, |m, c| m.max(c.abs()))) {
846 return None;
847 }
848 return Some(R::Point(v(best)));
849 }
850 None
851 }
852
853 fn sketch3d_sync_references(&mut self, listed: &[String]) {
862 let names: Vec<String> = self
863 .sketch3d_edit
864 .as_ref()
865 .map(|edit| {
866 edit.external_refs
867 .iter()
868 .filter_map(|r| r.get("name").and_then(Value::as_str).map(String::from))
869 .collect()
870 })
871 .unwrap_or_default();
872 for name in &names {
873 if !listed.contains(name) {
874 self.sketch3d_unlink_reference(name);
875 if let Some(edit) = self.sketch3d_edit.as_mut() {
876 edit.undo_stack.clear();
878 edit.redo_stack.clear();
879 }
880 }
881 }
882 let names: Vec<String> = names.into_iter().filter(|n| listed.contains(n)).collect();
883 for name in &names {
884 let _ = self.sketch3d_link_reference_silent(name);
885 }
886 let mut unlinked = Vec::new();
887 for name in listed {
888 if names.contains(name) {
889 continue;
890 }
891 if self.sketch3d_link_reference_silent(name).is_none() {
892 unlinked.push(name.clone());
893 }
894 }
895 if let Some(edit) = self.sketch3d_edit.as_mut() {
896 edit.listed_unlinked = unlinked;
897 let _ = edit.session.resolve();
898 }
899 }
900
901 pub fn sketch3d_unlink_reference(&mut self, name: &str) -> bool {
904 let Some(edit) = self.sketch3d_edit.as_mut() else { return false };
905 let Some(record) = edit
906 .external_refs
907 .iter()
908 .find(|r| r.get("name").and_then(Value::as_str) == Some(name))
909 .cloned()
910 else {
911 return false;
912 };
913 edit.record_undo();
914 edit.external_refs.retain(|r| r.get("name").and_then(Value::as_str) != Some(name));
915 let ids: Vec<Value> = record.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
916 let gid = record.get("geometry").filter(|g| !g.is_null()).cloned();
917 let mut refs: Vec<Value> = ids.iter().map(crate::sketch3d::entity_point_ref).collect();
918 if let Some(gid) = gid {
919 refs.push(crate::sketch3d::entity_geometry_ref(&gid));
920 }
921 edit.session.selection = refs;
922 self.sketch3d_delete_selection();
923 true
924 }
925
926 pub fn sketch3d_hover_at(&mut self, x: f64, y: f64) -> bool {
929 if self.sketch3d_edit.is_none() {
930 return false;
931 }
932 let world = self.sketch3d_cursor_world(x, y);
933 let radius = self.sketch3d_pick_radius_px();
934 let new_hover = self
935 .sketch3d_edit
936 .as_ref()
937 .and_then(|edit| edit.session.pick_entity(&self.camera, x, y, radius));
938 let (changed, preview_live) = match self.sketch3d_edit.as_mut() {
939 Some(edit) => {
940 let changed = !crate::sketch::session::entity_ref_eq(edit.session.hovered.as_ref(), new_hover.as_ref());
941 edit.session.hovered = new_hover;
942 let moved = edit.hover_world != world;
943 edit.hover_world = world;
944 (changed, edit.session.tool.is_some() && moved)
945 }
946 None => (false, false),
947 };
948 if changed || preview_live {
949 self.refresh_sketch3d_overlay();
950 self.dirty = true;
951 }
952 changed
953 }
954
955 pub fn sketch3d_clear_hover(&mut self) -> bool {
956 let changed = self
957 .sketch3d_edit
958 .as_mut()
959 .map_or(false, |edit| {
960 let had = edit.session.hovered.take().is_some() || edit.hover_world.take().is_some();
961 had
962 });
963 if changed {
964 self.refresh_sketch3d_overlay();
965 self.dirty = true;
966 }
967 changed
968 }
969
970 pub fn sketch3d_click_at(&mut self, x: f64, y: f64, additive: bool) {
973 let toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
974 let radius = self.sketch3d_pick_radius_px();
975 let hit = self
976 .sketch3d_edit
977 .as_ref()
978 .and_then(|edit| edit.session.pick_entity(&self.camera, x, y, radius));
979 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
980 match hit {
981 None => edit.session.clear_selection(),
982 Some(entity) => {
983 if additive || toggles {
984 edit.session.toggle_selection(entity);
985 } else {
986 edit.session.clear_selection();
987 edit.session.toggle_selection(entity);
988 }
989 }
990 }
991 self.refresh_sketch3d_overlay();
992 self.dirty = true;
993 }
994
995 pub fn sketch3d_select_entity(&mut self, entity_ref: Value, additive: bool) {
997 if let Some(edit) = self.sketch3d_edit.as_mut() {
998 if !additive {
999 edit.session.clear_selection();
1000 }
1001 edit.session.toggle_selection(entity_ref);
1002 }
1003 self.refresh_sketch3d_overlay();
1004 self.dirty = true;
1005 }
1006
1007 pub fn sketch3d_clear_selection(&mut self) {
1008 if let Some(edit) = self.sketch3d_edit.as_mut() {
1009 edit.session.clear_selection();
1010 }
1011 self.refresh_sketch3d_overlay();
1012 self.dirty = true;
1013 }
1014
1015 pub fn sketch3d_drag_begin(&mut self, x: f64, y: f64) -> bool {
1018 let radius = self.sketch3d_pick_radius_px();
1019 let points = self.sketch3d_edit.as_ref().and_then(|edit| match edit.session.hovered.as_ref() {
1020 Some(entity) => edit.session.drag_points_from_ref(entity),
1021 None => edit
1022 .session
1023 .pick_draggable_point(&self.camera, x, y, radius)
1024 .and_then(|(id, fixed)| edit.session.point_world(&id).map(|p| vec![(id, p, fixed)])),
1025 });
1026 let Some(points) = points else { return false };
1027 let (_, _, forward) = self.camera.basis();
1029 let plane = crate::sketch::PlaneFrame::from_normal(points[0].1, [-forward[0], -forward[1], -forward[2]]);
1030 let ray = self.camera.pick_ray(x, y);
1031 let Some((u, v)) = crate::sketch::ray_plane_uv(&plane, ray.origin, ray.dir) else {
1032 return false;
1033 };
1034 let anchor = plane.to_world(u, v);
1035 if let Some(edit) = self.sketch3d_edit.as_mut() {
1036 edit.record_undo();
1037 edit.drag = Some(Sketch3dDrag { points, plane, anchor });
1038 }
1039 true
1040 }
1041
1042 pub fn sketch3d_drag_to(&mut self, x: f64, y: f64) {
1046 let ray = self.camera.pick_ray(x, y);
1047 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1048 let Some(drag) = edit.drag.clone() else { return };
1049 let Some((u, v)) = crate::sketch::ray_plane_uv(&drag.plane, ray.origin, ray.dir) else {
1050 return;
1051 };
1052 let here = drag.plane.to_world(u, v);
1053 let delta = [here[0] - drag.anchor[0], here[1] - drag.anchor[1], here[2] - drag.anchor[2]];
1054 let session = &mut edit.session;
1055 for (id, orig, _) in &drag.points {
1056 if let Some(p) = session.doc.point_mut(id) {
1057 p.x = orig[0] + delta[0];
1058 p.y = orig[1] + delta[1];
1059 p.z = orig[2] + delta[2];
1060 p.fixed = true;
1061 }
1062 }
1063 match session.resolve() {
1064 Ok(()) => {
1065 for (id, _, fixed) in &drag.points {
1066 if let Some(p) = session.doc.point_mut(id) {
1067 p.fixed = *fixed;
1068 }
1069 }
1070 }
1071 Err(_) => {
1072 for (id, orig, fixed) in &drag.points {
1073 if let Some(p) = session.doc.point_mut(id) {
1074 p.x = orig[0];
1075 p.y = orig[1];
1076 p.z = orig[2];
1077 p.fixed = *fixed;
1078 }
1079 }
1080 }
1081 }
1082 self.refresh_sketch3d_overlay();
1083 self.dirty = true;
1084 }
1085
1086 pub fn sketch3d_drag_end(&mut self) {
1087 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1088 if edit.drag.take().is_none() {
1089 return;
1090 }
1091 let _ = edit.session.resolve();
1092 if edit.undo_stack.last().map_or(false, |snap| snap.doc == edit.session.doc) {
1093 edit.undo_stack.pop();
1094 }
1095 self.refresh_sketch3d_overlay();
1096 self.dirty = true;
1097 }
1098
1099 pub fn sketch3d_add_constraint(&mut self, ctype: &str) -> Result<Value, String> {
1105 let Some(edit) = self.sketch3d_edit.as_mut() else {
1106 return Err("not in 3D sketch mode".into());
1107 };
1108 let offer = edit
1109 .session
1110 .applicable_constraints()
1111 .into_iter()
1112 .find(|c| c.ctype == ctype)
1113 .ok_or_else(|| format!("constraint '{ctype}' does not apply to the current selection"))?;
1114 edit.record_undo();
1115 if ctype == "fixed" {
1116 for id in &offer.points {
1117 if let Some(p) = edit.session.doc.point_mut(id) {
1118 p.fixed = true;
1119 }
1120 }
1121 let _ = edit.session.resolve();
1122 self.refresh_sketch3d_overlay();
1123 self.dirty = true;
1124 return Ok(Value::Null);
1125 }
1126 let value = if offer.dimensional {
1127 edit.session.measure(ctype, &offer.points)
1128 } else {
1129 None
1130 };
1131 let id = edit.session.doc.add_constraint(ctype, offer.points, value);
1132 if let Err(error) = edit.session.resolve() {
1133 if let Some(snap) = edit.undo_stack.pop() {
1135 edit.restore(snap);
1136 }
1137 return Err(error);
1138 }
1139 self.refresh_sketch3d_overlay();
1140 self.dirty = true;
1141 Ok(id)
1142 }
1143
1144 pub fn sketch3d_add_constraint_over(&mut self, ctype: &str, points: Vec<Value>, value: Option<f64>) -> Result<Value, String> {
1146 let Some(edit) = self.sketch3d_edit.as_mut() else {
1147 return Err("not in 3D sketch mode".into());
1148 };
1149 edit.record_undo();
1150 let id = edit.session.doc.add_constraint(ctype, points, value);
1151 if let Err(error) = edit.session.resolve() {
1152 if let Some(snap) = edit.undo_stack.pop() {
1153 edit.restore(snap);
1154 }
1155 return Err(error);
1156 }
1157 self.refresh_sketch3d_overlay();
1158 self.dirty = true;
1159 Ok(id)
1160 }
1161
1162 pub fn sketch3d_applicable_constraints_json(&self) -> String {
1164 self.sketch3d_edit
1165 .as_ref()
1166 .and_then(|edit| serde_json::to_string(&edit.session.applicable_constraints()).ok())
1167 .unwrap_or_else(|| "[]".to_string())
1168 }
1169
1170 pub fn sketch3d_set_dimension_value(&mut self, id: &Value, value: &str) -> Result<(), String> {
1174 let trimmed = value.trim();
1175 let number: f64 = match trimmed.parse::<f64>() {
1176 Ok(n) => n,
1177 Err(_) => brep_kernel::eval_expression(
1178 &self.history.expressions(),
1179 &self.history.configurator(),
1180 trimmed,
1181 )
1182 .map_err(|e| format!("dimension `{trimmed}`: {e}"))?,
1183 };
1184 if !number.is_finite() {
1185 return Err(format!("dimension `{trimmed}` is not finite"));
1186 }
1187 let Some(edit) = self.sketch3d_edit.as_mut() else {
1188 return Err("not in 3D sketch mode".into());
1189 };
1190 edit.record_undo();
1191 {
1192 let key = brep_kernel::sketch_id_key(id);
1193 let Some(constraint) = edit
1194 .session
1195 .doc
1196 .constraints
1197 .iter_mut()
1198 .find(|c| c.id().map(brep_kernel::sketch_id_key) == Some(key.clone()))
1199 else {
1200 edit.undo_stack.pop();
1201 return Err(format!("no constraint with id {key}"));
1202 };
1203 constraint.raw.insert("value".into(), Value::from(number));
1204 if trimmed.parse::<f64>().is_ok() {
1205 constraint.raw.remove("valueExpr");
1206 } else {
1207 constraint.raw.insert("valueExpr".into(), Value::String(trimmed.to_string()));
1208 }
1209 }
1210 if let Err(error) = edit.session.resolve() {
1211 if let Some(snap) = edit.undo_stack.pop() {
1212 edit.restore(snap);
1213 }
1214 return Err(error);
1215 }
1216 self.refresh_sketch3d_overlay();
1217 self.dirty = true;
1218 Ok(())
1219 }
1220
1221 pub fn sketch3d_toggle_construction(&mut self) {
1223 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1224 let (_, geometries, _) = edit.session.selection_by_kind();
1225 if geometries.is_empty() {
1226 return;
1227 }
1228 edit.record_undo();
1229 for gid in &geometries {
1230 if let Some(g) = edit.session.doc.geometry_mut(gid) {
1231 if g.geom_type == "plane" {
1232 continue;
1233 }
1234 let now = g.construction();
1235 g.extra.insert("construction".into(), Value::Bool(!now));
1236 }
1237 }
1238 self.refresh_sketch3d_overlay();
1239 self.dirty = true;
1240 }
1241
1242 pub fn sketch3d_toggle_fixed(&mut self) {
1245 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1246 let (points, _, _) = edit.session.selection_by_kind();
1247 if points.is_empty() {
1248 return;
1249 }
1250 edit.record_undo();
1251 for pid in &points {
1252 if let Some(p) = edit.session.doc.point_mut(pid) {
1253 if p.external_reference {
1254 continue;
1255 }
1256 p.fixed = !p.fixed;
1257 }
1258 }
1259 let _ = edit.session.resolve();
1260 self.refresh_sketch3d_overlay();
1261 self.dirty = true;
1262 }
1263
1264 pub fn sketch3d_delete_selection(&mut self) {
1269 let Some(edit) = self.sketch3d_edit.as_mut() else { return };
1270 let (mut points, mut geometries, constraints) = edit.session.selection_by_kind();
1271 if points.is_empty() && geometries.is_empty() && constraints.is_empty() {
1272 return;
1273 }
1274 edit.record_undo();
1275 let key = brep_kernel::sketch_id_key;
1276 let mut taken: Vec<Value> = Vec::new();
1281 for record in &edit.external_refs {
1282 let ids: Vec<Value> = record.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
1283 let gid = record.get("geometry").filter(|g| !g.is_null()).cloned();
1284 let hit = ids.iter().any(|id| points.iter().any(|p| key(p) == key(id)))
1285 || gid.as_ref().map_or(false, |g| geometries.iter().any(|s| key(s) == key(g)));
1286 if hit {
1287 for id in ids {
1288 if !points.iter().any(|p| key(p) == key(&id)) {
1289 points.push(id);
1290 }
1291 }
1292 if let Some(g) = gid {
1293 if !geometries.iter().any(|s| key(s) == key(&g)) {
1294 geometries.push(g);
1295 }
1296 }
1297 taken.push(record.clone());
1298 }
1299 }
1300 edit.external_refs.retain(|r| !taken.iter().any(|t| t == r));
1301 let doc = &mut edit.session.doc;
1302 let ckeys: Vec<String> = constraints.iter().map(key).collect();
1303 doc.constraints.retain(|c| c.id().map_or(true, |id| !ckeys.contains(&key(id))));
1304 let gkeys: Vec<String> = geometries.iter().map(key).collect();
1308 let mut pkeys: Vec<String> = points.iter().map(key).collect();
1309 doc.geometries.retain(|g| !gkeys.contains(&key(&g.id)));
1310 doc.geometries.retain(|g| !g.points.iter().any(|pid| pkeys.contains(&key(pid))));
1312 doc.points.retain(|p| !pkeys.contains(&key(&p.id)));
1313 let used: std::collections::HashSet<String> = doc
1317 .geometries
1318 .iter()
1319 .flat_map(|g| g.points.iter().map(key))
1320 .collect();
1321 let orphans: Vec<String> = doc
1322 .points
1323 .iter()
1324 .filter(|p| !p.external_reference && !used.contains(&key(&p.id)))
1325 .map(|p| key(&p.id))
1326 .collect();
1327 doc.points.retain(|p| !orphans.contains(&key(&p.id)));
1328 pkeys.extend(orphans);
1329 doc.constraints.retain(|c| !c.points().iter().any(|pid| pkeys.contains(&key(pid))));
1331 edit.external_refs.retain(|r| {
1333 let ids: Vec<Value> = r.get("points").and_then(Value::as_array).cloned().unwrap_or_default();
1334 let gid = r.get("geometry").filter(|g| !g.is_null());
1335 let points_alive = ids.iter().all(|id| doc.point(id).is_some());
1336 let geometry_alive = gid.map_or(true, |g| doc.geometry(g).is_some());
1337 points_alive && geometry_alive
1338 });
1339 edit.session.selection.clear();
1340 edit.session.hovered = None;
1341 edit.pending.clear();
1342 let _ = edit.session.resolve();
1343 self.refresh_sketch3d_overlay();
1344 self.dirty = true;
1345 }
1346
1347 pub fn sketch3d_undo(&mut self) -> bool {
1348 let Some(edit) = self.sketch3d_edit.as_mut() else { return false };
1349 let Some(snap) = edit.undo_stack.pop() else { return false };
1350 let current = edit.snapshot();
1351 edit.redo_stack.push(current);
1352 edit.restore(snap);
1353 self.refresh_sketch3d_overlay();
1354 self.dirty = true;
1355 true
1356 }
1357
1358 pub fn sketch3d_redo(&mut self) -> bool {
1359 let Some(edit) = self.sketch3d_edit.as_mut() else { return false };
1360 let Some(snap) = edit.redo_stack.pop() else { return false };
1361 let current = edit.snapshot();
1362 edit.undo_stack.push(current);
1363 edit.restore(snap);
1364 self.refresh_sketch3d_overlay();
1365 self.dirty = true;
1366 true
1367 }
1368
1369 pub fn sketch3d_can_undo(&self) -> bool {
1370 self.sketch3d_edit.as_ref().map_or(false, |edit| !edit.undo_stack.is_empty())
1371 }
1372
1373 pub fn sketch3d_can_redo(&self) -> bool {
1374 self.sketch3d_edit.as_ref().map_or(false, |edit| !edit.redo_stack.is_empty())
1375 }
1376}
1377
1378fn fmt_coord(value: f64) -> String {
1379 let text = format!("{value:.6}");
1380 let text = text.trim_end_matches('0').trim_end_matches('.').to_string();
1381 if text.is_empty() || text == "-0" {
1382 "0".to_string()
1383 } else {
1384 text
1385 }
1386}
1387
1388fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
1389 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
1390}
1391
1392fn classify_edge_polyline(polyline: &[[f64; 3]]) -> Option<brep_kernel::Sketch3dResolvedReference> {
1397 use brep_kernel::{Sketch3dResolvedReference as R, Vec3, SKETCH3D_REFERENCE_TIP_LENGTH as TIP};
1398 let v = |a: [f64; 3]| Vec3::new(a[0], a[1], a[2]);
1399 if polyline.len() < 2 {
1400 return None;
1401 }
1402 let start = v(polyline[0]);
1403 let end = v(*polyline.last()?);
1404 let extent = polyline
1405 .iter()
1406 .map(|p| v(*p).sub(start).length())
1407 .fold(0.0f64, f64::max)
1408 .max(1e-12);
1409 let chord = end.sub(start);
1410 if chord.length() > 1e-9 * extent {
1412 let d = chord.normalized().ok()?;
1413 let off = polyline
1414 .iter()
1415 .map(|p| {
1416 let w = v(*p).sub(start);
1417 w.sub(d.scale(d.dot(w))).length()
1418 })
1419 .fold(0.0f64, f64::max);
1420 if off <= 1e-6 * extent {
1421 return Some(R::Line(start, end));
1422 }
1423 }
1424 if polyline.len() >= 3 {
1427 let a = start;
1428 let b = v(polyline[polyline.len() / 2]);
1429 let c = if chord.length() > 1e-9 * extent { end } else { v(polyline[polyline.len() / 4]) };
1430 if let Some((center, radius, axis)) = circle_through(a, b, c) {
1431 let worst = polyline
1432 .iter()
1433 .map(|p| (v(*p).sub(center).length() - radius).abs())
1434 .fold(0.0f64, f64::max);
1435 let planar = polyline
1436 .iter()
1437 .map(|p| v(*p).sub(center).dot(axis).abs())
1438 .fold(0.0f64, f64::max);
1439 if worst <= 1e-4 * radius && planar <= 1e-4 * radius {
1440 if chord.length() <= 1e-6 * extent {
1441 return Some(R::Line(center, center.add(axis.scale(radius.max(TIP)))));
1442 }
1443 return Some(R::Arc(center, start, end));
1444 }
1445 }
1446 }
1447 (chord.length() > 1e-9 * extent).then_some(R::Line(start, end))
1448}
1449
1450fn circle_through(a: brep_kernel::Vec3, b: brep_kernel::Vec3, c: brep_kernel::Vec3) -> Option<(brep_kernel::Vec3, f64, brep_kernel::Vec3)> {
1452 let ab = b.sub(a);
1453 let ac = c.sub(a);
1454 let n = ab.cross(ac);
1455 let n2 = n.dot(n);
1456 if n2 <= 1e-24 {
1457 return None;
1458 }
1459 let term = ab.scale(ac.dot(ac)).sub(ac.scale(ab.dot(ab))).cross(n).scale(1.0 / (2.0 * n2));
1461 let center = a.add(term);
1462 let radius = a.sub(center).length();
1463 let axis = n.normalized().ok()?;
1464 Some((center, radius, axis))
1465}