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}
38
39#[derive(Clone, Copy)]
44pub(super) struct TransformDrag {
45 handle: u32,
46 sx: f32,
47 sy: f32,
48 start: TransformPose,
49}
50
51#[derive(Clone, Copy, PartialEq, Debug)]
55struct TransformPose {
56 position: [f64; 3],
57 rotation_deg: [f64; 3],
58 scale: [f64; 3],
59}
60
61#[derive(Clone, Copy, PartialEq, Debug)]
63pub(super) enum TransformDelta {
64 Translate([f64; 3]),
66 Rotate { axis: [f64; 3], radians: f64 },
68}
69
70pub(super) fn parse_drag_delta(json: &str) -> Option<TransformDelta> {
72 let value: serde_json::Value = serde_json::from_str(json).ok()?;
73 match value.get("kind").and_then(|k| k.as_str()) {
74 Some("translate") => Some(TransformDelta::Translate(
75 crate::json_support::vec3_or(value.get("world"), [0.0; 3]),
76 )),
77 Some("rotate") => Some(TransformDelta::Rotate {
78 axis: crate::json_support::vec3_or(value.get("axisWorld"), [0.0; 3]),
79 radians: value.get("radians").and_then(|n| n.as_f64()).unwrap_or(0.0),
80 }),
81 _ => None,
82 }
83}
84
85impl EngineState {
86 pub fn transform_armed(&self) -> bool {
90 matches!(self.transform_gizmo.mode, GizmoMode::Transform)
91 }
92
93 pub fn transform_armed_feature(&self) -> String {
95 if self.transform_armed() {
96 self.transform_gizmo.feature_id.clone().unwrap_or_default()
97 } else {
98 String::new()
99 }
100 }
101
102 pub fn gizmo_state_json(&self) -> String {
108 let mode = match self.transform_gizmo.mode {
109 GizmoMode::None => "none",
110 GizmoMode::Transform => "transform",
111 GizmoMode::Dimension => "dimension",
112 };
113 serde_json::json!({
114 "mode": mode,
115 "armed": self.transform_armed(),
116 "feature": self.transform_gizmo.feature_id,
117 "anchor": self.transform_gizmo.anchor,
118 "origin": self.widgets.transform_origin(),
119 })
120 .to_string()
121 }
122
123 pub fn transform_armed_for(&self, feature_id: &str) -> bool {
125 self.transform_armed() && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
126 }
127
128 pub fn arm_transform(&mut self, feature_id: &str) {
139 let is_acomp = self
140 .history
141 .index_of(feature_id)
142 .and_then(|index| self.history.feature_type(index))
143 .is_some_and(|ty| super::components::is_acomp_feature_type(&ty));
144 if is_acomp {
145 self.component_move_arm(feature_id);
146 return;
147 }
148 self.component_move_reset();
149 self.transform_gizmo.feature_id = Some(feature_id.to_string());
150 self.transform_gizmo.mode = GizmoMode::Transform;
151 self.transform_gizmo.drag = None;
152 self.transform_gizmo.anchor = None;
153 self.clear_feature_dimension_overlay();
154 self.sync_transform_gizmo();
155 }
156
157 pub fn disarm_transform(&mut self) {
161 self.component_move_reset();
162 self.transform_gizmo.feature_id = None;
163 self.transform_gizmo.mode = GizmoMode::None;
164 self.transform_gizmo.drag = None;
165 self.transform_gizmo.anchor = None;
166 let _ = self.widgets.set_transform_json("null");
167 self.clear_feature_dimension_overlay();
168 self.dirty = true;
169 }
170
171 fn armed_pose(&self) -> Option<TransformPose> {
174 let id = self.transform_gizmo.feature_id.as_deref()?;
175 let index = self.history.index_of(id)?;
176 if let Some(anchor) = self.transform_gizmo.anchor {
177 let (position, rotation_deg) = self.spline_anchor_pose(index, anchor)?;
180 return Some(TransformPose {
181 position,
182 rotation_deg,
183 scale: [1.0, 1.0, 1.0],
184 });
185 }
186 let params = self.history.feature_params(index)?;
187 let transform = params.get("transform");
188 let pivot = if self.is_xform_feature(index) {
189 self.xform_pivot(index, ¶ms)
190 } else {
191 [0.0; 3]
192 };
193 let env = brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator())
196 .ok();
197 let env = env.as_ref();
198 let position = read_pose_vec3(env, transform, "position", [0.0; 3]);
199 Some(TransformPose {
200 position: std::array::from_fn(|axis| position[axis] + pivot[axis]),
201 rotation_deg: read_pose_vec3(env, transform, "rotationEuler", [0.0, 0.0, 0.0]),
202 scale: read_pose_vec3(env, transform, "scale", [1.0, 1.0, 1.0]),
203 })
204 }
205
206 fn is_xform_feature(&self, index: usize) -> bool {
207 self.history.feature_type(index).is_some_and(|ty| {
208 ty.eq_ignore_ascii_case("XFORM") || ty.eq_ignore_ascii_case("TRANSFORM")
209 })
210 }
211
212 fn xform_pivot(&self, index: usize, params: &serde_json::Value) -> [f64; 3] {
216 if !params["pivot"]
217 .as_str()
218 .is_some_and(|p| p.eq_ignore_ascii_case("BBOX_CENTER"))
219 {
220 return [0.0; 3];
221 }
222 let Ok(request) = serde_json::from_value::<HistoryRequest>(self.history.prefix_request())
225 else {
226 return [0.0; 3];
227 };
228 let result = brep_kernel::execute_history(&request);
229 let mut handles = HashMap::new();
230 for feature in result.results.iter().take(index) {
231 for name in &feature.removed {
232 handles.remove(name);
233 }
234 for solid in &feature.added {
235 handles.insert(solid.name.clone(), solid.handle);
236 }
237 }
238 let Some(refs) = params["solids"].as_array() else {
239 return [0.0; 3];
240 };
241 for reference in refs {
242 let Some(name) = reference.as_str().or_else(|| reference["name"].as_str()) else {
243 continue;
244 };
245 let Some(handle) = handles.get(name.trim()) else {
246 continue;
247 };
248 if let Ok(center) = brep_kernel::transform_pivot_native(*handle) {
249 return center;
250 }
251 }
252 [0.0; 3]
253 }
254
255 pub fn sync_transform_gizmo(&mut self) {
262 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
265 return;
266 }
267 let Some(pose) = self.armed_pose() else {
268 self.disarm_transform();
269 return;
270 };
271 let _ = self
272 .widgets
273 .set_transform_json(&transform_frame_json(&pose));
274 self.dirty = true;
275 }
276
277 pub fn feature_has_transform(&self, feature_id: &str) -> bool {
281 self.history
282 .index_of(feature_id)
283 .and_then(|i| self.history.feature_type(i))
284 .and_then(|ty| crate::features::feature_schema(&ty))
285 .is_some_and(|schema| schema["inputParamsSchema"]["transform"]["type"] == "transform")
286 }
287
288 pub fn transform_gizmo_anchor(&self) -> Option<(f64, f64)> {
293 let pose = self.armed_pose()?;
294 let (sx, sy, _) = self.camera.project(pose.position);
295 self.camera.projectable(pose.position).then_some((sx, sy))
296 }
297
298 pub fn transform_axis_labels_json(&self) -> String {
303 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
304 return "[]".to_string();
305 }
306 let Some(pose) = self.armed_pose() else {
307 return "[]".to_string();
308 };
309 let euler = [
310 pose.rotation_deg[0].to_radians(),
311 pose.rotation_deg[1].to_radians(),
312 pose.rotation_deg[2].to_radians(),
313 ];
314 let axes = [
315 normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler)),
316 normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler)),
317 normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler)),
318 ];
319 let gap_px = 12.0_f64;
321 let dist =
322 (brep_gizmos::transform::PX_AXIS_LEN as f64 + gap_px) * self.camera.world_per_pixel();
323 let labels: [(&str, [f32; 3]); 3] = [
324 ("XC", [0.92, 0.26, 0.28]), ("YC", [0.30, 0.78, 0.36]), ("ZC", [0.30, 0.52, 0.98]), ];
328 let out: Vec<serde_json::Value> = (0..3)
329 .map(|i| {
330 let o = pose.position;
331 let a = axes[i];
332 serde_json::json!({
333 "text": labels[i].0,
334 "rgb": labels[i].1,
335 "world": [o[0] + a[0] * dist, o[1] + a[1] * dist, o[2] + a[2] * dist],
336 })
337 })
338 .collect();
339 serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
340 }
341
342 pub fn transform_hit_areas_json(&self) -> String {
346 let cam = gizmo_camera(&self.camera);
347 let regions = self.widgets.transform_hit_regions(&cam);
348 super::camera_widgets::hit_shapes_json(regions.iter().map(|(_, shape)| shape))
349 }
350
351 pub fn transform_press(&mut self, x: f64, y: f64) -> bool {
356 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
359 return false;
360 }
361 let handle = self.transform_pick(x, y);
362 if handle == 0 {
363 return false;
364 }
365 let Some(pose) = self.armed_pose() else {
366 return false;
367 };
368 self.widgets.set_transform_active(handle);
369 self.transform_gizmo.drag = Some(TransformDrag {
370 handle,
371 sx: x as f32,
372 sy: y as f32,
373 start: pose,
374 });
375 self.dirty = true;
376 true
377 }
378
379 pub fn transform_dragging(&self) -> bool {
381 self.transform_gizmo.drag.is_some()
382 }
383
384 pub fn transform_drag_to(&mut self, cx: f64, cy: f64) {
391 let Some(drag) = self.transform_gizmo.drag else {
392 return;
393 };
394 let Some(delta) = self.resolve_transform_delta(&drag, cx, cy) else {
395 return;
396 };
397 let pose = apply_transform_delta(&drag.start, &delta);
398 self.write_armed_pose(&pose);
399 self.sync_transform_gizmo();
404 }
405
406 pub fn transform_release(&mut self) {
409 if self.transform_gizmo.drag.take().is_some() {
410 self.widgets.set_transform_active(0);
411 self.sync_transform_gizmo();
412 }
413 }
414
415 fn resolve_transform_delta(
418 &self,
419 drag: &TransformDrag,
420 cx: f64,
421 cy: f64,
422 ) -> Option<TransformDelta> {
423 let cam = gizmo_camera(&self.camera);
424 let frame_json = transform_frame_json(&drag.start);
425 let json = self.widgets.transform_drag_json_with_frame(
426 &cam,
427 &frame_json,
428 drag.handle,
429 drag.sx,
430 drag.sy,
431 cx as f32,
432 cy as f32,
433 );
434 parse_drag_delta(&json)
435 }
436
437 fn write_armed_pose(&mut self, pose: &TransformPose) {
440 let Some(id) = self.transform_gizmo.feature_id.clone() else {
441 return;
442 };
443 let Some(index) = self.history.index_of(&id) else {
444 return;
445 };
446 if let Some(anchor) = self.transform_gizmo.anchor {
447 self.write_spline_anchor_pose(&id, anchor, pose.position, pose.rotation_deg);
448 return;
449 }
450 let mut params = self
451 .history
452 .feature_params(index)
453 .unwrap_or_else(|| serde_json::json!({}));
454 let pivot = if self.is_xform_feature(index) {
455 self.xform_pivot(index, ¶ms)
456 } else {
457 [0.0; 3]
458 };
459 let position: [f64; 3] = std::array::from_fn(|axis| pose.position[axis] - pivot[axis]);
460 if !params
462 .get("transform")
463 .map(|t| t.is_object())
464 .unwrap_or(false)
465 {
466 if let Some(object) = params.as_object_mut() {
467 object.insert("transform".into(), serde_json::json!({}));
468 }
469 }
470 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
471 transform.insert("position".into(), serde_json::json!(position));
472 transform.insert("rotationEuler".into(), serde_json::json!(pose.rotation_deg));
473 }
474 let _ = self.update_feature_params(&id, ¶ms.to_string());
475 }
476}
477
478fn apply_transform_delta(start: &TransformPose, delta: &TransformDelta) -> TransformPose {
480 match delta {
481 TransformDelta::Translate(world) => TransformPose {
482 position: [
483 start.position[0] + world[0],
484 start.position[1] + world[1],
485 start.position[2] + world[2],
486 ],
487 ..*start
488 },
489 TransformDelta::Rotate { axis, radians } => {
490 let q0 = quat_from_euler_xyz_deg(start.rotation_deg);
491 let dq = quat_from_axis_angle(*axis, *radians);
492 let nq = quat_mul(dq, q0);
493 TransformPose {
494 rotation_deg: euler_xyz_deg_from_quat(nq),
495 ..*start
496 }
497 }
498 }
499}
500
501fn read_pose_vec3(
504 env: Option<&brep_kernel::Env>,
505 transform: Option<&serde_json::Value>,
506 key: &str,
507 default: [f64; 3],
508) -> [f64; 3] {
509 let array = transform
510 .and_then(|t| t.get(key))
511 .and_then(|v| v.as_array());
512 let mut out = default;
513 if let Some(array) = array {
514 for (index, slot) in out.iter_mut().enumerate() {
515 match array.get(index) {
516 Some(serde_json::Value::Number(number)) => {
517 if let Some(number) = number.as_f64() {
518 *slot = number;
519 }
520 }
521 Some(serde_json::Value::String(source)) => {
527 if let Some(number) = env
528 .and_then(|env| env.eval(source).ok())
529 .filter(|number| number.is_finite())
530 {
531 *slot = number;
532 }
533 }
534 _ => {}
535 }
536 }
537 }
538 out
539}
540
541fn transform_frame_json(pose: &TransformPose) -> String {
545 let euler = [
546 pose.rotation_deg[0].to_radians(),
547 pose.rotation_deg[1].to_radians(),
548 pose.rotation_deg[2].to_radians(),
549 ];
550 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
551 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
552 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
553 serde_json::json!({
554 "origin": pose.position,
555 "x": x,
556 "y": y,
557 "z": z,
558 "showCenter": true,
559 })
560 .to_string()
561}
562
563pub(super) fn normalize3(v: [f64; 3]) -> [f64; 3] {
564 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
565 if length < 1e-12 {
566 [0.0, 0.0, 1.0]
567 } else {
568 [v[0] / length, v[1] / length, v[2] / length]
569 }
570}
571
572pub(crate) fn rotate_euler_xyz_f64(v: [f64; 3], euler: [f64; 3]) -> [f64; 3] {
576 let (c1, s1) = (euler[0].cos(), euler[0].sin());
577 let (c2, s2) = (euler[1].cos(), euler[1].sin());
578 let (c3, s3) = (euler[2].cos(), euler[2].sin());
579 let m00 = c2 * c3;
580 let m01 = -c2 * s3;
581 let m02 = s2;
582 let m10 = c1 * s3 + c3 * s1 * s2;
583 let m11 = c1 * c3 - s1 * s2 * s3;
584 let m12 = -c2 * s1;
585 let m20 = s1 * s3 - c1 * c3 * s2;
586 let m21 = c3 * s1 + c1 * s2 * s3;
587 let m22 = c1 * c2;
588 [
589 m00 * v[0] + m01 * v[1] + m02 * v[2],
590 m10 * v[0] + m11 * v[1] + m12 * v[2],
591 m20 * v[0] + m21 * v[1] + m22 * v[2],
592 ]
593}
594
595pub(super) type Quat = [f64; 4]; pub(super) fn quat_from_axis_angle(axis: [f64; 3], angle: f64) -> Quat {
600 let n = normalize3(axis);
601 let half = angle * 0.5;
602 let s = half.sin();
603 [n[0] * s, n[1] * s, n[2] * s, half.cos()]
604}
605
606pub(super) fn quat_from_euler_xyz_deg(deg: [f64; 3]) -> Quat {
608 let (c1, s1) = (
609 (deg[0].to_radians() * 0.5).cos(),
610 (deg[0].to_radians() * 0.5).sin(),
611 );
612 let (c2, s2) = (
613 (deg[1].to_radians() * 0.5).cos(),
614 (deg[1].to_radians() * 0.5).sin(),
615 );
616 let (c3, s3) = (
617 (deg[2].to_radians() * 0.5).cos(),
618 (deg[2].to_radians() * 0.5).sin(),
619 );
620 [
621 s1 * c2 * c3 + c1 * s2 * s3,
622 c1 * s2 * c3 - s1 * c2 * s3,
623 c1 * c2 * s3 + s1 * s2 * c3,
624 c1 * c2 * c3 - s1 * s2 * s3,
625 ]
626}
627
628pub(super) fn quat_mul(a: Quat, b: Quat) -> Quat {
630 [
631 a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
632 a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
633 a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
634 a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
635 ]
636}
637
638pub(super) fn euler_xyz_deg_from_quat(q: Quat) -> [f64; 3] {
642 let [x, y, z, w] = q;
643 let (x2, y2, z2) = (x + x, y + y, z + z);
644 let (xx, xy, xz) = (x * x2, x * y2, x * z2);
645 let (yy, yz, zz) = (y * y2, y * z2, z * z2);
646 let (wx, wy, wz) = (w * x2, w * y2, w * z2);
647 let m11 = 1.0 - (yy + zz);
649 let m12 = xy - wz;
650 let m13 = xz + wy;
651 let m22 = 1.0 - (xx + zz);
652 let m23 = yz - wx;
653 let m32 = yz + wx;
654 let m33 = 1.0 - (xx + yy);
655 let ey = m13.clamp(-1.0, 1.0).asin();
656 let (ex, ez) = if m13.abs() < 0.9999999 {
657 ((-m23).atan2(m33), (-m12).atan2(m11))
658 } else {
659 (m32.atan2(m22), 0.0)
660 };
661 [ex.to_degrees(), ey.to_degrees(), ez.to_degrees()]
662}
663
664