1use super::*;
2
3#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
10pub enum GizmoMode {
11 #[default]
13 None,
14 Transform,
16 Dimension,
18}
19
20#[derive(Default)]
22pub struct TransformArm {
23 pub(super) feature_id: Option<String>,
26 pub(super) mode: GizmoMode,
29 pub(super) drag: Option<TransformDrag>,
31 pub(super) anchor: Option<usize>,
37 pub(super) port: Option<String>,
43}
44
45#[derive(Clone, Copy)]
50pub(super) struct TransformDrag {
51 handle: u32,
52 sx: f32,
53 sy: f32,
54 start: TransformPose,
55 last: Option<TransformDelta>,
62}
63
64#[derive(Clone, Copy, PartialEq, Debug)]
68pub(super) struct TransformPose {
69 pub(super) position: [f64; 3],
70 pub(super) rotation_deg: [f64; 3],
71 pub(super) scale: [f64; 3],
72}
73
74#[derive(Clone, Copy, PartialEq, Debug)]
76pub(super) enum TransformDelta {
77 Translate([f64; 3]),
79 Rotate { axis: [f64; 3], radians: f64 },
81}
82
83pub(super) fn parse_drag_delta(json: &str) -> Option<TransformDelta> {
85 let value: serde_json::Value = serde_json::from_str(json).ok()?;
86 match value.get("kind").and_then(|k| k.as_str()) {
87 Some("translate") => Some(TransformDelta::Translate(
88 crate::json_support::vec3_or(value.get("world"), [0.0; 3]),
89 )),
90 Some("rotate") => Some(TransformDelta::Rotate {
91 axis: crate::json_support::vec3_or(value.get("axisWorld"), [0.0; 3]),
92 radians: value.get("radians").and_then(|n| n.as_f64()).unwrap_or(0.0),
93 }),
94 _ => None,
95 }
96}
97
98impl EngineState {
99 pub fn transform_armed(&self) -> bool {
103 matches!(self.transform_gizmo.mode, GizmoMode::Transform)
104 }
105
106 pub fn transform_armed_feature(&self) -> String {
108 if self.transform_armed() {
109 self.transform_gizmo.feature_id.clone().unwrap_or_default()
110 } else {
111 String::new()
112 }
113 }
114
115 pub fn gizmo_state_json(&self) -> String {
121 let mode = match self.transform_gizmo.mode {
122 GizmoMode::None => "none",
123 GizmoMode::Transform => "transform",
124 GizmoMode::Dimension => "dimension",
125 };
126 serde_json::json!({
127 "mode": mode,
128 "armed": self.transform_armed(),
129 "feature": self.transform_gizmo.feature_id,
130 "anchor": self.transform_gizmo.anchor,
131 "portPoint": self.transform_gizmo.port,
132 "origin": self.widgets.transform_origin(),
133 "pivot": self.armed_face_transform_pivot(),
139 })
140 .to_string()
141 }
142
143 pub fn armed_face_transform_pivot(&self) -> Option<[f64; 3]> {
147 let id = self.transform_gizmo.feature_id.as_deref()?;
148 let index = self.history.index_of(id)?;
149 if !self.is_face_transform_feature(index) {
150 return None;
151 }
152 let params = self.history.feature_params(index)?;
153 let env = brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).ok();
154 self.face_transform_pivot(index, ¶ms, env.as_ref())
155 }
156
157 pub fn transform_readout_json(&self) -> String {
182 let quiet = serde_json::json!({ "active": false }).to_string();
183 if !self.transform_armed() {
184 return quiet;
185 }
186 let Some(origin) = self.widgets.transform_origin() else {
187 return quiet;
188 };
189
190 let refusal = self.armed_face_transform_refusal();
193 let drag = self.transform_gizmo.drag.as_ref();
194 let delta = drag.and_then(|d| d.last);
195
196 if delta.is_none() && refusal.is_none() {
199 return quiet;
200 }
201
202 let (kind, label, value, unit, text) = match delta {
206 Some(TransformDelta::Translate(world)) => {
207 let distance = (world[0] * world[0] + world[1] * world[1] + world[2] * world[2]).sqrt();
208 let axis = dominant_axis_label(&world);
209 let shown = crate::formatting::compact_decimal(distance, 4);
210 (
211 "translate",
212 axis.to_string(),
213 distance,
214 "mm",
215 format!("{axis} {shown} mm"),
216 )
217 }
218 Some(TransformDelta::Rotate { axis, radians }) => {
219 let degrees = radians.to_degrees();
220 let label = dominant_axis_label(&axis);
221 let shown = crate::formatting::compact_decimal(degrees, 4);
222 (
223 "rotate",
224 label.to_string(),
225 degrees,
226 "deg",
227 format!("{label} {shown}\u{00b0}"),
228 )
229 }
230 None => ("none", String::new(), 0.0, "", String::new()),
233 };
234
235 let mut out = serde_json::json!({
236 "active": true,
237 "kind": kind,
238 "axis": label,
239 "value": value,
240 "unit": unit,
241 "text": text,
242 "origin": origin,
243 "dragging": drag.is_some(),
244 "refused": refusal.is_some(),
245 });
246 if let Some(refusal) = refusal {
247 out["motion"] = serde_json::json!(refusal.motion);
248 out["reason"] = serde_json::json!(refusal.reason);
249 out["hint"] = serde_json::json!(refusal.hint);
250 out["kernel"] = serde_json::json!(refusal.kernel);
251 }
252 out.to_string()
253 }
254
255 fn armed_face_transform_refusal(&self) -> Option<crate::face_transform_help::FaceTransformRefusal> {
260 let id = self.transform_gizmo.feature_id.as_deref()?;
261 let index = self.history.index_of(id)?;
262 if !self.is_face_transform_feature(index) {
263 return None;
264 }
265 let report: serde_json::Value = serde_json::from_str(&self.history_report).ok()?;
266 let prefix = format!("{id}: ");
270 let message = report
271 .get("featureErrors")?
272 .as_array()?
273 .iter()
274 .filter_map(serde_json::Value::as_str)
275 .find(|entry| entry.starts_with(&prefix))
276 .map(|entry| &entry[prefix.len()..])?;
277 let params = self.history.feature_params(index)?;
278 let faces = brep_kernel::reference_names(params.get("faces"));
279 let facts = crate::face_transform_help::RefusalFacts::from_report(&report, id);
280 Some(crate::face_transform_help::explain_refusal(message, facts, &faces))
281 }
282
283 pub fn transform_armed_for(&self, feature_id: &str) -> bool {
285 self.transform_armed() && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
286 }
287
288 pub fn arm_transform(&mut self, feature_id: &str) {
299 let is_acomp = self
300 .history
301 .index_of(feature_id)
302 .and_then(|index| self.history.feature_type(index))
303 .is_some_and(|ty| super::components::is_acomp_feature_type(&ty));
304 if is_acomp {
305 self.component_move_arm(feature_id);
306 return;
307 }
308 self.component_move_reset();
309 self.transform_gizmo.feature_id = Some(feature_id.to_string());
310 self.transform_gizmo.mode = GizmoMode::Transform;
311 self.transform_gizmo.drag = None;
312 self.transform_gizmo.anchor = None;
313 self.transform_gizmo.port = None;
314 self.clear_feature_dimension_overlay();
315 self.sync_transform_gizmo();
316 }
317
318 pub fn disarm_transform(&mut self) {
322 self.component_move_reset();
323 self.transform_gizmo.feature_id = None;
324 self.transform_gizmo.mode = GizmoMode::None;
325 self.transform_gizmo.drag = None;
326 self.transform_gizmo.anchor = None;
327 self.transform_gizmo.port = None;
328 let _ = self.widgets.set_transform_json("null");
329 self.clear_feature_dimension_overlay();
330 self.dirty = true;
331 }
332
333 fn armed_pose(&self) -> Option<TransformPose> {
336 if let Some(address) = self.transform_gizmo.port.clone() {
339 return self.port_point_pose(&address);
340 }
341 let id = self.transform_gizmo.feature_id.as_deref()?;
342 if let Some(row) = self.pmi_report.as_ref().and_then(|r| r.annotation(id)) {
343 if self.pmi_active_view.as_deref()
344 != self
345 .pmi_state()
346 .find_annotation(id)
347 .map(|(v, _)| v.id.as_str())
348 {
349 return None;
350 }
351 if let brep_kernel::PmiGeometry::Explode {
352 center,
353 translate,
354 rotate_deg,
355 scale,
356 ..
357 } = &row.geometry
358 {
359 return Some(TransformPose {
360 position: std::array::from_fn(|i| center[i] + translate[i]),
361 rotation_deg: *rotate_deg,
362 scale: *scale,
363 });
364 }
365 }
366 let index = self.history.index_of(id)?;
367 if let Some(anchor) = self.transform_gizmo.anchor {
368 let (position, rotation_deg) = self.spline_anchor_pose(index, anchor)?;
371 return Some(TransformPose {
372 position,
373 rotation_deg,
374 scale: [1.0, 1.0, 1.0],
375 });
376 }
377 let params = self.history.feature_params(index)?;
378 let transform = params.get("transform");
379 let env =
382 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).ok();
383 let env = env.as_ref();
384 let pivot = if self.is_xform_feature(index) {
385 self.xform_pivot(index, ¶ms)
386 } else if self.is_face_transform_feature(index) {
387 self.face_transform_pivot(index, ¶ms, env)?
389 } else {
390 [0.0; 3]
391 };
392 let position = read_pose_vec3(env, transform, "position", [0.0; 3]);
393 Some(TransformPose {
394 position: std::array::from_fn(|axis| position[axis] + pivot[axis]),
395 rotation_deg: read_pose_vec3(env, transform, "rotationEuler", [0.0, 0.0, 0.0]),
396 scale: read_pose_vec3(env, transform, "scale", [1.0, 1.0, 1.0]),
397 })
398 }
399
400 fn is_xform_feature(&self, index: usize) -> bool {
401 self.history.feature_type(index).is_some_and(|ty| {
402 ty.eq_ignore_ascii_case("XFORM") || ty.eq_ignore_ascii_case("TRANSFORM")
403 })
404 }
405
406 pub fn is_face_transform_feature(&self, index: usize) -> bool {
409 self.history
410 .feature_type(index)
411 .is_some_and(|ty| is_face_transform_type(&ty))
412 }
413
414 fn face_transform_pivot(
419 &self,
420 index: usize,
421 params: &serde_json::Value,
422 env: Option<&brep_kernel::Env>,
423 ) -> Option<[f64; 3]> {
424 match params.get("pivot") {
425 Some(pivot) if !pivot.is_null() => Some(read_pose_vec3(env, Some(params), "pivot", [0.0; 3])),
426 _ => self.face_transform_default_pivot(index, params),
427 }
428 }
429
430 pub fn face_transform_has_pivot(&self, feature_id: &str) -> bool {
434 let Some(index) = self.history.index_of(feature_id) else {
435 return false;
436 };
437 let Some(params) = self.history.feature_params(index) else {
438 return false;
439 };
440 self.is_face_transform_feature(index) && self.face_transform_pivot(index, ¶ms, None).is_some()
441 }
442
443 fn face_transform_default_pivot(
449 &self,
450 index: usize,
451 params: &serde_json::Value,
452 ) -> Option<[f64; 3]> {
453 let names = brep_kernel::reference_names(params.get("faces"));
454 if names.is_empty() {
455 return None;
456 }
457 let request: HistoryRequest =
458 serde_json::from_value(self.history.request_before(index)?).ok()?;
459 let _trace = crate::run_trace::span("face_transform_pivot");
460 let result = self.execute_plugin_history(&request);
461 brep_kernel::face_transform_pivot(&result.results, &names)
462 }
463
464 pub(crate) fn stamp_face_transform_pivot(&self, index: usize, params: &mut serde_json::Value) {
474 if !self.is_face_transform_feature(index) {
475 return;
476 }
477 let before = self.history.feature_params(index).unwrap_or(serde_json::Value::Null);
478 let faces_changed = brep_kernel::reference_names(before.get("faces"))
479 != brep_kernel::reference_names(params.get("faces"));
480 let pivot_set_here = before.get("pivot") != params.get("pivot");
481 let unset = params.get("pivot").is_none_or(serde_json::Value::is_null);
482 if !(unset || (faces_changed && !pivot_set_here)) {
483 return;
484 }
485 let pivot = self.face_transform_default_pivot(index, params);
486 if let Some(object) = params.as_object_mut() {
487 object.insert(
488 "pivot".into(),
489 pivot.map_or(serde_json::Value::Null, |pivot| serde_json::json!(pivot)),
490 );
491 }
492 }
493
494 fn xform_pivot(&self, index: usize, params: &serde_json::Value) -> [f64; 3] {
498 if !params["pivot"]
499 .as_str()
500 .is_some_and(|p| p.eq_ignore_ascii_case("BBOX_CENTER"))
501 {
502 return [0.0; 3];
503 }
504 let Ok(request) = serde_json::from_value::<HistoryRequest>(self.run_request_value())
507 else {
508 return [0.0; 3];
509 };
510 let _trace = crate::run_trace::span("xform_pivot");
511 let result = self.execute_plugin_history(&request);
512 let mut handles = HashMap::new();
513 for feature in result.results.iter().take(index) {
514 for name in &feature.removed {
515 handles.remove(name);
516 }
517 for solid in &feature.added {
518 handles.insert(solid.name.clone(), solid.handle);
519 }
520 }
521 let Some(refs) = params["solids"].as_array() else {
522 return [0.0; 3];
523 };
524 for reference in refs {
525 let Some(name) = reference.as_str().or_else(|| reference["name"].as_str()) else {
526 continue;
527 };
528 let Some(handle) = handles.get(name.trim()) else {
529 continue;
530 };
531 if let Ok(center) = brep_kernel::transform_pivot_native(*handle) {
532 return center;
533 }
534 }
535 [0.0; 3]
536 }
537
538 pub fn sync_transform_gizmo(&mut self) {
545 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
548 return;
549 }
550 let Some(pose) = self.armed_pose() else {
551 self.disarm_transform();
552 return;
553 };
554 let _ = self
555 .widgets
556 .set_transform_json(&transform_frame_json(&pose));
557 self.dirty = true;
558 }
559
560 pub fn feature_has_transform(&self, feature_id: &str) -> bool {
564 self.history
565 .index_of(feature_id)
566 .and_then(|i| self.history.feature_type(i))
567 .and_then(|ty| self.feature_schema(&ty))
568 .is_some_and(|schema| schema["inputParamsSchema"]["transform"]["type"] == "transform")
569 }
570
571 pub fn transform_gizmo_anchor(&self) -> Option<(f64, f64)> {
576 let pose = self.armed_pose()?;
577 let (sx, sy, _) = self.camera.project(pose.position);
578 self.camera.projectable(pose.position).then_some((sx, sy))
579 }
580
581 pub fn transform_axis_labels_json(&self) -> String {
586 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
587 return "[]".to_string();
588 }
589 let Some(pose) = self.armed_pose() else {
590 return "[]".to_string();
591 };
592 let euler = [
593 pose.rotation_deg[0].to_radians(),
594 pose.rotation_deg[1].to_radians(),
595 pose.rotation_deg[2].to_radians(),
596 ];
597 let axes = [
598 normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler)),
599 normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler)),
600 normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler)),
601 ];
602 let gap_px = 12.0_f64;
604 let dist =
605 (brep_gizmos::transform::PX_AXIS_LEN as f64 + gap_px) * self.camera.world_per_pixel();
606 let labels: [(&str, [f32; 3]); 3] = [
607 ("XC", [0.92, 0.26, 0.28]), ("YC", [0.30, 0.78, 0.36]), ("ZC", [0.30, 0.52, 0.98]), ];
611 let out: Vec<serde_json::Value> = (0..3)
612 .map(|i| {
613 let o = pose.position;
614 let a = axes[i];
615 serde_json::json!({
616 "text": labels[i].0,
617 "rgb": labels[i].1,
618 "world": [o[0] + a[0] * dist, o[1] + a[1] * dist, o[2] + a[2] * dist],
619 })
620 })
621 .collect();
622 serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
623 }
624
625 pub fn transform_handle_points(&self) -> Vec<(&'static str, (f64, f64))> {
634 use brep_gizmos::hit_region::HitShape;
635 use brep_gizmos::transform as gizmo;
636 let cam = gizmo_camera(&self.camera);
637 self.widgets
638 .transform_hit_regions(&cam)
639 .into_iter()
640 .filter_map(|(handle, shape)| {
641 let key = match handle {
642 gizmo::HANDLE_AXIS_X => "axis:x",
643 gizmo::HANDLE_AXIS_Y => "axis:y",
644 gizmo::HANDLE_AXIS_Z => "axis:z",
645 gizmo::HANDLE_RING_X => "ring:x",
646 gizmo::HANDLE_RING_Y => "ring:y",
647 gizmo::HANDLE_RING_Z => "ring:z",
648 _ => return None,
649 };
650 let [x, y] = match shape {
651 HitShape::Capsule { a, b, .. } => {
652 [a[0] + 0.75 * (b[0] - a[0]), a[1] + 0.75 * (b[1] - a[1])]
653 }
654 HitShape::Circle { c, .. } => c,
655 };
656 let point = (f64::from(x), f64::from(y));
657 (self.transform_pick(point.0, point.1) == handle).then_some((key, point))
658 })
659 .collect()
660 }
661
662 pub fn transform_hit_areas_json(&self) -> String {
666 let cam = gizmo_camera(&self.camera);
667 let regions = self.widgets.transform_hit_regions(&cam);
668 super::camera_widgets::hit_shapes_json(regions.iter().map(|(_, shape)| shape))
669 }
670
671 pub fn transform_press(&mut self, x: f64, y: f64) -> bool {
676 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
679 return false;
680 }
681 let handle = self.transform_pick(x, y);
682 if handle == 0 {
683 return false;
684 }
685 let Some(pose) = self.armed_pose() else {
686 return false;
687 };
688 self.widgets.set_transform_active(handle);
689 self.transform_gizmo.drag = Some(TransformDrag {
690 handle,
691 sx: x as f32,
692 sy: y as f32,
693 start: pose,
694 last: None,
695 });
696 self.dirty = true;
697 true
698 }
699
700 pub fn transform_dragging(&self) -> bool {
702 self.transform_gizmo.drag.is_some()
703 }
704
705 pub fn transform_drag_to(&mut self, cx: f64, cy: f64) {
712 let Some(drag) = self.transform_gizmo.drag else {
713 return;
714 };
715 let Some(delta) = self.resolve_transform_delta(&drag, cx, cy) else {
716 return;
717 };
718 let pose = apply_transform_delta(&drag.start, &delta);
719 if let Some(live) = self.transform_gizmo.drag.as_mut() {
723 live.last = Some(delta);
724 }
725 self.write_armed_pose(&pose);
726 self.sync_transform_gizmo();
731 }
732
733 pub fn transform_release(&mut self) {
736 if self.transform_gizmo.drag.take().is_some() {
737 self.widgets.set_transform_active(0);
738 self.history.break_coalescing();
749 self.sync_transform_gizmo();
750 }
751 }
752
753 fn resolve_transform_delta(
756 &self,
757 drag: &TransformDrag,
758 cx: f64,
759 cy: f64,
760 ) -> Option<TransformDelta> {
761 let cam = gizmo_camera(&self.camera);
762 let frame_json = transform_frame_json(&drag.start);
763 let json = self.widgets.transform_drag_json_with_frame(
764 &cam,
765 &frame_json,
766 drag.handle,
767 drag.sx,
768 drag.sy,
769 cx as f32,
770 cy as f32,
771 );
772 parse_drag_delta(&json)
773 }
774
775 fn write_armed_pose(&mut self, pose: &TransformPose) {
778 if let Some(address) = self.transform_gizmo.port.clone() {
779 self.write_port_point_pose(&address, pose);
780 return;
781 }
782 let Some(id) = self.transform_gizmo.feature_id.clone() else {
783 return;
784 };
785 let pmi = self.pmi_state();
786 if let Some((_, annotation)) = pmi.find_annotation(&id) {
787 if annotation.kind == "explode" {
788 let center = self
789 .pmi_report
790 .as_ref()
791 .and_then(|r| r.annotation(&id))
792 .and_then(|r| match &r.geometry {
793 brep_kernel::PmiGeometry::Explode { center, .. } => Some(*center),
794 _ => None,
795 });
796 if let Some(center) = center {
797 let mut params = annotation.params.clone();
798 params["transform"] = serde_json::json!({ "position": std::array::from_fn::<_, 3, _>(|i| pose.position[i] - center[i]), "rotationEuler": pose.rotation_deg, "scale": pose.scale });
799 let _ = self.pmi_update_annotation(&id, ¶ms.to_string());
800 }
801 return;
802 }
803 }
804 let Some(index) = self.history.index_of(&id) else {
805 return;
806 };
807 if let Some(anchor) = self.transform_gizmo.anchor {
808 self.write_spline_anchor_pose(&id, anchor, pose.position, pose.rotation_deg);
809 return;
810 }
811 let mut params = self
812 .history
813 .feature_params(index)
814 .unwrap_or_else(|| serde_json::json!({}));
815 let pivot = if self.is_xform_feature(index) {
816 self.xform_pivot(index, ¶ms)
817 } else if self.is_face_transform_feature(index) {
818 let env =
819 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator())
820 .ok();
821 let Some(pivot) = self.face_transform_pivot(index, ¶ms, env.as_ref()) else {
822 return;
823 };
824 pivot
825 } else {
826 [0.0; 3]
827 };
828 let position: [f64; 3] = std::array::from_fn(|axis| pose.position[axis] - pivot[axis]);
829 if !params
831 .get("transform")
832 .map(|t| t.is_object())
833 .unwrap_or(false)
834 {
835 if let Some(object) = params.as_object_mut() {
836 object.insert("transform".into(), serde_json::json!({}));
837 }
838 }
839 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
840 transform.insert("position".into(), serde_json::json!(position));
841 transform.insert("rotationEuler".into(), serde_json::json!(pose.rotation_deg));
842 }
843 let _ = self.update_feature_params(&id, ¶ms.to_string());
844 }
845}
846
847fn dominant_axis_label(v: &[f64; 3]) -> &'static str {
857 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
858 if length < 1e-12 {
859 return "\u{0394}";
860 }
861 const NAMES: [&str; 3] = ["X", "Y", "Z"];
862 for axis in 0..3 {
863 if v[axis].abs() >= length * 0.999 {
864 return NAMES[axis];
865 }
866 }
867 "\u{0394}"
868}
869
870pub fn is_face_transform_type(feature_type: &str) -> bool {
873 matches!(
874 feature_type.trim().to_ascii_uppercase().as_str(),
875 "TF" | "TRANSFORM FACE" | "TRANSFORMFACE"
876 )
877}
878
879fn apply_transform_delta(start: &TransformPose, delta: &TransformDelta) -> TransformPose {
881 match delta {
882 TransformDelta::Translate(world) => TransformPose {
883 position: [
884 start.position[0] + world[0],
885 start.position[1] + world[1],
886 start.position[2] + world[2],
887 ],
888 ..*start
889 },
890 TransformDelta::Rotate { axis, radians } => {
891 let q0 = quat_from_euler_xyz_deg(start.rotation_deg);
892 let dq = quat_from_axis_angle(*axis, *radians);
893 let nq = quat_mul(dq, q0);
894 TransformPose {
895 rotation_deg: euler_xyz_deg_from_quat(nq),
896 ..*start
897 }
898 }
899 }
900}
901
902fn read_pose_vec3(
905 env: Option<&brep_kernel::Env>,
906 transform: Option<&serde_json::Value>,
907 key: &str,
908 default: [f64; 3],
909) -> [f64; 3] {
910 let array = transform
911 .and_then(|t| t.get(key))
912 .and_then(|v| v.as_array());
913 let mut out = default;
914 if let Some(array) = array {
915 for (index, slot) in out.iter_mut().enumerate() {
916 match array.get(index) {
917 Some(serde_json::Value::Number(number)) => {
918 if let Some(number) = number.as_f64() {
919 *slot = number;
920 }
921 }
922 Some(serde_json::Value::String(source)) => {
928 if let Some(number) = env
929 .and_then(|env| env.eval(source).ok())
930 .filter(|number| number.is_finite())
931 {
932 *slot = number;
933 }
934 }
935 _ => {}
936 }
937 }
938 }
939 out
940}
941
942fn transform_frame_json(pose: &TransformPose) -> String {
946 let euler = [
947 pose.rotation_deg[0].to_radians(),
948 pose.rotation_deg[1].to_radians(),
949 pose.rotation_deg[2].to_radians(),
950 ];
951 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
952 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
953 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
954 serde_json::json!({
955 "origin": pose.position,
956 "x": x,
957 "y": y,
958 "z": z,
959 "showCenter": true,
960 })
961 .to_string()
962}
963
964pub(super) fn normalize3(v: [f64; 3]) -> [f64; 3] {
965 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
966 if length < 1e-12 {
967 [0.0, 0.0, 1.0]
968 } else {
969 [v[0] / length, v[1] / length, v[2] / length]
970 }
971}
972
973pub(crate) fn rotate_euler_xyz_f64(v: [f64; 3], euler: [f64; 3]) -> [f64; 3] {
977 let (c1, s1) = (euler[0].cos(), euler[0].sin());
978 let (c2, s2) = (euler[1].cos(), euler[1].sin());
979 let (c3, s3) = (euler[2].cos(), euler[2].sin());
980 let m00 = c2 * c3;
981 let m01 = -c2 * s3;
982 let m02 = s2;
983 let m10 = c1 * s3 + c3 * s1 * s2;
984 let m11 = c1 * c3 - s1 * s2 * s3;
985 let m12 = -c2 * s1;
986 let m20 = s1 * s3 - c1 * c3 * s2;
987 let m21 = c3 * s1 + c1 * s2 * s3;
988 let m22 = c1 * c2;
989 [
990 m00 * v[0] + m01 * v[1] + m02 * v[2],
991 m10 * v[0] + m11 * v[1] + m12 * v[2],
992 m20 * v[0] + m21 * v[1] + m22 * v[2],
993 ]
994}
995
996pub(super) type Quat = [f64; 4]; pub(super) fn quat_from_axis_angle(axis: [f64; 3], angle: f64) -> Quat {
1001 let n = normalize3(axis);
1002 let half = angle * 0.5;
1003 let s = half.sin();
1004 [n[0] * s, n[1] * s, n[2] * s, half.cos()]
1005}
1006
1007pub(super) fn quat_from_euler_xyz_deg(deg: [f64; 3]) -> Quat {
1009 let (c1, s1) = (
1010 (deg[0].to_radians() * 0.5).cos(),
1011 (deg[0].to_radians() * 0.5).sin(),
1012 );
1013 let (c2, s2) = (
1014 (deg[1].to_radians() * 0.5).cos(),
1015 (deg[1].to_radians() * 0.5).sin(),
1016 );
1017 let (c3, s3) = (
1018 (deg[2].to_radians() * 0.5).cos(),
1019 (deg[2].to_radians() * 0.5).sin(),
1020 );
1021 [
1022 s1 * c2 * c3 + c1 * s2 * s3,
1023 c1 * s2 * c3 - s1 * c2 * s3,
1024 c1 * c2 * s3 + s1 * s2 * c3,
1025 c1 * c2 * c3 - s1 * s2 * s3,
1026 ]
1027}
1028
1029pub(super) fn quat_mul(a: Quat, b: Quat) -> Quat {
1031 [
1032 a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
1033 a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
1034 a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
1035 a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
1036 ]
1037}
1038
1039pub(super) fn euler_xyz_deg_from_quat(q: Quat) -> [f64; 3] {
1043 let [x, y, z, w] = q;
1044 let (x2, y2, z2) = (x + x, y + y, z + z);
1045 let (xx, xy, xz) = (x * x2, x * y2, x * z2);
1046 let (yy, yz, zz) = (y * y2, y * z2, z * z2);
1047 let (wx, wy, wz) = (w * x2, w * y2, w * z2);
1048 let m11 = 1.0 - (yy + zz);
1050 let m12 = xy - wz;
1051 let m13 = xz + wy;
1052 let m22 = 1.0 - (xx + zz);
1053 let m23 = yz - wx;
1054 let m32 = yz + wx;
1055 let m33 = 1.0 - (xx + yy);
1056 let ey = m13.clamp(-1.0, 1.0).asin();
1057 let (ex, ez) = if m13.abs() < 0.9999999 {
1058 ((-m23).atan2(m33), (-m12).atan2(m11))
1059 } else {
1060 (m32.atan2(m22), 0.0)
1061 };
1062 [ex.to_degrees(), ey.to_degrees(), ez.to_degrees()]
1063}
1064
1065impl EngineState {
1086 pub fn transform_armed_port_point(&self) -> Option<&str> {
1088 self.transform_armed()
1089 .then(|| self.transform_gizmo.port.as_deref())
1090 .flatten()
1091 }
1092
1093 pub fn arm_transform_for_port_point(&mut self, address: &str) -> bool {
1096 if let Some(reason) = self.port_point_gizmo_refusal(address) {
1097 self.push_notice(format!("no handles on {address}: {reason}"));
1098 return false;
1099 }
1100 self.component_move_reset();
1101 self.transform_gizmo.feature_id = None;
1102 self.transform_gizmo.anchor = None;
1103 self.transform_gizmo.port = Some(address.to_string());
1104 self.transform_gizmo.mode = GizmoMode::Transform;
1105 self.transform_gizmo.drag = None;
1106 self.clear_feature_dimension_overlay();
1107 self.sync_transform_gizmo();
1108 true
1109 }
1110
1111 pub fn port_point_gizmo_refusal(&self, address: &str) -> Option<String> {
1130 let point = self.declared_point_value(address)?;
1131 if let Some(target) = point
1132 .get("mapsTo")
1133 .and_then(serde_json::Value::as_str)
1134 .map(str::trim)
1135 .filter(|target| !target.is_empty())
1136 {
1137 return Some(format!(
1138 "it maps to '{target}', and that point's placement is this one's"
1139 ));
1140 }
1141 let transform = point.get("transform")?;
1142 for key in ["position", "rotationEuler"] {
1143 let Some(components) = transform.get(key).and_then(serde_json::Value::as_array) else {
1144 continue;
1145 };
1146 for component in components {
1147 if let Some(expression) = component.as_str() {
1148 return Some(format!(
1149 "'{expression}' places it, and a drag writes numbers"
1150 ));
1151 }
1152 }
1153 }
1154 None
1155 }
1156
1157 fn port_point_seat(&self, address: &str) -> brep_kernel::Frame {
1161 self.port_point_row(address)
1162 .map(|row| row.seat.to_frame())
1163 .unwrap_or_else(brep_kernel::world_seat)
1164 }
1165
1166 pub(super) fn port_point_pose(&self, address: &str) -> Option<TransformPose> {
1169 let point = self.declared_point_value(address)?;
1170 let transform = point.get("transform");
1171 let local_position = read_pose_vec3(None, transform, "position", [0.0; 3]);
1172 let local_rotation = read_pose_vec3(None, transform, "rotationEuler", [0.0; 3]);
1173 let seat = self.port_point_seat(address);
1174 let world_axes = super::spline_edit::axes_from_euler_deg(local_rotation)
1175 .map(|axis| seat_axis(&seat, axis));
1176 Some(TransformPose {
1177 position: array3(brep_kernel::seat_point(&seat, vec3(local_position))),
1178 rotation_deg: super::spline_edit::euler_xyz_deg_from_axes(
1179 world_axes[0],
1180 world_axes[1],
1181 world_axes[2],
1182 ),
1183 scale: [1.0, 1.0, 1.0],
1184 })
1185 }
1186
1187 pub(super) fn write_port_point_pose(&mut self, address: &str, pose: &TransformPose) {
1190 let seat = self.port_point_seat(address);
1191 let position = array3(brep_kernel::unseat_point(&seat, vec3(pose.position)));
1192 let local_axes = super::spline_edit::axes_from_euler_deg(pose.rotation_deg)
1193 .map(|axis| unseat_axis(&seat, axis));
1194 let rotation = super::spline_edit::euler_xyz_deg_from_axes(
1195 local_axes[0],
1196 local_axes[1],
1197 local_axes[2],
1198 );
1199 let key = format!("port-point-pose:{address}");
1200 self.edit_port_point(address, Some(&key), |point| {
1201 point.insert(
1202 "transform".into(),
1203 serde_json::json!({ "position": position, "rotationEuler": rotation }),
1204 );
1205 });
1206 }
1207}
1208
1209fn vec3(a: [f64; 3]) -> brep_kernel::Vec3 {
1210 brep_kernel::Vec3::new(a[0], a[1], a[2])
1211}
1212
1213fn array3(v: brep_kernel::Vec3) -> [f64; 3] {
1214 [v.x, v.y, v.z]
1215}
1216
1217fn seat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
1219 array3(brep_kernel::seat_vector(seat, vec3(axis)))
1220}
1221
1222fn unseat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
1224 array3(brep_kernel::unseat_vector(seat, vec3(axis)))
1225}
1226