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 pub(super) cage: Option<super::cage_edit::CageArm>,
48}
49
50#[derive(Clone, Copy)]
55pub(super) struct TransformDrag {
56 handle: u32,
57 sx: f32,
58 sy: f32,
59 start: TransformPose,
60 last: Option<TransformDelta>,
67}
68
69#[derive(Clone, Copy, PartialEq, Debug)]
73pub(super) struct TransformPose {
74 pub(super) position: [f64; 3],
75 pub(super) rotation_deg: [f64; 3],
76 pub(super) scale: [f64; 3],
77}
78
79#[derive(Clone, Copy, PartialEq, Debug)]
81pub(super) enum TransformDelta {
82 Translate([f64; 3]),
84 Rotate { axis: [f64; 3], radians: f64 },
86}
87
88pub(super) fn parse_drag_delta(json: &str) -> Option<TransformDelta> {
90 let value: serde_json::Value = serde_json::from_str(json).ok()?;
91 match value.get("kind").and_then(|k| k.as_str()) {
92 Some("translate") => Some(TransformDelta::Translate(
93 crate::json_support::vec3_or(value.get("world"), [0.0; 3]),
94 )),
95 Some("rotate") => Some(TransformDelta::Rotate {
96 axis: crate::json_support::vec3_or(value.get("axisWorld"), [0.0; 3]),
97 radians: value.get("radians").and_then(|n| n.as_f64()).unwrap_or(0.0),
98 }),
99 _ => None,
100 }
101}
102
103impl EngineState {
104 pub fn transform_armed(&self) -> bool {
108 matches!(self.transform_gizmo.mode, GizmoMode::Transform)
109 }
110
111 pub fn transform_armed_feature(&self) -> String {
113 if self.transform_armed() {
114 self.transform_gizmo.feature_id.clone().unwrap_or_default()
115 } else {
116 String::new()
117 }
118 }
119
120 pub fn gizmo_state_json(&self) -> String {
126 let mode = match self.transform_gizmo.mode {
127 GizmoMode::None => "none",
128 GizmoMode::Transform => "transform",
129 GizmoMode::Dimension => "dimension",
130 };
131 serde_json::json!({
132 "mode": mode,
133 "armed": self.transform_armed(),
134 "feature": self.transform_gizmo.feature_id,
135 "anchor": self.transform_gizmo.anchor,
136 "portPoint": self.transform_gizmo.port,
137 "cage": self.transform_gizmo.cage.as_ref().map(|cage| cage.selected.clone()),
138 "origin": self.widgets.transform_origin(),
139 "pivot": self.armed_face_transform_pivot(),
145 })
146 .to_string()
147 }
148
149 pub fn armed_face_transform_pivot(&self) -> Option<[f64; 3]> {
153 let id = self.transform_gizmo.feature_id.as_deref()?;
154 let index = self.history.index_of(id)?;
155 if !self.is_face_transform_feature(index) {
156 return None;
157 }
158 let params = self.history.feature_params(index)?;
159 let env = brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).ok();
160 self.face_transform_pivot(index, ¶ms, env.as_ref())
161 }
162
163 pub fn transform_readout_json(&self) -> String {
188 let quiet = serde_json::json!({ "active": false }).to_string();
189 if !self.transform_armed() {
190 return quiet;
191 }
192 let Some(origin) = self.widgets.transform_origin() else {
193 return quiet;
194 };
195
196 let refusal = self.armed_face_transform_refusal();
199 let drag = self.transform_gizmo.drag.as_ref();
200 let delta = drag.and_then(|d| d.last);
201
202 if delta.is_none() && refusal.is_none() {
205 return quiet;
206 }
207
208 let (kind, label, value, unit, text) = match delta {
212 Some(TransformDelta::Translate(world)) => {
213 let distance = (world[0] * world[0] + world[1] * world[1] + world[2] * world[2]).sqrt();
214 let axis = dominant_axis_label(&world);
215 let shown = crate::formatting::compact_decimal(distance, 4);
216 (
217 "translate",
218 axis.to_string(),
219 distance,
220 "mm",
221 format!("{axis} {shown} mm"),
222 )
223 }
224 Some(TransformDelta::Rotate { axis, radians }) => {
225 let degrees = radians.to_degrees();
226 let label = dominant_axis_label(&axis);
227 let shown = crate::formatting::compact_decimal(degrees, 4);
228 (
229 "rotate",
230 label.to_string(),
231 degrees,
232 "deg",
233 format!("{label} {shown}\u{00b0}"),
234 )
235 }
236 None => ("none", String::new(), 0.0, "", String::new()),
239 };
240
241 let mut out = serde_json::json!({
242 "active": true,
243 "kind": kind,
244 "axis": label,
245 "value": value,
246 "unit": unit,
247 "text": text,
248 "origin": origin,
249 "dragging": drag.is_some(),
250 "refused": refusal.is_some(),
251 });
252 if let Some(refusal) = refusal {
253 out["motion"] = serde_json::json!(refusal.motion);
254 out["reason"] = serde_json::json!(refusal.reason);
255 out["hint"] = serde_json::json!(refusal.hint);
256 out["kernel"] = serde_json::json!(refusal.kernel);
257 }
258 out.to_string()
259 }
260
261 fn armed_face_transform_refusal(&self) -> Option<crate::face_transform_help::FaceTransformRefusal> {
266 let id = self.transform_gizmo.feature_id.as_deref()?;
267 let index = self.history.index_of(id)?;
268 if !self.is_face_transform_feature(index) {
269 return None;
270 }
271 let report: serde_json::Value = serde_json::from_str(&self.history_report).ok()?;
272 let prefix = format!("{id}: ");
276 let message = report
277 .get("featureErrors")?
278 .as_array()?
279 .iter()
280 .filter_map(serde_json::Value::as_str)
281 .find(|entry| entry.starts_with(&prefix))
282 .map(|entry| &entry[prefix.len()..])?;
283 let params = self.history.feature_params(index)?;
284 let faces = brep_kernel::reference_names(params.get("faces"));
285 let facts = crate::face_transform_help::RefusalFacts::from_report(&report, id);
286 Some(crate::face_transform_help::explain_refusal(message, facts, &faces))
287 }
288
289 pub fn transform_armed_for(&self, feature_id: &str) -> bool {
291 self.transform_armed() && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
292 }
293
294 pub fn arm_transform(&mut self, feature_id: &str) {
305 let is_acomp = self
306 .history
307 .index_of(feature_id)
308 .and_then(|index| self.history.feature_type(index))
309 .is_some_and(|ty| super::components::is_acomp_feature_type(&ty));
310 if is_acomp {
311 self.component_move_arm(feature_id);
312 return;
313 }
314 self.component_move_reset();
315 self.transform_gizmo.feature_id = Some(feature_id.to_string());
316 self.transform_gizmo.mode = GizmoMode::Transform;
317 self.transform_gizmo.drag = None;
318 self.transform_gizmo.anchor = None;
319 self.transform_gizmo.port = None;
320 self.transform_gizmo.cage = None;
321 self.clear_feature_dimension_overlay();
322 self.sync_transform_gizmo();
323 }
324
325 pub fn disarm_transform(&mut self) {
329 self.component_move_reset();
330 self.transform_gizmo.feature_id = None;
331 self.transform_gizmo.mode = GizmoMode::None;
332 self.transform_gizmo.drag = None;
333 self.transform_gizmo.anchor = None;
334 self.transform_gizmo.port = None;
335 self.transform_gizmo.cage = None;
336 let _ = self.widgets.set_transform_json("null");
337 self.clear_feature_dimension_overlay();
338 self.dirty = true;
339 }
340
341 fn armed_pose(&self) -> Option<TransformPose> {
344 if let Some(address) = self.transform_gizmo.port.clone() {
347 return self.port_point_pose(&address);
348 }
349 let id = self.transform_gizmo.feature_id.as_deref()?;
350 if let Some(row) = self.pmi_report.as_ref().and_then(|r| r.annotation(id)) {
351 if self.pmi_active_view.as_deref()
352 != self
353 .pmi_state()
354 .find_annotation(id)
355 .map(|(v, _)| v.id.as_str())
356 {
357 return None;
358 }
359 if let brep_kernel::PmiGeometry::Explode {
360 center,
361 translate,
362 rotate_deg,
363 scale,
364 ..
365 } = &row.geometry
366 {
367 return Some(TransformPose {
368 position: std::array::from_fn(|i| center[i] + translate[i]),
369 rotation_deg: *rotate_deg,
370 scale: *scale,
371 });
372 }
373 }
374 let index = self.history.index_of(id)?;
375 if self.transform_gizmo.cage.is_some() {
376 return self.cage_pose();
379 }
380 if let Some(anchor) = self.transform_gizmo.anchor {
381 let (position, rotation_deg) = self.spline_anchor_pose(index, anchor)?;
384 return Some(TransformPose {
385 position,
386 rotation_deg,
387 scale: [1.0, 1.0, 1.0],
388 });
389 }
390 let params = self.history.feature_params(index)?;
391 let transform = params.get("transform");
392 let env =
395 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).ok();
396 let env = env.as_ref();
397 let pivot = if self.is_xform_feature(index) {
398 self.xform_pivot(index, ¶ms)
399 } else if self.is_face_transform_feature(index) {
400 self.face_transform_pivot(index, ¶ms, env)?
402 } else {
403 [0.0; 3]
404 };
405 let position = read_pose_vec3(env, transform, "position", [0.0; 3]);
406 Some(TransformPose {
407 position: std::array::from_fn(|axis| position[axis] + pivot[axis]),
408 rotation_deg: read_pose_vec3(env, transform, "rotationEuler", [0.0, 0.0, 0.0]),
409 scale: read_pose_vec3(env, transform, "scale", [1.0, 1.0, 1.0]),
410 })
411 }
412
413 fn is_xform_feature(&self, index: usize) -> bool {
414 self.history.feature_type(index).is_some_and(|ty| {
415 ty.eq_ignore_ascii_case("XFORM") || ty.eq_ignore_ascii_case("TRANSFORM")
416 })
417 }
418
419 pub fn is_face_transform_feature(&self, index: usize) -> bool {
422 self.history
423 .feature_type(index)
424 .is_some_and(|ty| is_face_transform_type(&ty))
425 }
426
427 fn face_transform_pivot(
432 &self,
433 index: usize,
434 params: &serde_json::Value,
435 env: Option<&brep_kernel::Env>,
436 ) -> Option<[f64; 3]> {
437 match params.get("pivot") {
438 Some(pivot) if !pivot.is_null() => Some(read_pose_vec3(env, Some(params), "pivot", [0.0; 3])),
439 _ => self.face_transform_default_pivot(index, params),
440 }
441 }
442
443 pub fn face_transform_has_pivot(&self, feature_id: &str) -> bool {
447 let Some(index) = self.history.index_of(feature_id) else {
448 return false;
449 };
450 let Some(params) = self.history.feature_params(index) else {
451 return false;
452 };
453 self.is_face_transform_feature(index) && self.face_transform_pivot(index, ¶ms, None).is_some()
454 }
455
456 fn face_transform_default_pivot(
462 &self,
463 index: usize,
464 params: &serde_json::Value,
465 ) -> Option<[f64; 3]> {
466 let names = brep_kernel::reference_names(params.get("faces"));
467 if names.is_empty() {
468 return None;
469 }
470 let request: HistoryRequest =
471 serde_json::from_value(self.history.request_before(index)?).ok()?;
472 let _trace = crate::run_trace::span("face_transform_pivot");
473 let result = self.execute_plugin_history(&request);
474 brep_kernel::face_transform_pivot(&result.results, &names)
475 }
476
477 pub(crate) fn stamp_face_transform_pivot(&self, index: usize, params: &mut serde_json::Value) {
487 if !self.is_face_transform_feature(index) {
488 return;
489 }
490 let before = self.history.feature_params(index).unwrap_or(serde_json::Value::Null);
491 let faces_changed = brep_kernel::reference_names(before.get("faces"))
492 != brep_kernel::reference_names(params.get("faces"));
493 let pivot_set_here = before.get("pivot") != params.get("pivot");
494 let unset = params.get("pivot").is_none_or(serde_json::Value::is_null);
495 if !(unset || (faces_changed && !pivot_set_here)) {
496 return;
497 }
498 let pivot = self.face_transform_default_pivot(index, params);
499 if let Some(object) = params.as_object_mut() {
500 object.insert(
501 "pivot".into(),
502 pivot.map_or(serde_json::Value::Null, |pivot| serde_json::json!(pivot)),
503 );
504 }
505 }
506
507 fn xform_pivot(&self, index: usize, params: &serde_json::Value) -> [f64; 3] {
511 if !params["pivot"]
512 .as_str()
513 .is_some_and(|p| p.eq_ignore_ascii_case("BBOX_CENTER"))
514 {
515 return [0.0; 3];
516 }
517 let Ok(request) = serde_json::from_value::<HistoryRequest>(self.run_request_value())
520 else {
521 return [0.0; 3];
522 };
523 let _trace = crate::run_trace::span("xform_pivot");
524 let result = self.execute_plugin_history(&request);
525 let mut handles = HashMap::new();
526 for feature in result.results.iter().take(index) {
527 for name in &feature.removed {
528 handles.remove(name);
529 }
530 for solid in &feature.added {
531 handles.insert(solid.name.clone(), solid.handle);
532 }
533 }
534 let Some(refs) = params["solids"].as_array() else {
535 return [0.0; 3];
536 };
537 for reference in refs {
538 let Some(name) = reference.as_str().or_else(|| reference["name"].as_str()) else {
539 continue;
540 };
541 let Some(handle) = handles.get(name.trim()) else {
542 continue;
543 };
544 if let Ok(center) = brep_kernel::transform_pivot_native(*handle) {
545 return center;
546 }
547 }
548 [0.0; 3]
549 }
550
551 pub fn sync_transform_gizmo(&mut self) {
558 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
561 return;
562 }
563 let Some(pose) = self.armed_pose() else {
564 self.disarm_transform();
565 return;
566 };
567 let show_rings = self.cage_shows_rings().unwrap_or(true);
570 let _ = self
571 .widgets
572 .set_transform_json(&transform_frame_json_with(&pose, show_rings));
573 self.dirty = true;
574 }
575
576 pub fn feature_has_transform(&self, feature_id: &str) -> bool {
580 self.history
581 .index_of(feature_id)
582 .and_then(|i| self.history.feature_type(i))
583 .and_then(|ty| self.feature_schema(&ty))
584 .is_some_and(|schema| schema["inputParamsSchema"]["transform"]["type"] == "transform")
585 }
586
587 pub fn transform_gizmo_anchor(&self) -> Option<(f64, f64)> {
592 let pose = self.armed_pose()?;
593 let (sx, sy, _) = self.camera.project(pose.position);
594 self.camera.projectable(pose.position).then_some((sx, sy))
595 }
596
597 pub fn transform_axis_labels_json(&self) -> String {
602 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
603 return "[]".to_string();
604 }
605 let Some(pose) = self.armed_pose() else {
606 return "[]".to_string();
607 };
608 let euler = [
609 pose.rotation_deg[0].to_radians(),
610 pose.rotation_deg[1].to_radians(),
611 pose.rotation_deg[2].to_radians(),
612 ];
613 let axes = [
614 normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler)),
615 normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler)),
616 normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler)),
617 ];
618 let gap_px = 12.0_f64;
620 let dist =
621 (brep_gizmos::transform::PX_AXIS_LEN as f64 + gap_px) * self.camera.world_per_pixel();
622 let labels: [(&str, [f32; 3]); 3] = [
623 ("XC", [0.92, 0.26, 0.28]), ("YC", [0.30, 0.78, 0.36]), ("ZC", [0.30, 0.52, 0.98]), ];
627 let out: Vec<serde_json::Value> = (0..3)
628 .map(|i| {
629 let o = pose.position;
630 let a = axes[i];
631 serde_json::json!({
632 "text": labels[i].0,
633 "rgb": labels[i].1,
634 "world": [o[0] + a[0] * dist, o[1] + a[1] * dist, o[2] + a[2] * dist],
635 })
636 })
637 .collect();
638 serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
639 }
640
641 pub fn transform_handle_points(&self) -> Vec<(&'static str, (f64, f64))> {
650 use brep_gizmos::hit_region::HitShape;
651 use brep_gizmos::transform as gizmo;
652 let cam = gizmo_camera(&self.camera);
653 self.widgets
654 .transform_hit_regions(&cam)
655 .into_iter()
656 .filter_map(|(handle, shape)| {
657 let key = match handle {
658 gizmo::HANDLE_AXIS_X => "axis:x",
659 gizmo::HANDLE_AXIS_Y => "axis:y",
660 gizmo::HANDLE_AXIS_Z => "axis:z",
661 gizmo::HANDLE_RING_X => "ring:x",
662 gizmo::HANDLE_RING_Y => "ring:y",
663 gizmo::HANDLE_RING_Z => "ring:z",
664 _ => return None,
665 };
666 let [x, y] = match shape {
667 HitShape::Capsule { a, b, .. } => {
668 [a[0] + 0.75 * (b[0] - a[0]), a[1] + 0.75 * (b[1] - a[1])]
669 }
670 HitShape::Circle { c, .. } => c,
671 };
672 let point = (f64::from(x), f64::from(y));
673 (self.transform_pick(point.0, point.1) == handle).then_some((key, point))
674 })
675 .collect()
676 }
677
678 pub fn transform_hit_areas_json(&self) -> String {
682 let cam = gizmo_camera(&self.camera);
683 let regions = self.widgets.transform_hit_regions(&cam);
684 super::camera_widgets::hit_shapes_json(regions.iter().map(|(_, shape)| shape))
685 }
686
687 pub fn transform_press(&mut self, x: f64, y: f64) -> bool {
692 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
695 return false;
696 }
697 let handle = self.transform_pick(x, y);
698 if handle == 0 {
699 return false;
700 }
701 let Some(pose) = self.armed_pose() else {
702 return false;
703 };
704 self.widgets.set_transform_active(handle);
705 self.transform_gizmo.drag = Some(TransformDrag {
706 handle,
707 sx: x as f32,
708 sy: y as f32,
709 start: pose,
710 last: None,
711 });
712 self.cage_grab_begin();
714 self.dirty = true;
715 true
716 }
717
718 pub fn transform_dragging(&self) -> bool {
720 self.transform_gizmo.drag.is_some()
721 }
722
723 pub fn transform_drag_to(&mut self, cx: f64, cy: f64) {
730 let Some(drag) = self.transform_gizmo.drag else {
731 return;
732 };
733 let Some(delta) = self.resolve_transform_delta(&drag, cx, cy) else {
734 return;
735 };
736 let pose = apply_transform_delta(&drag.start, &delta);
737 if let Some(live) = self.transform_gizmo.drag.as_mut() {
741 live.last = Some(delta);
742 }
743 self.write_armed_pose(&pose);
744 self.sync_transform_gizmo();
749 }
750
751 pub fn transform_release(&mut self) {
754 if self.transform_gizmo.drag.take().is_some() {
755 self.widgets.set_transform_active(0);
756 self.history.break_coalescing();
767 self.cage_grab_end();
768 self.sync_transform_gizmo();
769 }
770 }
771
772 fn resolve_transform_delta(
775 &self,
776 drag: &TransformDrag,
777 cx: f64,
778 cy: f64,
779 ) -> Option<TransformDelta> {
780 let cam = gizmo_camera(&self.camera);
781 let frame_json = transform_frame_json(&drag.start);
782 let json = self.widgets.transform_drag_json_with_frame(
783 &cam,
784 &frame_json,
785 drag.handle,
786 drag.sx,
787 drag.sy,
788 cx as f32,
789 cy as f32,
790 );
791 parse_drag_delta(&json)
792 }
793
794 fn write_armed_pose(&mut self, pose: &TransformPose) {
797 if let Some(address) = self.transform_gizmo.port.clone() {
798 self.write_port_point_pose(&address, pose);
799 return;
800 }
801 let Some(id) = self.transform_gizmo.feature_id.clone() else {
802 return;
803 };
804 let pmi = self.pmi_state();
805 if let Some((_, annotation)) = pmi.find_annotation(&id) {
806 if annotation.kind == "explode" {
807 let center = self
808 .pmi_report
809 .as_ref()
810 .and_then(|r| r.annotation(&id))
811 .and_then(|r| match &r.geometry {
812 brep_kernel::PmiGeometry::Explode { center, .. } => Some(*center),
813 _ => None,
814 });
815 if let Some(center) = center {
816 let mut params = annotation.params.clone();
817 params["transform"] = serde_json::json!({ "position": std::array::from_fn::<_, 3, _>(|i| pose.position[i] - center[i]), "rotationEuler": pose.rotation_deg, "scale": pose.scale });
818 let _ = self.pmi_update_annotation(&id, ¶ms.to_string());
819 }
820 return;
821 }
822 }
823 let Some(index) = self.history.index_of(&id) else {
824 return;
825 };
826 if self.transform_gizmo.cage.is_some() {
827 self.write_cage_pose(pose);
828 return;
829 }
830 if let Some(anchor) = self.transform_gizmo.anchor {
831 self.write_spline_anchor_pose(&id, anchor, pose.position, pose.rotation_deg);
832 return;
833 }
834 let mut params = self
835 .history
836 .feature_params(index)
837 .unwrap_or_else(|| serde_json::json!({}));
838 let pivot = if self.is_xform_feature(index) {
839 self.xform_pivot(index, ¶ms)
840 } else if self.is_face_transform_feature(index) {
841 let env =
842 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator())
843 .ok();
844 let Some(pivot) = self.face_transform_pivot(index, ¶ms, env.as_ref()) else {
845 return;
846 };
847 pivot
848 } else {
849 [0.0; 3]
850 };
851 let position: [f64; 3] = std::array::from_fn(|axis| pose.position[axis] - pivot[axis]);
852 if !params
854 .get("transform")
855 .map(|t| t.is_object())
856 .unwrap_or(false)
857 {
858 if let Some(object) = params.as_object_mut() {
859 object.insert("transform".into(), serde_json::json!({}));
860 }
861 }
862 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
863 transform.insert("position".into(), serde_json::json!(position));
864 transform.insert("rotationEuler".into(), serde_json::json!(pose.rotation_deg));
865 }
866 let _ = self.update_feature_params(&id, ¶ms.to_string());
867 }
868}
869
870fn dominant_axis_label(v: &[f64; 3]) -> &'static str {
880 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
881 if length < 1e-12 {
882 return "\u{0394}";
883 }
884 const NAMES: [&str; 3] = ["X", "Y", "Z"];
885 for axis in 0..3 {
886 if v[axis].abs() >= length * 0.999 {
887 return NAMES[axis];
888 }
889 }
890 "\u{0394}"
891}
892
893pub fn is_face_transform_type(feature_type: &str) -> bool {
896 matches!(
897 feature_type.trim().to_ascii_uppercase().as_str(),
898 "TF" | "TRANSFORM FACE" | "TRANSFORMFACE"
899 )
900}
901
902fn apply_transform_delta(start: &TransformPose, delta: &TransformDelta) -> TransformPose {
904 match delta {
905 TransformDelta::Translate(world) => TransformPose {
906 position: [
907 start.position[0] + world[0],
908 start.position[1] + world[1],
909 start.position[2] + world[2],
910 ],
911 ..*start
912 },
913 TransformDelta::Rotate { axis, radians } => {
914 let q0 = quat_from_euler_xyz_deg(start.rotation_deg);
915 let dq = quat_from_axis_angle(*axis, *radians);
916 let nq = quat_mul(dq, q0);
917 TransformPose {
918 rotation_deg: euler_xyz_deg_from_quat(nq),
919 ..*start
920 }
921 }
922 }
923}
924
925fn read_pose_vec3(
928 env: Option<&brep_kernel::Env>,
929 transform: Option<&serde_json::Value>,
930 key: &str,
931 default: [f64; 3],
932) -> [f64; 3] {
933 let array = transform
934 .and_then(|t| t.get(key))
935 .and_then(|v| v.as_array());
936 let mut out = default;
937 if let Some(array) = array {
938 for (index, slot) in out.iter_mut().enumerate() {
939 match array.get(index) {
940 Some(serde_json::Value::Number(number)) => {
941 if let Some(number) = number.as_f64() {
942 *slot = number;
943 }
944 }
945 Some(serde_json::Value::String(source)) => {
951 if let Some(number) = env
952 .and_then(|env| env.eval(source).ok())
953 .filter(|number| number.is_finite())
954 {
955 *slot = number;
956 }
957 }
958 _ => {}
959 }
960 }
961 }
962 out
963}
964
965fn transform_frame_json(pose: &TransformPose) -> String {
969 transform_frame_json_with(pose, true)
970}
971
972fn transform_frame_json_with(pose: &TransformPose, show_rings: bool) -> String {
974 let euler = [
975 pose.rotation_deg[0].to_radians(),
976 pose.rotation_deg[1].to_radians(),
977 pose.rotation_deg[2].to_radians(),
978 ];
979 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
980 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
981 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
982 serde_json::json!({
983 "origin": pose.position,
984 "x": x,
985 "y": y,
986 "z": z,
987 "showCenter": true,
988 "showAxes": true,
989 "showRings": show_rings,
990 })
991 .to_string()
992}
993
994pub(super) fn normalize3(v: [f64; 3]) -> [f64; 3] {
995 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
996 if length < 1e-12 {
997 [0.0, 0.0, 1.0]
998 } else {
999 [v[0] / length, v[1] / length, v[2] / length]
1000 }
1001}
1002
1003pub(crate) fn rotate_euler_xyz_f64(v: [f64; 3], euler: [f64; 3]) -> [f64; 3] {
1007 let (c1, s1) = (euler[0].cos(), euler[0].sin());
1008 let (c2, s2) = (euler[1].cos(), euler[1].sin());
1009 let (c3, s3) = (euler[2].cos(), euler[2].sin());
1010 let m00 = c2 * c3;
1011 let m01 = -c2 * s3;
1012 let m02 = s2;
1013 let m10 = c1 * s3 + c3 * s1 * s2;
1014 let m11 = c1 * c3 - s1 * s2 * s3;
1015 let m12 = -c2 * s1;
1016 let m20 = s1 * s3 - c1 * c3 * s2;
1017 let m21 = c3 * s1 + c1 * s2 * s3;
1018 let m22 = c1 * c2;
1019 [
1020 m00 * v[0] + m01 * v[1] + m02 * v[2],
1021 m10 * v[0] + m11 * v[1] + m12 * v[2],
1022 m20 * v[0] + m21 * v[1] + m22 * v[2],
1023 ]
1024}
1025
1026pub(super) type Quat = [f64; 4]; pub(super) fn quat_from_axis_angle(axis: [f64; 3], angle: f64) -> Quat {
1031 let n = normalize3(axis);
1032 let half = angle * 0.5;
1033 let s = half.sin();
1034 [n[0] * s, n[1] * s, n[2] * s, half.cos()]
1035}
1036
1037pub(super) fn quat_from_euler_xyz_deg(deg: [f64; 3]) -> Quat {
1039 let (c1, s1) = (
1040 (deg[0].to_radians() * 0.5).cos(),
1041 (deg[0].to_radians() * 0.5).sin(),
1042 );
1043 let (c2, s2) = (
1044 (deg[1].to_radians() * 0.5).cos(),
1045 (deg[1].to_radians() * 0.5).sin(),
1046 );
1047 let (c3, s3) = (
1048 (deg[2].to_radians() * 0.5).cos(),
1049 (deg[2].to_radians() * 0.5).sin(),
1050 );
1051 [
1052 s1 * c2 * c3 + c1 * s2 * s3,
1053 c1 * s2 * c3 - s1 * c2 * s3,
1054 c1 * c2 * s3 + s1 * s2 * c3,
1055 c1 * c2 * c3 - s1 * s2 * s3,
1056 ]
1057}
1058
1059pub(super) fn quat_mul(a: Quat, b: Quat) -> Quat {
1061 [
1062 a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
1063 a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
1064 a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
1065 a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
1066 ]
1067}
1068
1069pub(super) fn euler_xyz_deg_from_quat(q: Quat) -> [f64; 3] {
1073 let [x, y, z, w] = q;
1074 let (x2, y2, z2) = (x + x, y + y, z + z);
1075 let (xx, xy, xz) = (x * x2, x * y2, x * z2);
1076 let (yy, yz, zz) = (y * y2, y * z2, z * z2);
1077 let (wx, wy, wz) = (w * x2, w * y2, w * z2);
1078 let m11 = 1.0 - (yy + zz);
1080 let m12 = xy - wz;
1081 let m13 = xz + wy;
1082 let m22 = 1.0 - (xx + zz);
1083 let m23 = yz - wx;
1084 let m32 = yz + wx;
1085 let m33 = 1.0 - (xx + yy);
1086 let ey = m13.clamp(-1.0, 1.0).asin();
1087 let (ex, ez) = if m13.abs() < 0.9999999 {
1088 ((-m23).atan2(m33), (-m12).atan2(m11))
1089 } else {
1090 (m32.atan2(m22), 0.0)
1091 };
1092 [ex.to_degrees(), ey.to_degrees(), ez.to_degrees()]
1093}
1094
1095impl EngineState {
1116 pub fn transform_armed_port_point(&self) -> Option<&str> {
1118 self.transform_armed()
1119 .then(|| self.transform_gizmo.port.as_deref())
1120 .flatten()
1121 }
1122
1123 pub fn arm_transform_for_port_point(&mut self, address: &str) -> bool {
1126 if let Some(reason) = self.port_point_gizmo_refusal(address) {
1127 self.push_notice(format!("no handles on {address}: {reason}"));
1128 return false;
1129 }
1130 self.component_move_reset();
1131 self.transform_gizmo.feature_id = None;
1132 self.transform_gizmo.anchor = None;
1133 self.transform_gizmo.cage = None;
1134 self.transform_gizmo.port = Some(address.to_string());
1135 self.transform_gizmo.mode = GizmoMode::Transform;
1136 self.transform_gizmo.drag = None;
1137 self.clear_feature_dimension_overlay();
1138 self.sync_transform_gizmo();
1139 true
1140 }
1141
1142 pub fn port_point_gizmo_refusal(&self, address: &str) -> Option<String> {
1161 let point = self.declared_point_value(address)?;
1162 if let Some(target) = point
1163 .get("mapsTo")
1164 .and_then(serde_json::Value::as_str)
1165 .map(str::trim)
1166 .filter(|target| !target.is_empty())
1167 {
1168 return Some(format!(
1169 "it maps to '{target}', and that point's placement is this one's"
1170 ));
1171 }
1172 let transform = point.get("transform")?;
1173 for key in ["position", "rotationEuler"] {
1174 let Some(components) = transform.get(key).and_then(serde_json::Value::as_array) else {
1175 continue;
1176 };
1177 for component in components {
1178 if let Some(expression) = component.as_str() {
1179 return Some(format!(
1180 "'{expression}' places it, and a drag writes numbers"
1181 ));
1182 }
1183 }
1184 }
1185 None
1186 }
1187
1188 fn port_point_seat(&self, address: &str) -> brep_kernel::Frame {
1192 self.port_point_row(address)
1193 .map(|row| row.seat.to_frame())
1194 .unwrap_or_else(brep_kernel::world_seat)
1195 }
1196
1197 pub(super) fn port_point_pose(&self, address: &str) -> Option<TransformPose> {
1200 let point = self.declared_point_value(address)?;
1201 let transform = point.get("transform");
1202 let local_position = read_pose_vec3(None, transform, "position", [0.0; 3]);
1203 let local_rotation = read_pose_vec3(None, transform, "rotationEuler", [0.0; 3]);
1204 let seat = self.port_point_seat(address);
1205 let world_axes = super::spline_edit::axes_from_euler_deg(local_rotation)
1206 .map(|axis| seat_axis(&seat, axis));
1207 Some(TransformPose {
1208 position: array3(brep_kernel::seat_point(&seat, vec3(local_position))),
1209 rotation_deg: super::spline_edit::euler_xyz_deg_from_axes(
1210 world_axes[0],
1211 world_axes[1],
1212 world_axes[2],
1213 ),
1214 scale: [1.0, 1.0, 1.0],
1215 })
1216 }
1217
1218 pub(super) fn write_port_point_pose(&mut self, address: &str, pose: &TransformPose) {
1221 let seat = self.port_point_seat(address);
1222 let position = array3(brep_kernel::unseat_point(&seat, vec3(pose.position)));
1223 let local_axes = super::spline_edit::axes_from_euler_deg(pose.rotation_deg)
1224 .map(|axis| unseat_axis(&seat, axis));
1225 let rotation = super::spline_edit::euler_xyz_deg_from_axes(
1226 local_axes[0],
1227 local_axes[1],
1228 local_axes[2],
1229 );
1230 let key = format!("port-point-pose:{address}");
1231 self.edit_port_point(address, Some(&key), |point| {
1232 point.insert(
1233 "transform".into(),
1234 serde_json::json!({ "position": position, "rotationEuler": rotation }),
1235 );
1236 });
1237 }
1238}
1239
1240fn vec3(a: [f64; 3]) -> brep_kernel::Vec3 {
1241 brep_kernel::Vec3::new(a[0], a[1], a[2])
1242}
1243
1244fn array3(v: brep_kernel::Vec3) -> [f64; 3] {
1245 [v.x, v.y, v.z]
1246}
1247
1248fn seat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
1250 array3(brep_kernel::seat_vector(seat, vec3(axis)))
1251}
1252
1253fn unseat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
1255 array3(brep_kernel::unseat_vector(seat, vec3(axis)))
1256}
1257