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 {
211 Some(TransformDelta::Translate(world)) => {
212 let distance = crate::formatting::snap_drag_value(
213 (world[0] * world[0] + world[1] * world[1] + world[2] * world[2]).sqrt(),
214 );
215 let axis = dominant_axis_label(&world);
216 let shown = crate::formatting::compact_decimal(distance, 3);
217 (
218 "translate",
219 axis.to_string(),
220 distance,
221 "mm",
222 format!("{axis} {shown} mm"),
223 )
224 }
225 Some(TransformDelta::Rotate { axis, radians }) => {
226 let degrees = crate::formatting::snap_drag_value(radians.to_degrees());
227 let label = dominant_axis_label(&axis);
228 let shown = crate::formatting::compact_decimal(degrees, 3);
229 (
230 "rotate",
231 label.to_string(),
232 degrees,
233 "deg",
234 format!("{label} {shown}\u{00b0}"),
235 )
236 }
237 None => ("none", String::new(), 0.0, "", String::new()),
240 };
241
242 let mut out = serde_json::json!({
243 "active": true,
244 "kind": kind,
245 "axis": label,
246 "value": value,
247 "unit": unit,
248 "text": text,
249 "origin": origin,
250 "dragging": drag.is_some(),
251 "refused": refusal.is_some(),
252 });
253 if let Some(refusal) = refusal {
254 out["motion"] = serde_json::json!(refusal.motion);
255 out["reason"] = serde_json::json!(refusal.reason);
256 out["hint"] = serde_json::json!(refusal.hint);
257 out["kernel"] = serde_json::json!(refusal.kernel);
258 }
259 out.to_string()
260 }
261
262 fn armed_face_transform_refusal(&self) -> Option<crate::face_transform_help::FaceTransformRefusal> {
267 let id = self.transform_gizmo.feature_id.as_deref()?;
268 let index = self.history.index_of(id)?;
269 if !self.is_face_transform_feature(index) {
270 return None;
271 }
272 let report: serde_json::Value = serde_json::from_str(&self.history_report).ok()?;
273 let prefix = format!("{id}: ");
277 let message = report
278 .get("featureErrors")?
279 .as_array()?
280 .iter()
281 .filter_map(serde_json::Value::as_str)
282 .find(|entry| entry.starts_with(&prefix))
283 .map(|entry| &entry[prefix.len()..])?;
284 let params = self.history.feature_params(index)?;
285 let faces = brep_kernel::reference_names(params.get("faces"));
286 let facts = crate::face_transform_help::RefusalFacts::from_report(&report, id);
287 Some(crate::face_transform_help::explain_refusal(message, facts, &faces))
288 }
289
290 pub fn transform_armed_for(&self, feature_id: &str) -> bool {
292 self.transform_armed() && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
293 }
294
295 pub fn arm_transform(&mut self, feature_id: &str) {
306 let is_acomp = self
307 .history
308 .index_of(feature_id)
309 .and_then(|index| self.history.feature_type(index))
310 .is_some_and(|ty| super::components::is_acomp_feature_type(&ty));
311 if is_acomp {
312 self.component_move_arm(feature_id);
313 return;
314 }
315 self.component_move_reset();
316 self.transform_gizmo.feature_id = Some(feature_id.to_string());
317 self.transform_gizmo.mode = GizmoMode::Transform;
318 self.transform_gizmo.drag = None;
319 self.transform_gizmo.anchor = None;
320 self.transform_gizmo.port = None;
321 self.transform_gizmo.cage = None;
322 self.clear_feature_dimension_overlay();
323 self.sync_transform_gizmo();
324 }
325
326 pub fn disarm_transform(&mut self) {
330 self.component_move_reset();
331 self.transform_gizmo.feature_id = None;
332 self.transform_gizmo.mode = GizmoMode::None;
333 self.transform_gizmo.drag = None;
334 self.transform_gizmo.anchor = None;
335 self.transform_gizmo.port = None;
336 self.transform_gizmo.cage = None;
337 let _ = self.widgets.set_transform_json("null");
338 self.clear_feature_dimension_overlay();
339 self.dirty = true;
340 }
341
342 fn armed_pose(&self) -> Option<TransformPose> {
345 if let Some(address) = self.transform_gizmo.port.clone() {
348 return self.port_point_pose(&address);
349 }
350 let id = self.transform_gizmo.feature_id.as_deref()?;
351 let pmi_annotation = {
352 let state = self.pmi_state_shared();
353 state
354 .find_annotation(id)
355 .map(|(view, annotation)| (view.id.clone(), annotation.kind.clone(), annotation.params.clone()))
356 };
357 if let Some((view_id, kind, params)) = pmi_annotation {
358 if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
359 return None;
360 }
361 if let Some(row) = self.pmi_report.as_ref().and_then(|r| r.annotation(id)) {
362 let fraction = self.explode_step_of(id).map_or(1.0, |step| self.explode_fraction(step));
365 if let brep_kernel::PmiGeometry::Explode {
366 center,
367 translate,
368 rotate_deg,
369 scale,
370 ..
371 } = &super::exploded_ops::explode_at(&row.geometry, fraction)
372 {
373 return Some(TransformPose {
374 position: std::array::from_fn(|i| center[i] + translate[i]),
375 rotation_deg: *rotate_deg,
376 scale: *scale,
377 });
378 }
379 }
380 if kind == "explode" {
385 let center = self.pmi_explode_live_center(id)?;
386 let transform = params.get("transform");
387 let position = read_pose_vec3(None, transform, "position", [0.0; 3]);
388 return Some(TransformPose {
389 position: std::array::from_fn(|axis| center[axis] + position[axis]),
390 rotation_deg: read_pose_vec3(None, transform, "rotationEuler", [0.0; 3]),
391 scale: read_pose_vec3(None, transform, "scale", [1.0; 3]),
392 });
393 }
394 return None;
395 }
396 let index = self.history.index_of(id)?;
397 if self.transform_gizmo.cage.is_some() {
398 return self.cage_pose();
401 }
402 if let Some(anchor) = self.transform_gizmo.anchor {
403 let (position, rotation_deg) = self.spline_anchor_pose(index, anchor)?;
406 return Some(TransformPose {
407 position,
408 rotation_deg,
409 scale: [1.0, 1.0, 1.0],
410 });
411 }
412 let params = self.history.feature_params(index)?;
413 let transform = params.get("transform");
414 let env =
417 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).ok();
418 let env = env.as_ref();
419 let pivot = if self.is_xform_feature(index) {
420 self.xform_pivot(index, ¶ms)
421 } else if self.is_face_transform_feature(index) {
422 self.face_transform_pivot(index, ¶ms, env)?
424 } else {
425 [0.0; 3]
426 };
427 let position = read_pose_vec3(env, transform, "position", [0.0; 3]);
428 Some(TransformPose {
429 position: std::array::from_fn(|axis| position[axis] + pivot[axis]),
430 rotation_deg: read_pose_vec3(env, transform, "rotationEuler", [0.0, 0.0, 0.0]),
431 scale: read_pose_vec3(env, transform, "scale", [1.0, 1.0, 1.0]),
432 })
433 }
434
435 fn is_xform_feature(&self, index: usize) -> bool {
436 self.history.feature_type(index).is_some_and(|ty| {
437 ty.eq_ignore_ascii_case("XFORM") || ty.eq_ignore_ascii_case("TRANSFORM")
438 })
439 }
440
441 pub fn is_face_transform_feature(&self, index: usize) -> bool {
444 self.history
445 .feature_type(index)
446 .is_some_and(|ty| is_face_transform_type(&ty))
447 }
448
449 fn face_transform_pivot(
454 &self,
455 index: usize,
456 params: &serde_json::Value,
457 env: Option<&brep_kernel::Env>,
458 ) -> Option<[f64; 3]> {
459 match params.get("pivot") {
460 Some(pivot) if !pivot.is_null() => Some(read_pose_vec3(env, Some(params), "pivot", [0.0; 3])),
461 _ => self.face_transform_default_pivot(index, params),
462 }
463 }
464
465 pub fn face_transform_has_pivot(&self, feature_id: &str) -> bool {
469 let Some(index) = self.history.index_of(feature_id) else {
470 return false;
471 };
472 let Some(params) = self.history.feature_params(index) else {
473 return false;
474 };
475 self.is_face_transform_feature(index) && self.face_transform_pivot(index, ¶ms, None).is_some()
476 }
477
478 fn face_transform_default_pivot(
484 &self,
485 index: usize,
486 params: &serde_json::Value,
487 ) -> Option<[f64; 3]> {
488 let names = brep_kernel::reference_names(params.get("faces"));
489 if names.is_empty() {
490 return None;
491 }
492 let request: HistoryRequest =
493 serde_json::from_value(self.history.request_before(index)?).ok()?;
494 let _trace = crate::run_trace::span("face_transform_pivot");
495 let result = self.execute_plugin_history(&request);
496 brep_kernel::face_transform_pivot(&result.results, &names)
497 }
498
499 pub(crate) fn stamp_face_transform_pivot(&self, index: usize, params: &mut serde_json::Value) {
509 if !self.is_face_transform_feature(index) {
510 return;
511 }
512 let before = self.history.feature_params(index).unwrap_or(serde_json::Value::Null);
513 let faces_changed = brep_kernel::reference_names(before.get("faces"))
514 != brep_kernel::reference_names(params.get("faces"));
515 let pivot_set_here = before.get("pivot") != params.get("pivot");
516 let unset = params.get("pivot").is_none_or(serde_json::Value::is_null);
517 if !(unset || (faces_changed && !pivot_set_here)) {
518 return;
519 }
520 let pivot = self.face_transform_default_pivot(index, params);
521 if let Some(object) = params.as_object_mut() {
522 object.insert(
523 "pivot".into(),
524 pivot.map_or(serde_json::Value::Null, |pivot| serde_json::json!(pivot)),
525 );
526 }
527 }
528
529 fn xform_pivot(&self, index: usize, params: &serde_json::Value) -> [f64; 3] {
533 if !params["pivot"]
534 .as_str()
535 .is_some_and(|p| p.eq_ignore_ascii_case("BBOX_CENTER"))
536 {
537 return [0.0; 3];
538 }
539 let Ok(request) = serde_json::from_value::<HistoryRequest>(self.run_request_value())
542 else {
543 return [0.0; 3];
544 };
545 let _trace = crate::run_trace::span("xform_pivot");
546 let result = self.execute_plugin_history(&request);
547 let mut handles = HashMap::new();
548 for feature in result.results.iter().take(index) {
549 for name in &feature.removed {
550 handles.remove(name);
551 }
552 for solid in &feature.added {
553 handles.insert(solid.name.clone(), solid.handle);
554 }
555 }
556 let Some(refs) = params["solids"].as_array() else {
557 return [0.0; 3];
558 };
559 for reference in refs {
560 let Some(name) = reference.as_str().or_else(|| reference["name"].as_str()) else {
561 continue;
562 };
563 let Some(handle) = handles.get(name.trim()) else {
564 continue;
565 };
566 if let Ok(center) = brep_kernel::transform_pivot_native(*handle) {
567 return center;
568 }
569 }
570 [0.0; 3]
571 }
572
573 pub fn sync_transform_gizmo(&mut self) {
580 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
583 return;
584 }
585 let Some(pose) = self.armed_pose() else {
586 self.disarm_transform();
587 return;
588 };
589 let show_rings = self.cage_shows_rings().unwrap_or(true);
592 let _ = self
593 .widgets
594 .set_transform_json(&transform_frame_json_with(&pose, show_rings));
595 self.dirty = true;
596 }
597
598 pub fn feature_has_transform(&self, feature_id: &str) -> bool {
602 self.history
603 .index_of(feature_id)
604 .and_then(|i| self.history.feature_type(i))
605 .and_then(|ty| self.feature_schema(&ty))
606 .is_some_and(|schema| schema["inputParamsSchema"]["transform"]["type"] == "transform")
607 }
608
609 pub fn transform_gizmo_anchor(&self) -> Option<(f64, f64)> {
614 let pose = self.armed_pose()?;
615 let (sx, sy, _) = self.camera.project(pose.position);
616 self.camera.projectable(pose.position).then_some((sx, sy))
617 }
618
619 pub fn transform_axis_labels_json(&self) -> String {
624 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
625 return "[]".to_string();
626 }
627 let Some(pose) = self.armed_pose() else {
628 return "[]".to_string();
629 };
630 let euler = [
631 pose.rotation_deg[0].to_radians(),
632 pose.rotation_deg[1].to_radians(),
633 pose.rotation_deg[2].to_radians(),
634 ];
635 let axes = [
636 normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler)),
637 normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler)),
638 normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler)),
639 ];
640 let gap_px = 12.0_f64;
642 let dist =
643 (brep_gizmos::transform::PX_AXIS_LEN as f64 + gap_px) * self.camera.world_per_pixel();
644 let labels: [(&str, [f32; 3]); 3] = [
645 ("XC", [0.92, 0.26, 0.28]), ("YC", [0.30, 0.78, 0.36]), ("ZC", [0.30, 0.52, 0.98]), ];
649 let out: Vec<serde_json::Value> = (0..3)
650 .map(|i| {
651 let o = pose.position;
652 let a = axes[i];
653 serde_json::json!({
654 "text": labels[i].0,
655 "rgb": labels[i].1,
656 "world": [o[0] + a[0] * dist, o[1] + a[1] * dist, o[2] + a[2] * dist],
657 })
658 })
659 .collect();
660 serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
661 }
662
663 pub fn transform_handle_points(&self) -> Vec<(&'static str, (f64, f64))> {
672 use brep_gizmos::hit_region::HitShape;
673 use brep_gizmos::transform as gizmo;
674 let cam = gizmo_camera(&self.camera);
675 self.widgets
676 .transform_hit_regions(&cam)
677 .into_iter()
678 .filter_map(|(handle, shape)| {
679 let key = match handle {
680 gizmo::HANDLE_AXIS_X => "axis:x",
681 gizmo::HANDLE_AXIS_Y => "axis:y",
682 gizmo::HANDLE_AXIS_Z => "axis:z",
683 gizmo::HANDLE_RING_X => "ring:x",
684 gizmo::HANDLE_RING_Y => "ring:y",
685 gizmo::HANDLE_RING_Z => "ring:z",
686 _ => return None,
687 };
688 let [x, y] = match shape {
689 HitShape::Capsule { a, b, .. } => {
690 [a[0] + 0.75 * (b[0] - a[0]), a[1] + 0.75 * (b[1] - a[1])]
691 }
692 HitShape::Circle { c, .. } => c,
693 };
694 let point = (f64::from(x), f64::from(y));
695 (self.transform_pick(point.0, point.1) == handle).then_some((key, point))
696 })
697 .collect()
698 }
699
700 pub fn transform_hit_areas_json(&self) -> String {
704 let cam = gizmo_camera(&self.camera);
705 let regions = self.widgets.transform_hit_regions(&cam);
706 super::camera_widgets::hit_shapes_json(regions.iter().map(|(_, shape)| shape))
707 }
708
709 pub fn transform_press(&mut self, x: f64, y: f64) -> bool {
714 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
717 return false;
718 }
719 let handle = self.transform_pick(x, y);
720 if handle == 0 {
721 return false;
722 }
723 let Some(pose) = self.armed_pose() else {
724 return false;
725 };
726 self.widgets.set_transform_active(handle);
727 self.transform_gizmo.drag = Some(TransformDrag {
728 handle,
729 sx: x as f32,
730 sy: y as f32,
731 start: pose,
732 last: None,
733 });
734 self.cage_grab_begin();
736 self.dirty = true;
737 true
738 }
739
740 pub fn transform_dragging(&self) -> bool {
742 self.transform_gizmo.drag.is_some()
743 }
744
745 pub fn transform_drag_to(&mut self, cx: f64, cy: f64) {
752 let Some(drag) = self.transform_gizmo.drag else {
753 return;
754 };
755 let Some(delta) = self.resolve_transform_delta(&drag, cx, cy) else {
756 return;
757 };
758 let pose = apply_transform_delta(&drag.start, &delta);
759 if let Some(live) = self.transform_gizmo.drag.as_mut() {
763 live.last = Some(delta);
764 }
765 self.write_armed_pose(&pose);
766 self.sync_transform_gizmo();
771 }
772
773 pub fn transform_release(&mut self) {
776 if self.transform_gizmo.drag.take().is_some() {
777 self.widgets.set_transform_active(0);
778 self.dirty = true;
780 self.history.break_coalescing();
791 self.cage_grab_end();
792 self.sync_transform_gizmo();
793 }
794 }
795
796 fn resolve_transform_delta(
799 &self,
800 drag: &TransformDrag,
801 cx: f64,
802 cy: f64,
803 ) -> Option<TransformDelta> {
804 let cam = gizmo_camera(&self.camera);
805 let frame_json = transform_frame_json(&drag.start);
806 let json = self.widgets.transform_drag_json_with_frame(
807 &cam,
808 &frame_json,
809 drag.handle,
810 drag.sx,
811 drag.sy,
812 cx as f32,
813 cy as f32,
814 );
815 parse_drag_delta(&json)
816 }
817
818 fn write_armed_pose(&mut self, pose: &TransformPose) {
821 let start = self.transform_gizmo.drag.map(|drag| drag.start);
824 if let Some(address) = self.transform_gizmo.port.clone() {
825 self.write_port_point_pose(&address, pose);
826 return;
827 }
828 let Some(id) = self.transform_gizmo.feature_id.clone() else {
829 return;
830 };
831 let pmi = self.pmi_state();
832 if let Some((_, annotation)) = pmi.find_annotation(&id) {
833 if annotation.kind == "explode" {
834 let laid_out = self
835 .pmi_report
836 .as_ref()
837 .and_then(|r| r.annotation(&id))
838 .and_then(|r| match &r.geometry {
839 brep_kernel::PmiGeometry::Explode { center, .. } => Some(*center),
840 _ => None,
841 });
842 let center = laid_out.or_else(|| self.pmi_explode_live_center(&id));
846 if let Some(center) = center {
847 if let (true, Some(progress), Some(step)) = (laid_out.is_some(), self.explode_playback.progress, self.explode_step_of(&id)) {
852 self.explode_playback.progress = Some(progress.max(step as f64 + 1.0));
853 }
854 let mut params = annotation.params.clone();
855 let position: [f64; 3] = std::array::from_fn(|i| {
856 drag_stored(start.map(|s| s.position[i] - center[i]), pose.position[i] - center[i])
857 });
858 let rotation = drag_stored_rotation(start.as_ref(), pose);
859 params["transform"] = serde_json::json!({ "position": position, "rotationEuler": rotation, "scale": pose.scale });
860 let _ = self.pmi_update_annotation(&id, ¶ms.to_string());
861 }
862 return;
863 }
864 }
865 let Some(index) = self.history.index_of(&id) else {
866 return;
867 };
868 if self.transform_gizmo.cage.is_some() {
869 self.write_cage_pose(pose);
870 return;
871 }
872 if let Some(anchor) = self.transform_gizmo.anchor {
873 let position = std::array::from_fn(|i| drag_stored(start.map(|s| s.position[i]), pose.position[i]));
874 let rotation = drag_stored_rotation(start.as_ref(), pose);
875 self.write_spline_anchor_pose(&id, anchor, position, rotation);
876 return;
877 }
878 let mut params = self
879 .history
880 .feature_params(index)
881 .unwrap_or_else(|| serde_json::json!({}));
882 let pivot = if self.is_xform_feature(index) {
883 self.xform_pivot(index, ¶ms)
884 } else if self.is_face_transform_feature(index) {
885 let env =
886 brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator())
887 .ok();
888 let Some(pivot) = self.face_transform_pivot(index, ¶ms, env.as_ref()) else {
889 return;
890 };
891 pivot
892 } else {
893 [0.0; 3]
894 };
895 let position: [f64; 3] = std::array::from_fn(|axis| {
896 drag_stored(start.map(|s| s.position[axis] - pivot[axis]), pose.position[axis] - pivot[axis])
897 });
898 let rotation = drag_stored_rotation(start.as_ref(), pose);
899 if !params
901 .get("transform")
902 .map(|t| t.is_object())
903 .unwrap_or(false)
904 {
905 if let Some(object) = params.as_object_mut() {
906 object.insert("transform".into(), serde_json::json!({}));
907 }
908 }
909 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
910 transform.insert("position".into(), serde_json::json!(position));
911 transform.insert("rotationEuler".into(), serde_json::json!(rotation));
912 }
913 let _ = self.update_feature_params(&id, ¶ms.to_string());
914 }
915}
916
917fn drag_stored(before: Option<f64>, after: f64) -> f64 {
920 match before {
921 Some(before) => crate::formatting::snap_drag_moved(before, after),
922 None => after,
923 }
924}
925
926fn drag_stored_rotation(start: Option<&TransformPose>, pose: &TransformPose) -> [f64; 3] {
928 std::array::from_fn(|i| drag_stored(start.map(|s| s.rotation_deg[i]), pose.rotation_deg[i]))
929}
930
931fn dominant_axis_label(v: &[f64; 3]) -> &'static str {
941 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
942 if length < 1e-12 {
943 return "\u{0394}";
944 }
945 const NAMES: [&str; 3] = ["X", "Y", "Z"];
946 for axis in 0..3 {
947 if v[axis].abs() >= length * 0.999 {
948 return NAMES[axis];
949 }
950 }
951 "\u{0394}"
952}
953
954pub fn is_face_transform_type(feature_type: &str) -> bool {
957 matches!(
958 feature_type.trim().to_ascii_uppercase().as_str(),
959 "TF" | "TRANSFORM FACE" | "TRANSFORMFACE"
960 )
961}
962
963fn apply_transform_delta(start: &TransformPose, delta: &TransformDelta) -> TransformPose {
965 match delta {
966 TransformDelta::Translate(world) => TransformPose {
967 position: [
968 start.position[0] + world[0],
969 start.position[1] + world[1],
970 start.position[2] + world[2],
971 ],
972 ..*start
973 },
974 TransformDelta::Rotate { axis, radians } => {
975 let q0 = quat_from_euler_xyz_deg(start.rotation_deg);
976 let dq = quat_from_axis_angle(*axis, *radians);
977 let nq = quat_mul(dq, q0);
978 TransformPose {
979 rotation_deg: euler_xyz_deg_from_quat(nq),
980 ..*start
981 }
982 }
983 }
984}
985
986fn read_pose_vec3(
989 env: Option<&brep_kernel::Env>,
990 transform: Option<&serde_json::Value>,
991 key: &str,
992 default: [f64; 3],
993) -> [f64; 3] {
994 let array = transform
995 .and_then(|t| t.get(key))
996 .and_then(|v| v.as_array());
997 let mut out = default;
998 if let Some(array) = array {
999 for (index, slot) in out.iter_mut().enumerate() {
1000 match array.get(index) {
1001 Some(serde_json::Value::Number(number)) => {
1002 if let Some(number) = number.as_f64() {
1003 *slot = number;
1004 }
1005 }
1006 Some(serde_json::Value::String(source)) => {
1012 if let Some(number) = env
1013 .and_then(|env| env.eval(source).ok())
1014 .filter(|number| number.is_finite())
1015 {
1016 *slot = number;
1017 }
1018 }
1019 _ => {}
1020 }
1021 }
1022 }
1023 out
1024}
1025
1026fn transform_frame_json(pose: &TransformPose) -> String {
1030 transform_frame_json_with(pose, true)
1031}
1032
1033fn transform_frame_json_with(pose: &TransformPose, show_rings: bool) -> String {
1035 let euler = [
1036 pose.rotation_deg[0].to_radians(),
1037 pose.rotation_deg[1].to_radians(),
1038 pose.rotation_deg[2].to_radians(),
1039 ];
1040 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
1041 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
1042 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
1043 serde_json::json!({
1044 "origin": pose.position,
1045 "x": x,
1046 "y": y,
1047 "z": z,
1048 "showCenter": true,
1049 "showAxes": true,
1050 "showRings": show_rings,
1051 })
1052 .to_string()
1053}
1054
1055pub(super) fn normalize3(v: [f64; 3]) -> [f64; 3] {
1056 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
1057 if length < 1e-12 {
1058 [0.0, 0.0, 1.0]
1059 } else {
1060 [v[0] / length, v[1] / length, v[2] / length]
1061 }
1062}
1063
1064pub(crate) fn rotate_euler_xyz_f64(v: [f64; 3], euler: [f64; 3]) -> [f64; 3] {
1068 let (c1, s1) = (euler[0].cos(), euler[0].sin());
1069 let (c2, s2) = (euler[1].cos(), euler[1].sin());
1070 let (c3, s3) = (euler[2].cos(), euler[2].sin());
1071 let m00 = c2 * c3;
1072 let m01 = -c2 * s3;
1073 let m02 = s2;
1074 let m10 = c1 * s3 + c3 * s1 * s2;
1075 let m11 = c1 * c3 - s1 * s2 * s3;
1076 let m12 = -c2 * s1;
1077 let m20 = s1 * s3 - c1 * c3 * s2;
1078 let m21 = c3 * s1 + c1 * s2 * s3;
1079 let m22 = c1 * c2;
1080 [
1081 m00 * v[0] + m01 * v[1] + m02 * v[2],
1082 m10 * v[0] + m11 * v[1] + m12 * v[2],
1083 m20 * v[0] + m21 * v[1] + m22 * v[2],
1084 ]
1085}
1086
1087pub(super) type Quat = [f64; 4]; pub(super) fn quat_from_axis_angle(axis: [f64; 3], angle: f64) -> Quat {
1092 let n = normalize3(axis);
1093 let half = angle * 0.5;
1094 let s = half.sin();
1095 [n[0] * s, n[1] * s, n[2] * s, half.cos()]
1096}
1097
1098pub(super) fn quat_from_euler_xyz_deg(deg: [f64; 3]) -> Quat {
1100 let (c1, s1) = (
1101 (deg[0].to_radians() * 0.5).cos(),
1102 (deg[0].to_radians() * 0.5).sin(),
1103 );
1104 let (c2, s2) = (
1105 (deg[1].to_radians() * 0.5).cos(),
1106 (deg[1].to_radians() * 0.5).sin(),
1107 );
1108 let (c3, s3) = (
1109 (deg[2].to_radians() * 0.5).cos(),
1110 (deg[2].to_radians() * 0.5).sin(),
1111 );
1112 [
1113 s1 * c2 * c3 + c1 * s2 * s3,
1114 c1 * s2 * c3 - s1 * c2 * s3,
1115 c1 * c2 * s3 + s1 * s2 * c3,
1116 c1 * c2 * c3 - s1 * s2 * s3,
1117 ]
1118}
1119
1120pub(super) fn quat_mul(a: Quat, b: Quat) -> Quat {
1122 [
1123 a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
1124 a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
1125 a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
1126 a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
1127 ]
1128}
1129
1130pub(super) fn euler_xyz_deg_from_quat(q: Quat) -> [f64; 3] {
1134 let [x, y, z, w] = q;
1135 let (x2, y2, z2) = (x + x, y + y, z + z);
1136 let (xx, xy, xz) = (x * x2, x * y2, x * z2);
1137 let (yy, yz, zz) = (y * y2, y * z2, z * z2);
1138 let (wx, wy, wz) = (w * x2, w * y2, w * z2);
1139 let m11 = 1.0 - (yy + zz);
1141 let m12 = xy - wz;
1142 let m13 = xz + wy;
1143 let m22 = 1.0 - (xx + zz);
1144 let m23 = yz - wx;
1145 let m32 = yz + wx;
1146 let m33 = 1.0 - (xx + yy);
1147 let ey = m13.clamp(-1.0, 1.0).asin();
1148 let (ex, ez) = if m13.abs() < 0.9999999 {
1149 ((-m23).atan2(m33), (-m12).atan2(m11))
1150 } else {
1151 (m32.atan2(m22), 0.0)
1152 };
1153 [ex.to_degrees(), ey.to_degrees(), ez.to_degrees()]
1154}
1155
1156impl EngineState {
1177 pub fn transform_armed_port_point(&self) -> Option<&str> {
1179 self.transform_armed()
1180 .then(|| self.transform_gizmo.port.as_deref())
1181 .flatten()
1182 }
1183
1184 pub fn arm_transform_for_port_point(&mut self, address: &str) -> bool {
1187 if let Some(reason) = self.port_point_gizmo_refusal(address) {
1188 self.push_notice(format!("no handles on {address}: {reason}"));
1189 return false;
1190 }
1191 self.component_move_reset();
1192 self.transform_gizmo.feature_id = None;
1193 self.transform_gizmo.anchor = None;
1194 self.transform_gizmo.cage = None;
1195 self.transform_gizmo.port = Some(address.to_string());
1196 self.transform_gizmo.mode = GizmoMode::Transform;
1197 self.transform_gizmo.drag = None;
1198 self.clear_feature_dimension_overlay();
1199 self.sync_transform_gizmo();
1200 true
1201 }
1202
1203 pub fn port_point_gizmo_refusal(&self, address: &str) -> Option<String> {
1222 let point = self.declared_point_value(address)?;
1223 if let Some(target) = point
1224 .get("mapsTo")
1225 .and_then(serde_json::Value::as_str)
1226 .map(str::trim)
1227 .filter(|target| !target.is_empty())
1228 {
1229 return Some(format!(
1230 "it maps to '{target}', and that point's placement is this one's"
1231 ));
1232 }
1233 let transform = point.get("transform")?;
1234 for key in ["position", "rotationEuler"] {
1235 let Some(components) = transform.get(key).and_then(serde_json::Value::as_array) else {
1236 continue;
1237 };
1238 for component in components {
1239 if let Some(expression) = component.as_str() {
1240 return Some(format!(
1241 "'{expression}' places it, and a drag writes numbers"
1242 ));
1243 }
1244 }
1245 }
1246 None
1247 }
1248
1249 fn port_point_seat(&self, address: &str) -> brep_kernel::Frame {
1253 self.port_point_row(address)
1254 .map(|row| row.seat.to_frame())
1255 .unwrap_or_else(brep_kernel::world_seat)
1256 }
1257
1258 pub(super) fn port_point_pose(&self, address: &str) -> Option<TransformPose> {
1261 let point = self.declared_point_value(address)?;
1262 let transform = point.get("transform");
1263 let local_position = read_pose_vec3(None, transform, "position", [0.0; 3]);
1264 let local_rotation = read_pose_vec3(None, transform, "rotationEuler", [0.0; 3]);
1265 let seat = self.port_point_seat(address);
1266 let world_axes = super::spline_edit::axes_from_euler_deg(local_rotation)
1267 .map(|axis| seat_axis(&seat, axis));
1268 Some(TransformPose {
1269 position: array3(brep_kernel::seat_point(&seat, vec3(local_position))),
1270 rotation_deg: super::spline_edit::euler_xyz_deg_from_axes(
1271 world_axes[0],
1272 world_axes[1],
1273 world_axes[2],
1274 ),
1275 scale: [1.0, 1.0, 1.0],
1276 })
1277 }
1278
1279 pub(super) fn write_port_point_pose(&mut self, address: &str, pose: &TransformPose) {
1282 let seat = self.port_point_seat(address);
1283 let local = |pose: &TransformPose| {
1284 let local_axes = super::spline_edit::axes_from_euler_deg(pose.rotation_deg)
1285 .map(|axis| unseat_axis(&seat, axis));
1286 (
1287 array3(brep_kernel::unseat_point(&seat, vec3(pose.position))),
1288 super::spline_edit::euler_xyz_deg_from_axes(local_axes[0], local_axes[1], local_axes[2]),
1289 )
1290 };
1291 let (position, rotation) = local(pose);
1292 let before = self.transform_gizmo.drag.map(|drag| local(&drag.start));
1293 let position: [f64; 3] = std::array::from_fn(|i| drag_stored(before.map(|b| b.0[i]), position[i]));
1294 let rotation: [f64; 3] = std::array::from_fn(|i| drag_stored(before.map(|b| b.1[i]), rotation[i]));
1295 let key = format!("port-point-pose:{address}");
1296 self.edit_port_point(address, Some(&key), |point| {
1297 point.insert(
1298 "transform".into(),
1299 serde_json::json!({ "position": position, "rotationEuler": rotation }),
1300 );
1301 });
1302 }
1303}
1304
1305fn vec3(a: [f64; 3]) -> brep_kernel::Vec3 {
1306 brep_kernel::Vec3::new(a[0], a[1], a[2])
1307}
1308
1309fn array3(v: brep_kernel::Vec3) -> [f64; 3] {
1310 [v.x, v.y, v.z]
1311}
1312
1313fn seat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
1315 array3(brep_kernel::seat_vector(seat, vec3(axis)))
1316}
1317
1318fn unseat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
1320 array3(brep_kernel::unseat_vector(seat, vec3(axis)))
1321}
1322