1use crate::json_support::vec3_or as read_vec3;
13use crate::geometry3d::cross3 as cross;
14
15use super::*;
16use super::transform_gizmo::{normalize3, rotate_euler_xyz_f64};
17use serde_json::Value;
18use brep_kernel::PortSide;
19
20const OVERLAY_GROUP: &str = "spline-edit";
22const CAGE_FORWARD: [f32; 3] = [0.35, 0.55, 1.0];
24const CAGE_BACKWARD: [f32; 3] = [0.22, 0.34, 0.62];
25const DOT_PLAIN: [f32; 3] = [0.62, 0.79, 1.0];
27const DOT_ATTACHED: [f32; 3] = [1.0, 0.66, 0.42];
28const DOT_SELECTED: [f32; 3] = [0.435, 0.886, 0.435];
29const DOT_SIZE_PX: f32 = 9.0;
30const NEW_ANCHOR_GAP: f64 = 2.0;
32
33#[derive(Debug, Clone, PartialEq, serde::Serialize)]
35pub struct SplineAnchorRow {
36 pub index: usize,
37 pub id: String,
38 pub position: [f64; 3],
41 pub direction: [f64; 3],
43 pub forward: f64,
44 pub backward: f64,
45 pub flip: bool,
46 pub attached: Option<(String, PortSide)>,
48}
49
50pub(super) fn euler_xyz_deg_from_axes(x: [f64; 3], y: [f64; 3], z: [f64; 3]) -> [f64; 3] {
55 let (m11, m12, m13) = (x[0], y[0], z[0]);
56 let (m22, m23) = (y[1], z[1]);
57 let (m32, m33) = (y[2], z[2]);
58 let ey = m13.clamp(-1.0, 1.0).asin();
59 let (ex, ez) = if m13.abs() < 0.9999999 {
60 ((-m23).atan2(m33), (-m12).atan2(m11))
61 } else {
62 (m32.atan2(m22), 0.0)
63 };
64 [ex.to_degrees(), ey.to_degrees(), ez.to_degrees()]
65}
66
67pub(super) fn axes_from_euler_deg(deg: [f64; 3]) -> [[f64; 3]; 3] {
69 let euler = [deg[0].to_radians(), deg[1].to_radians(), deg[2].to_radians()];
70 [
71 normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler)),
72 normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler)),
73 normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler)),
74 ]
75}
76
77fn axes_from_direction(direction: [f64; 3]) -> [[f64; 3]; 3] {
81 let x = normalize3(direction);
82 let up = if x[2].abs() > 0.9 { [0.0, 1.0, 0.0] } else { [0.0, 0.0, 1.0] };
83 let y = normalize3(cross(up, x));
84 let z = normalize3(cross(x, y));
85 [x, y, z]
86}
87
88fn read_axes(value: Option<&Value>) -> [[f64; 3]; 3] {
89 let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
90 let Some(array) = value.and_then(Value::as_array) else {
91 return identity;
92 };
93 if array.len() != 9 {
94 return identity;
95 }
96 let numbers: Vec<f64> = array.iter().filter_map(Value::as_f64).collect();
97 if numbers.len() != 9 {
98 return identity;
99 }
100 [
101 [numbers[0], numbers[1], numbers[2]],
102 [numbers[3], numbers[4], numbers[5]],
103 [numbers[6], numbers[7], numbers[8]],
104 ]
105}
106
107fn axes_value(axes: [[f64; 3]; 3]) -> Value {
108 Value::Array(axes.iter().flatten().map(|n| Value::from(*n)).collect())
109}
110
111fn distance(value: Option<&Value>) -> f64 {
112 value.and_then(Value::as_f64).map(|d| d.max(0.0)).unwrap_or(1.0)
113}
114
115fn normalize_point(point: &Value, index: usize) -> Value {
117 let id = point
118 .get("id")
119 .and_then(Value::as_str)
120 .filter(|id| !id.is_empty())
121 .map(str::to_string)
122 .unwrap_or_else(|| format!("p{index}"));
123 let attachment = point
124 .get("attachment")
125 .filter(|a| brep_kernel::SplineAttachment::parse(Some(a)).is_some())
126 .cloned()
127 .unwrap_or(Value::Null);
128 serde_json::json!({
129 "id": id,
130 "position": read_vec3(point.get("position"), [0.0, 0.0, 0.0]),
131 "rotation": axes_value(read_axes(point.get("rotation"))),
132 "forwardDistance": distance(point.get("forwardDistance")),
133 "backwardDistance": distance(point.get("backwardDistance")),
134 "flipDirection": point.get("flipDirection").and_then(Value::as_bool).unwrap_or(false),
135 "attachment": attachment,
136 })
137}
138
139fn normalized_points(persistent: Option<&Value>) -> Vec<Value> {
143 let stored: Vec<&Value> = persistent
144 .and_then(|p| p.get("spline"))
145 .and_then(|s| s.get("points"))
146 .and_then(Value::as_array)
147 .map(|points| points.iter().collect())
148 .unwrap_or_default();
149 if stored.len() < 2 {
150 return vec![
151 normalize_point(&serde_json::json!({ "id": "p0", "position": [0, 0, 0] }), 0),
152 normalize_point(&serde_json::json!({ "id": "p1", "position": [5, 0, 0] }), 1),
153 ];
154 }
155 stored
156 .iter()
157 .enumerate()
158 .map(|(index, point)| normalize_point(point, index))
159 .collect()
160}
161
162impl EngineState {
163 pub fn is_port_feature(&self, name: &str) -> bool {
167 self.history
168 .index_of(name)
169 .and_then(|index| self.history.feature_type(index))
170 .is_some_and(|ty| ty == "WP" || ty == "WAYPOINT")
171 }
172
173 pub fn is_port_id(&self, name: &str) -> bool {
178 self.is_port_feature(name)
179 || self.history.declared_points()
180 .iter()
181 .any(|point| point.address() == name)
182 || self
183 .wire_harness_report
184 .as_ref()
185 .is_some_and(|report| report.endpoints.iter().any(|endpoint| endpoint.id == name))
186 }
187
188 pub fn is_spline_feature(&self, feature_id: &str) -> bool {
190 self.history
191 .index_of(feature_id)
192 .and_then(|index| self.history.feature_type(index))
193 .is_some_and(|ty| ty == "SP")
194 }
195
196 pub fn spline_anchors(&self, feature_id: &str) -> Vec<SplineAnchorRow> {
198 let Some(index) = self.history.index_of(feature_id) else {
199 return Vec::new();
200 };
201 if !self.is_spline_feature(feature_id) {
202 return Vec::new();
203 }
204 let points = normalized_points(self.history.feature_persistent_data(index).as_ref());
205 points
206 .iter()
207 .enumerate()
208 .map(|(i, point)| {
209 let name = format!("{feature_id}:P{i}");
210 let axes = read_axes(point.get("rotation"));
211 let flip = point.get("flipDirection").and_then(Value::as_bool).unwrap_or(false);
212 let stored_direction = if flip {
213 [-axes[0][0], -axes[0][1], -axes[0][2]]
214 } else {
215 axes[0]
216 };
217 let position = self
218 .sketch_points
219 .iter()
220 .find(|(n, _)| *n == name)
221 .map(|(_, p)| [p.position.x, p.position.y, p.position.z])
222 .unwrap_or_else(|| read_vec3(point.get("position"), [0.0; 3]));
223 let direction = self
224 .sketch_axes
225 .iter()
226 .find(|(n, _)| *n == name)
227 .map(|(_, a)| [a.direction.x, a.direction.y, a.direction.z])
228 .unwrap_or_else(|| normalize3(stored_direction));
229 let attached = brep_kernel::SplineAttachment::parse(point.get("attachment"))
230 .map(|a| (a.port_ref, a.side));
231 SplineAnchorRow {
232 index: i,
233 id: point.get("id").and_then(Value::as_str).unwrap_or("").to_string(),
234 position,
235 direction,
236 forward: distance(point.get("forwardDistance")),
237 backward: distance(point.get("backwardDistance")),
238 flip,
239 attached,
240 }
241 })
242 .collect()
243 }
244
245 pub fn armed_spline_anchor(&self) -> Option<(String, usize)> {
247 let anchor = self.transform_gizmo.anchor?;
248 let id = self.transform_gizmo.feature_id.clone()?;
249 Some((id, anchor))
250 }
251
252 pub fn spline_edit_feature(&self) -> Option<&str> {
255 self.spline_edit_feature.as_deref()
256 }
257
258 pub fn spline_anchor_pick_at(&mut self, x: f64, y: f64) -> Option<usize> {
264 let feature_id = self.spline_edit_feature.clone()?;
265 let candidates = self.pick_candidates_at(x, y);
266 let hit = candidates
267 .iter()
268 .find(|c| matches!(c.kind, crate::pick::PickKind::Vertex) && c.solid == feature_id)?;
269 let rows = self.spline_anchors(&feature_id);
270 let index = rows
271 .iter()
272 .map(|row| {
273 let d = [0, 1, 2]
274 .iter()
275 .map(|&k| (row.position[k] - hit.position[k]).powi(2))
276 .sum::<f64>()
277 .sqrt();
278 (row.index, d, row.position)
279 })
280 .filter(|(_, d, p)| {
281 *d <= 1e-4 * (1.0 + p.iter().map(|v| v.abs()).fold(0.0, f64::max))
282 })
283 .min_by(|a, b| a.1.total_cmp(&b.1))?
284 .0;
285 self.arm_spline_anchor(&feature_id, index);
286 self.spline_anchor_picked = Some(index);
287 Some(index)
288 }
289
290 pub fn take_spline_anchor_pick(&mut self) -> Option<usize> {
293 self.spline_anchor_picked.take()
294 }
295
296 fn spline_points(&self, feature_id: &str) -> Option<Vec<Value>> {
300 let index = self.history.index_of(feature_id)?;
301 if !self.is_spline_feature(feature_id) {
302 return None;
303 }
304 Some(normalized_points(self.history.feature_persistent_data(index).as_ref()))
305 }
306
307 fn write_spline_points(&mut self, feature_id: &str, points: Vec<Value>, coalesce: Option<&str>) {
310 let Some(index) = self.history.index_of(feature_id) else {
311 return;
312 };
313 self.history.set_feature_persistent_field_coalesced(
314 index,
315 "spline",
316 serde_json::json!({ "points": points }),
317 coalesce,
318 );
319 self.rerun_history();
320 }
321
322 fn edit_spline_anchor(
324 &mut self,
325 feature_id: &str,
326 anchor: usize,
327 coalesce: Option<&str>,
328 edit: impl FnOnce(&mut serde_json::Map<String, Value>),
329 ) -> Result<(), String> {
330 let mut points = self
331 .spline_points(feature_id)
332 .ok_or_else(|| format!("'{feature_id}' is not a spline"))?;
333 let point = points
334 .get_mut(anchor)
335 .and_then(Value::as_object_mut)
336 .ok_or_else(|| format!("spline '{feature_id}' has no anchor {anchor}"))?;
337 edit(point);
338 self.write_spline_points(feature_id, points, coalesce);
339 Ok(())
340 }
341
342 pub fn spline_set_anchor_position(
344 &mut self,
345 feature_id: &str,
346 anchor: usize,
347 position: [f64; 3],
348 ) -> Result<(), String> {
349 let key = format!("spline-anchor-position:{feature_id}:{anchor}");
350 self.edit_spline_anchor(feature_id, anchor, Some(&key), |point| {
351 point.insert("position".into(), serde_json::json!(position));
352 })
353 }
354
355 pub fn spline_set_anchor_distances(
358 &mut self,
359 feature_id: &str,
360 anchor: usize,
361 forward: Option<f64>,
362 backward: Option<f64>,
363 ) -> Result<(), String> {
364 let key = format!("spline-anchor-distances:{feature_id}:{anchor}");
365 self.edit_spline_anchor(feature_id, anchor, Some(&key), |point| {
366 if let Some(forward) = forward {
367 point.insert("forwardDistance".into(), Value::from(forward.max(0.0)));
368 }
369 if let Some(backward) = backward {
370 point.insert("backwardDistance".into(), Value::from(backward.max(0.0)));
371 }
372 })
373 }
374
375 pub fn spline_set_anchor_flip(&mut self, feature_id: &str, anchor: usize, flip: bool) -> Result<(), String> {
378 self.edit_spline_anchor(feature_id, anchor, None, |point| {
379 point.insert("flipDirection".into(), Value::Bool(flip));
380 })
381 }
382
383 pub fn spline_set_anchor_side(&mut self, feature_id: &str, anchor: usize, side: PortSide) -> Result<(), String> {
385 self.edit_spline_anchor(feature_id, anchor, None, |point| {
386 if let Some(attachment) = point.get_mut("attachment").and_then(Value::as_object_mut) {
387 attachment.insert("side".into(), Value::String(side.letter().to_string()));
388 }
389 })
390 }
391
392 pub fn spline_detach_anchor(&mut self, feature_id: &str, anchor: usize) -> Result<(), String> {
396 let row = self
397 .spline_anchors(feature_id)
398 .into_iter()
399 .nth(anchor)
400 .ok_or_else(|| format!("spline '{feature_id}' has no anchor {anchor}"))?;
401 self.edit_spline_anchor(feature_id, anchor, None, |point| {
402 point.insert("attachment".into(), Value::Null);
403 point.insert("position".into(), serde_json::json!(row.position));
404 point.insert("rotation".into(), axes_value(axes_from_direction(row.direction)));
405 point.insert("flipDirection".into(), Value::Bool(false));
406 })
407 }
408
409 pub fn spline_add_anchor(&mut self, feature_id: &str, after: Option<usize>) -> Result<usize, String> {
413 let rows = self.spline_anchors(feature_id);
414 if rows.is_empty() {
415 return Err(format!("'{feature_id}' is not a spline"));
416 }
417 let mut points = self.spline_points(feature_id).expect("a spline by the check above");
418 let at = after.unwrap_or(rows.len() - 1).min(rows.len() - 1);
419 let reference = &rows[at];
420 let step = reference.forward + NEW_ANCHOR_GAP;
421 let position = [
422 reference.position[0] + reference.direction[0] * step,
423 reference.position[1] + reference.direction[1] * step,
424 reference.position[2] + reference.direction[2] * step,
425 ];
426 let next_number = points.len();
427 let point = serde_json::json!({
428 "id": format!("p{next_number}"),
429 "position": position,
430 "rotation": axes_value(axes_from_direction(reference.direction)),
431 "forwardDistance": 1.0,
432 "backwardDistance": 1.0,
433 "flipDirection": false,
434 "attachment": Value::Null,
435 });
436 let index = at + 1;
437 points.insert(index, point);
438 self.write_spline_points(feature_id, points, None);
439 Ok(index)
440 }
441
442 pub fn spline_remove_anchor(&mut self, feature_id: &str, anchor: usize) -> Result<(), String> {
444 let mut points = self
445 .spline_points(feature_id)
446 .ok_or_else(|| format!("'{feature_id}' is not a spline"))?;
447 if anchor >= points.len() {
448 return Err(format!("spline '{feature_id}' has no anchor {anchor}"));
449 }
450 if points.len() <= 2 {
451 return Err("a spline keeps at least two anchors".into());
452 }
453 points.remove(anchor);
454 if self.transform_gizmo.anchor.is_some()
455 && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
456 {
457 self.disarm_transform();
458 }
459 self.write_spline_points(feature_id, points, None);
460 Ok(())
461 }
462
463 pub fn spline_move_anchor(&mut self, feature_id: &str, anchor: usize, up: bool) -> Result<(), String> {
465 let mut points = self
466 .spline_points(feature_id)
467 .ok_or_else(|| format!("'{feature_id}' is not a spline"))?;
468 let target = if up {
469 anchor.checked_sub(1)
470 } else {
471 (anchor + 1 < points.len()).then_some(anchor + 1)
472 };
473 let Some(target) = target else {
474 return Ok(()); };
476 if anchor >= points.len() {
477 return Err(format!("spline '{feature_id}' has no anchor {anchor}"));
478 }
479 points.swap(anchor, target);
480 if self.transform_gizmo.anchor == Some(anchor)
481 && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
482 {
483 self.transform_gizmo.anchor = Some(target);
484 }
485 self.write_spline_points(feature_id, points, None);
486 Ok(())
487 }
488
489 pub fn arm_spline_anchor(&mut self, feature_id: &str, anchor: usize) -> bool {
494 let rows = self.spline_anchors(feature_id);
495 let Some(row) = rows.get(anchor) else {
496 return false;
497 };
498 if row.attached.is_some() {
499 if self.transform_gizmo.anchor.is_some() {
503 self.disarm_transform();
504 }
505 return false;
506 }
507 self.component_move_reset();
508 self.transform_gizmo.feature_id = Some(feature_id.to_string());
509 self.transform_gizmo.mode = GizmoMode::Transform;
510 self.transform_gizmo.drag = None;
511 self.transform_gizmo.anchor = Some(anchor);
512 self.clear_feature_dimension_overlay();
513 self.sync_transform_gizmo();
514 true
515 }
516
517 pub(super) fn spline_anchor_pose(&self, feature_index: usize, anchor: usize) -> Option<([f64; 3], [f64; 3])> {
520 let points = normalized_points(self.history.feature_persistent_data(feature_index).as_ref());
521 let point = points.get(anchor)?;
522 let axes = read_axes(point.get("rotation"));
523 Some((
524 read_vec3(point.get("position"), [0.0; 3]),
525 euler_xyz_deg_from_axes(axes[0], axes[1], axes[2]),
526 ))
527 }
528
529 pub(super) fn write_spline_anchor_pose(
532 &mut self,
533 feature_id: &str,
534 anchor: usize,
535 position: [f64; 3],
536 rotation_deg: [f64; 3],
537 ) {
538 let key = format!("spline-anchor-pose:{feature_id}:{anchor}");
539 let _ = self.edit_spline_anchor(feature_id, anchor, Some(&key), |point| {
540 point.insert("position".into(), serde_json::json!(position));
541 point.insert("rotation".into(), axes_value(axes_from_euler_deg(rotation_deg)));
542 });
543 }
544
545 pub fn begin_ref_select_for_spline_anchor(&mut self, feature_id: &str, anchor: usize) {
551 let restore_index = self.history.rollback();
552 let before = self
553 .history
554 .index_of(feature_id)
555 .map(|i| i.saturating_sub(1))
556 .unwrap_or(restore_index);
557 let filter: Vec<String> = ["SKETCH", "EDGE", "VERTEX"].iter().map(|s| s.to_string()).collect();
558 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
559 let seed = self
560 .spline_anchors(feature_id)
561 .get(anchor)
562 .and_then(|row| row.attached.as_ref().map(|(port, _)| port.clone()))
563 .into_iter()
564 .collect();
565 self.ref_select = Some(RefSelectState {
566 feature_id: feature_id.to_string(),
567 path: Vec::new(),
568 label: format!("Anchor {anchor}: attach to port"),
569 filter,
570 multiple: false,
571 names: seed,
572 restore_index,
573 target: RefSelectTarget::SplineAnchor { index: anchor },
574 picks: 0,
575 claimed: false,
576 finish_requested: false,
577 });
578 if before != restore_index {
579 self.history.set_rollback(before);
580 self.rerun_history();
581 }
582 self.sync_ref_select_emphasis();
583 }
584
585 pub(super) fn attach_spline_anchor_no_rerun(&mut self, feature_id: &str, anchor: usize, port: &str) {
589 let Some(index) = self.history.index_of(feature_id) else {
590 return;
591 };
592 let Some(mut points) = self.spline_points(feature_id) else {
593 return;
594 };
595 let Some(point) = points.get_mut(anchor).and_then(Value::as_object_mut) else {
596 return;
597 };
598 let side = brep_kernel::SplineAttachment::parse(point.get("attachment"))
599 .filter(|current| current.port_ref == port)
600 .map(|current| current.side)
601 .unwrap_or(PortSide::A);
602 point.insert(
603 "attachment".into(),
604 serde_json::json!({ "portRef": port, "side": side.letter() }),
605 );
606 if self.transform_gizmo.anchor == Some(anchor)
607 && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
608 {
609 self.transform_gizmo.anchor = None;
611 self.transform_gizmo.feature_id = None;
612 self.transform_gizmo.mode = GizmoMode::None;
613 let _ = self.widgets.set_transform_json("null");
614 }
615 self.history.set_feature_persistent_field(index, "spline", serde_json::json!({ "points": points }));
616 }
617
618 pub fn refresh_spline_edit_overlay(&mut self, feature_id: Option<&str>, selected: Option<usize>) {
624 if self.spline_edit_feature.as_deref() != feature_id {
625 self.spline_edit_feature = feature_id.map(str::to_string);
626 self.spline_anchor_picked = None;
627 }
628 let key = feature_id.map(|id| (id.to_string(), selected, self.applied_generation));
629 if self.spline_overlay_key == key {
630 return;
631 }
632 self.spline_overlay_key = key;
633 let Some(feature_id) = feature_id else {
634 let _ = self
635 .widgets
636 .set_overlay_json(&serde_json::json!({ "groups": [{ "name": OVERLAY_GROUP }] }).to_string());
637 self.dirty = true;
638 return;
639 };
640 let rows = self.spline_anchors(feature_id);
641 let mut line_positions: Vec<f64> = Vec::new();
642 let mut line_colors: Vec<f32> = Vec::new();
643 let mut dot_positions: Vec<f64> = Vec::new();
644 let mut dot_colors: Vec<f32> = Vec::new();
645 let mut push_line = |a: [f64; 3], b: [f64; 3], color: [f32; 3]| {
646 line_positions.extend_from_slice(&a);
647 line_positions.extend_from_slice(&b);
648 line_colors.extend_from_slice(&color);
649 line_colors.extend_from_slice(&color);
650 };
651 for row in &rows {
652 let along = |scale: f64| {
653 [
654 row.position[0] + row.direction[0] * scale,
655 row.position[1] + row.direction[1] * scale,
656 row.position[2] + row.direction[2] * scale,
657 ]
658 };
659 if row.forward > 0.0 {
660 push_line(row.position, along(row.forward), CAGE_FORWARD);
661 }
662 if row.backward > 0.0 {
663 push_line(row.position, along(-row.backward), CAGE_BACKWARD);
664 }
665 dot_positions.extend_from_slice(&row.position);
666 let color = if Some(row.index) == selected {
667 DOT_SELECTED
668 } else if row.attached.is_some() {
669 DOT_ATTACHED
670 } else {
671 DOT_PLAIN
672 };
673 dot_colors.extend_from_slice(&color);
674 }
675 let group = serde_json::json!({
676 "name": OVERLAY_GROUP,
677 "renderOrder": 6,
678 "lines": { "positions": line_positions, "colors": line_colors },
679 "points": { "positions": dot_positions, "colors": dot_colors, "size": DOT_SIZE_PX },
680 });
681 let _ = self
682 .widgets
683 .set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
684 self.dirty = true;
685 }
686}
687