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
67fn 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 == "PORT")
171 }
172
173 pub fn is_port_id(&self, name: &str) -> bool {
177 self.is_port_feature(name)
178 || self
179 .wire_harness_report
180 .as_ref()
181 .is_some_and(|report| report.endpoints.iter().any(|endpoint| endpoint.id == name))
182 }
183
184 pub fn is_spline_feature(&self, feature_id: &str) -> bool {
186 self.history
187 .index_of(feature_id)
188 .and_then(|index| self.history.feature_type(index))
189 .is_some_and(|ty| ty == "SP")
190 }
191
192 pub fn spline_anchors(&self, feature_id: &str) -> Vec<SplineAnchorRow> {
194 let Some(index) = self.history.index_of(feature_id) else {
195 return Vec::new();
196 };
197 if !self.is_spline_feature(feature_id) {
198 return Vec::new();
199 }
200 let points = normalized_points(self.history.feature_persistent_data(index).as_ref());
201 points
202 .iter()
203 .enumerate()
204 .map(|(i, point)| {
205 let name = format!("{feature_id}:P{i}");
206 let axes = read_axes(point.get("rotation"));
207 let flip = point.get("flipDirection").and_then(Value::as_bool).unwrap_or(false);
208 let stored_direction = if flip {
209 [-axes[0][0], -axes[0][1], -axes[0][2]]
210 } else {
211 axes[0]
212 };
213 let position = self
214 .sketch_points
215 .iter()
216 .find(|(n, _)| *n == name)
217 .map(|(_, p)| [p.position.x, p.position.y, p.position.z])
218 .unwrap_or_else(|| read_vec3(point.get("position"), [0.0; 3]));
219 let direction = self
220 .sketch_axes
221 .iter()
222 .find(|(n, _)| *n == name)
223 .map(|(_, a)| [a.direction.x, a.direction.y, a.direction.z])
224 .unwrap_or_else(|| normalize3(stored_direction));
225 let attached = brep_kernel::SplineAttachment::parse(point.get("attachment"))
226 .map(|a| (a.port_ref, a.side));
227 SplineAnchorRow {
228 index: i,
229 id: point.get("id").and_then(Value::as_str).unwrap_or("").to_string(),
230 position,
231 direction,
232 forward: distance(point.get("forwardDistance")),
233 backward: distance(point.get("backwardDistance")),
234 flip,
235 attached,
236 }
237 })
238 .collect()
239 }
240
241 pub fn armed_spline_anchor(&self) -> Option<(String, usize)> {
243 let anchor = self.transform_gizmo.anchor?;
244 let id = self.transform_gizmo.feature_id.clone()?;
245 Some((id, anchor))
246 }
247
248 pub fn spline_edit_feature(&self) -> Option<&str> {
251 self.spline_edit_feature.as_deref()
252 }
253
254 pub fn spline_anchor_pick_at(&mut self, x: f64, y: f64) -> Option<usize> {
260 let feature_id = self.spline_edit_feature.clone()?;
261 let candidates = self.pick_candidates_at(x, y);
262 let hit = candidates
263 .iter()
264 .find(|c| matches!(c.kind, crate::pick::PickKind::Vertex) && c.solid == feature_id)?;
265 let rows = self.spline_anchors(&feature_id);
266 let index = rows
267 .iter()
268 .map(|row| {
269 let d = [0, 1, 2]
270 .iter()
271 .map(|&k| (row.position[k] - hit.position[k]).powi(2))
272 .sum::<f64>()
273 .sqrt();
274 (row.index, d, row.position)
275 })
276 .filter(|(_, d, p)| {
277 *d <= 1e-4 * (1.0 + p.iter().map(|v| v.abs()).fold(0.0, f64::max))
278 })
279 .min_by(|a, b| a.1.total_cmp(&b.1))?
280 .0;
281 self.arm_spline_anchor(&feature_id, index);
282 self.spline_anchor_picked = Some(index);
283 Some(index)
284 }
285
286 pub fn take_spline_anchor_pick(&mut self) -> Option<usize> {
289 self.spline_anchor_picked.take()
290 }
291
292 fn spline_points(&self, feature_id: &str) -> Option<Vec<Value>> {
296 let index = self.history.index_of(feature_id)?;
297 if !self.is_spline_feature(feature_id) {
298 return None;
299 }
300 Some(normalized_points(self.history.feature_persistent_data(index).as_ref()))
301 }
302
303 fn write_spline_points(&mut self, feature_id: &str, points: Vec<Value>, coalesce: Option<&str>) {
306 let Some(index) = self.history.index_of(feature_id) else {
307 return;
308 };
309 self.history.set_feature_persistent_field_coalesced(
310 index,
311 "spline",
312 serde_json::json!({ "points": points }),
313 coalesce,
314 );
315 self.rerun_history();
316 }
317
318 fn edit_spline_anchor(
320 &mut self,
321 feature_id: &str,
322 anchor: usize,
323 coalesce: Option<&str>,
324 edit: impl FnOnce(&mut serde_json::Map<String, Value>),
325 ) -> Result<(), String> {
326 let mut points = self
327 .spline_points(feature_id)
328 .ok_or_else(|| format!("'{feature_id}' is not a spline"))?;
329 let point = points
330 .get_mut(anchor)
331 .and_then(Value::as_object_mut)
332 .ok_or_else(|| format!("spline '{feature_id}' has no anchor {anchor}"))?;
333 edit(point);
334 self.write_spline_points(feature_id, points, coalesce);
335 Ok(())
336 }
337
338 pub fn spline_set_anchor_position(
340 &mut self,
341 feature_id: &str,
342 anchor: usize,
343 position: [f64; 3],
344 ) -> Result<(), String> {
345 let key = format!("spline-anchor-position:{feature_id}:{anchor}");
346 self.edit_spline_anchor(feature_id, anchor, Some(&key), |point| {
347 point.insert("position".into(), serde_json::json!(position));
348 })
349 }
350
351 pub fn spline_set_anchor_distances(
354 &mut self,
355 feature_id: &str,
356 anchor: usize,
357 forward: Option<f64>,
358 backward: Option<f64>,
359 ) -> Result<(), String> {
360 let key = format!("spline-anchor-distances:{feature_id}:{anchor}");
361 self.edit_spline_anchor(feature_id, anchor, Some(&key), |point| {
362 if let Some(forward) = forward {
363 point.insert("forwardDistance".into(), Value::from(forward.max(0.0)));
364 }
365 if let Some(backward) = backward {
366 point.insert("backwardDistance".into(), Value::from(backward.max(0.0)));
367 }
368 })
369 }
370
371 pub fn spline_set_anchor_flip(&mut self, feature_id: &str, anchor: usize, flip: bool) -> Result<(), String> {
374 self.edit_spline_anchor(feature_id, anchor, None, |point| {
375 point.insert("flipDirection".into(), Value::Bool(flip));
376 })
377 }
378
379 pub fn spline_set_anchor_side(&mut self, feature_id: &str, anchor: usize, side: PortSide) -> Result<(), String> {
381 self.edit_spline_anchor(feature_id, anchor, None, |point| {
382 if let Some(attachment) = point.get_mut("attachment").and_then(Value::as_object_mut) {
383 attachment.insert("side".into(), Value::String(side.letter().to_string()));
384 }
385 })
386 }
387
388 pub fn spline_detach_anchor(&mut self, feature_id: &str, anchor: usize) -> Result<(), String> {
392 let row = self
393 .spline_anchors(feature_id)
394 .into_iter()
395 .nth(anchor)
396 .ok_or_else(|| format!("spline '{feature_id}' has no anchor {anchor}"))?;
397 self.edit_spline_anchor(feature_id, anchor, None, |point| {
398 point.insert("attachment".into(), Value::Null);
399 point.insert("position".into(), serde_json::json!(row.position));
400 point.insert("rotation".into(), axes_value(axes_from_direction(row.direction)));
401 point.insert("flipDirection".into(), Value::Bool(false));
402 })
403 }
404
405 pub fn spline_add_anchor(&mut self, feature_id: &str, after: Option<usize>) -> Result<usize, String> {
409 let rows = self.spline_anchors(feature_id);
410 if rows.is_empty() {
411 return Err(format!("'{feature_id}' is not a spline"));
412 }
413 let mut points = self.spline_points(feature_id).expect("a spline by the check above");
414 let at = after.unwrap_or(rows.len() - 1).min(rows.len() - 1);
415 let reference = &rows[at];
416 let step = reference.forward + NEW_ANCHOR_GAP;
417 let position = [
418 reference.position[0] + reference.direction[0] * step,
419 reference.position[1] + reference.direction[1] * step,
420 reference.position[2] + reference.direction[2] * step,
421 ];
422 let next_number = points.len();
423 let point = serde_json::json!({
424 "id": format!("p{next_number}"),
425 "position": position,
426 "rotation": axes_value(axes_from_direction(reference.direction)),
427 "forwardDistance": 1.0,
428 "backwardDistance": 1.0,
429 "flipDirection": false,
430 "attachment": Value::Null,
431 });
432 let index = at + 1;
433 points.insert(index, point);
434 self.write_spline_points(feature_id, points, None);
435 Ok(index)
436 }
437
438 pub fn spline_remove_anchor(&mut self, feature_id: &str, anchor: usize) -> Result<(), String> {
440 let mut points = self
441 .spline_points(feature_id)
442 .ok_or_else(|| format!("'{feature_id}' is not a spline"))?;
443 if anchor >= points.len() {
444 return Err(format!("spline '{feature_id}' has no anchor {anchor}"));
445 }
446 if points.len() <= 2 {
447 return Err("a spline keeps at least two anchors".into());
448 }
449 points.remove(anchor);
450 if self.transform_gizmo.anchor.is_some()
451 && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
452 {
453 self.disarm_transform();
454 }
455 self.write_spline_points(feature_id, points, None);
456 Ok(())
457 }
458
459 pub fn spline_move_anchor(&mut self, feature_id: &str, anchor: usize, up: bool) -> Result<(), String> {
461 let mut points = self
462 .spline_points(feature_id)
463 .ok_or_else(|| format!("'{feature_id}' is not a spline"))?;
464 let target = if up {
465 anchor.checked_sub(1)
466 } else {
467 (anchor + 1 < points.len()).then_some(anchor + 1)
468 };
469 let Some(target) = target else {
470 return Ok(()); };
472 if anchor >= points.len() {
473 return Err(format!("spline '{feature_id}' has no anchor {anchor}"));
474 }
475 points.swap(anchor, target);
476 if self.transform_gizmo.anchor == Some(anchor)
477 && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
478 {
479 self.transform_gizmo.anchor = Some(target);
480 }
481 self.write_spline_points(feature_id, points, None);
482 Ok(())
483 }
484
485 pub fn arm_spline_anchor(&mut self, feature_id: &str, anchor: usize) -> bool {
490 let rows = self.spline_anchors(feature_id);
491 let Some(row) = rows.get(anchor) else {
492 return false;
493 };
494 if row.attached.is_some() {
495 if self.transform_gizmo.anchor.is_some() {
499 self.disarm_transform();
500 }
501 return false;
502 }
503 self.component_move_reset();
504 self.transform_gizmo.feature_id = Some(feature_id.to_string());
505 self.transform_gizmo.mode = GizmoMode::Transform;
506 self.transform_gizmo.drag = None;
507 self.transform_gizmo.anchor = Some(anchor);
508 self.clear_feature_dimension_overlay();
509 self.sync_transform_gizmo();
510 true
511 }
512
513 pub(super) fn spline_anchor_pose(&self, feature_index: usize, anchor: usize) -> Option<([f64; 3], [f64; 3])> {
516 let points = normalized_points(self.history.feature_persistent_data(feature_index).as_ref());
517 let point = points.get(anchor)?;
518 let axes = read_axes(point.get("rotation"));
519 Some((
520 read_vec3(point.get("position"), [0.0; 3]),
521 euler_xyz_deg_from_axes(axes[0], axes[1], axes[2]),
522 ))
523 }
524
525 pub(super) fn write_spline_anchor_pose(
528 &mut self,
529 feature_id: &str,
530 anchor: usize,
531 position: [f64; 3],
532 rotation_deg: [f64; 3],
533 ) {
534 let key = format!("spline-anchor-pose:{feature_id}:{anchor}");
535 let _ = self.edit_spline_anchor(feature_id, anchor, Some(&key), |point| {
536 point.insert("position".into(), serde_json::json!(position));
537 point.insert("rotation".into(), axes_value(axes_from_euler_deg(rotation_deg)));
538 });
539 }
540
541 pub fn begin_ref_select_for_spline_anchor(&mut self, feature_id: &str, anchor: usize) {
547 let restore_index = self.history.rollback();
548 let before = self
549 .history
550 .index_of(feature_id)
551 .map(|i| i.saturating_sub(1))
552 .unwrap_or(restore_index);
553 let filter: Vec<String> = ["SKETCH", "EDGE", "VERTEX"].iter().map(|s| s.to_string()).collect();
554 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
555 let seed = self
556 .spline_anchors(feature_id)
557 .get(anchor)
558 .and_then(|row| row.attached.as_ref().map(|(port, _)| port.clone()))
559 .into_iter()
560 .collect();
561 self.ref_select = Some(RefSelectState {
562 feature_id: feature_id.to_string(),
563 path: Vec::new(),
564 label: format!("Anchor {anchor}: attach to port"),
565 filter,
566 multiple: false,
567 names: seed,
568 restore_index,
569 target: RefSelectTarget::SplineAnchor { index: anchor },
570 });
571 if before != restore_index {
572 self.history.set_rollback(before);
573 self.rerun_history();
574 }
575 self.sync_ref_select_emphasis();
576 }
577
578 pub(super) fn attach_spline_anchor_no_rerun(&mut self, feature_id: &str, anchor: usize, port: &str) {
582 let Some(index) = self.history.index_of(feature_id) else {
583 return;
584 };
585 let Some(mut points) = self.spline_points(feature_id) else {
586 return;
587 };
588 let Some(point) = points.get_mut(anchor).and_then(Value::as_object_mut) else {
589 return;
590 };
591 let side = brep_kernel::SplineAttachment::parse(point.get("attachment"))
592 .filter(|current| current.port_ref == port)
593 .map(|current| current.side)
594 .unwrap_or(PortSide::A);
595 point.insert(
596 "attachment".into(),
597 serde_json::json!({ "portRef": port, "side": side.letter() }),
598 );
599 if self.transform_gizmo.anchor == Some(anchor)
600 && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
601 {
602 self.transform_gizmo.anchor = None;
604 self.transform_gizmo.feature_id = None;
605 self.transform_gizmo.mode = GizmoMode::None;
606 let _ = self.widgets.set_transform_json("null");
607 }
608 self.history.set_feature_persistent_field(index, "spline", serde_json::json!({ "points": points }));
609 }
610
611 pub fn refresh_spline_edit_overlay(&mut self, feature_id: Option<&str>, selected: Option<usize>) {
617 if self.spline_edit_feature.as_deref() != feature_id {
618 self.spline_edit_feature = feature_id.map(str::to_string);
619 self.spline_anchor_picked = None;
620 }
621 let key = feature_id.map(|id| (id.to_string(), selected, self.applied_generation));
622 if self.spline_overlay_key == key {
623 return;
624 }
625 self.spline_overlay_key = key;
626 let Some(feature_id) = feature_id else {
627 let _ = self
628 .widgets
629 .set_overlay_json(&serde_json::json!({ "groups": [{ "name": OVERLAY_GROUP }] }).to_string());
630 self.dirty = true;
631 return;
632 };
633 let rows = self.spline_anchors(feature_id);
634 let mut line_positions: Vec<f64> = Vec::new();
635 let mut line_colors: Vec<f32> = Vec::new();
636 let mut dot_positions: Vec<f64> = Vec::new();
637 let mut dot_colors: Vec<f32> = Vec::new();
638 let mut push_line = |a: [f64; 3], b: [f64; 3], color: [f32; 3]| {
639 line_positions.extend_from_slice(&a);
640 line_positions.extend_from_slice(&b);
641 line_colors.extend_from_slice(&color);
642 line_colors.extend_from_slice(&color);
643 };
644 for row in &rows {
645 let along = |scale: f64| {
646 [
647 row.position[0] + row.direction[0] * scale,
648 row.position[1] + row.direction[1] * scale,
649 row.position[2] + row.direction[2] * scale,
650 ]
651 };
652 if row.forward > 0.0 {
653 push_line(row.position, along(row.forward), CAGE_FORWARD);
654 }
655 if row.backward > 0.0 {
656 push_line(row.position, along(-row.backward), CAGE_BACKWARD);
657 }
658 dot_positions.extend_from_slice(&row.position);
659 let color = if Some(row.index) == selected {
660 DOT_SELECTED
661 } else if row.attached.is_some() {
662 DOT_ATTACHED
663 } else {
664 DOT_PLAIN
665 };
666 dot_colors.extend_from_slice(&color);
667 }
668 let group = serde_json::json!({
669 "name": OVERLAY_GROUP,
670 "renderOrder": 6,
671 "lines": { "positions": line_positions, "colors": line_colors },
672 "points": { "positions": dot_positions, "colors": dot_colors, "size": DOT_SIZE_PX },
673 });
674 let _ = self
675 .widgets
676 .set_overlay_json(&serde_json::json!({ "groups": [group] }).to_string());
677 self.dirty = true;
678 }
679}
680
681