1use crate::geometry3d::{add3, cross3 as cross, sub3};
2use super::transform_gizmo::{
3 euler_xyz_deg_from_quat, normalize3, quat_from_axis_angle, quat_from_euler_xyz_deg, quat_mul,
4 parse_drag_delta, Quat, TransformDelta,
5};
6use super::*;
7
8pub struct ComponentMoveArm {
26 pub(super) feature_id: Option<String>,
29 pub(super) group: Vec<String>,
32 pub(super) unfix: Vec<String>,
37 select_after_run: bool,
39 pub(super) anchor: [f64; 3],
42 pub(super) drag: Option<ComponentMoveDrag>,
44 pub(super) live_originals: std::collections::HashMap<String, crate::scene::SolidDisplay>,
48 pub(super) live_applied: std::collections::HashMap<String, ([f64; 3], [f64; 3])>,
54 pub(super) budget_ms: f64,
56 pub(super) solve_cost_override_ms: Option<f64>,
59 pub(super) typed: Option<TypedMove>,
61}
62
63#[derive(Clone, Debug, PartialEq)]
67pub struct TypedMove {
68 pub field: String,
71 pub asked: f64,
72 pub applied: f64,
73 pub blocking: Vec<String>,
76 pub written: bool,
78}
79
80impl TypedMove {
81 pub fn complete(&self) -> bool {
83 (self.applied - self.asked).abs() <= TYPED_TOLERANCE * self.asked.abs().max(1.0)
84 }
85
86 pub fn summary(&self) -> String {
88 let (axis, unit, space) = typed_axis(&self.field).map(|(axis, rotate)| {
89 (["X", "Y", "Z"][axis], if rotate { "\u{00b0}" } else { " mm" }, if rotate { "about" } else { "along" })
90 }).unwrap_or(("?", "", "along"));
91 let shown = |value: f64| crate::formatting::compact_decimal(value, 4);
92 let limited = if self.blocking.is_empty() {
93 String::new()
94 } else {
95 format!(", limited by {}", self.blocking.join(", "))
96 };
97 if !self.written {
98 return match self.blocking.as_slice() {
99 [] => format!("Not moved {space} {axis}: the mates hold it here."),
100 held => format!("Not moved {space} {axis}: held by {}.", held.join(", ")),
101 };
102 }
103 if self.complete() {
104 return format!("Moved {}{unit} {space} {axis}.", shown(self.applied));
105 }
106 format!("Moved {} of {}{unit} {space} {axis}{limited}.", shown(self.applied), shown(self.asked))
107 }
108}
109
110const TYPED_TOLERANCE: f64 = 1e-6;
112
113fn typed_axis(field: &str) -> Option<(usize, bool)> {
115 match field {
116 "x" => Some((0, false)),
117 "y" => Some((1, false)),
118 "z" => Some((2, false)),
119 "rx" => Some((0, true)),
120 "ry" => Some((1, true)),
121 "rz" => Some((2, true)),
122 _ => None,
123 }
124}
125
126pub(super) const DRAG_SOLVE_BUDGET_MS: f64 = 30.0;
129const DRAG_SOLVE_MAX_SKIP: u32 = 10;
131
132impl Default for ComponentMoveArm {
133 fn default() -> Self {
134 Self {
135 feature_id: None,
136 group: Vec::new(),
137 unfix: Vec::new(),
138 select_after_run: false,
139 anchor: [0.0; 3],
140 drag: None,
141 live_originals: std::collections::HashMap::new(),
142 live_applied: std::collections::HashMap::new(),
143 budget_ms: DRAG_SOLVE_BUDGET_MS,
144 solve_cost_override_ms: None,
145 typed: None,
146 }
147 }
148}
149
150#[derive(Clone, Default, Debug)]
153pub(super) struct LiveSolve {
154 pub good: Option<std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>>,
157 pub held: bool,
159 pub skip: u32,
161 pub solves: u32,
163 pub skipped: u32,
164}
165
166#[derive(Clone)]
171pub(super) struct ComponentMoveDrag {
172 handle: u32,
173 sx: f32,
174 sy: f32,
175 start: ComponentPose,
176 members: Vec<(String, ComponentPose)>,
177 pending: Option<(TransformDelta, ComponentPose)>,
178 live: LiveSolve,
179}
180
181#[derive(Clone, Copy, PartialEq, Debug)]
184pub(super) struct ComponentPose {
185 pub translate: [f64; 3],
186 pub rotate_deg: [f64; 3],
187 pub anchor: [f64; 3],
188}
189
190impl EngineState {
191 pub fn component_move_armed(&self) -> bool {
193 self.component_move.feature_id.is_some()
194 }
195
196 pub fn component_move_armed_feature(&self) -> String {
198 self.component_move.feature_id.clone().unwrap_or_default()
199 }
200
201 pub fn component_move_group(&self) -> Vec<String> {
203 self.component_move.group.clone()
204 }
205
206 pub fn component_move_group_toggle(&mut self, ids: &[String]) {
213 let mut unique: Vec<String> = Vec::new();
214 for id in ids {
215 if !unique.contains(id) {
216 unique.push(id.clone());
217 }
218 }
219 if unique.len() <= 1 {
220 if let Some(id) = unique.first() {
221 self.component_move_toggle(id);
222 }
223 return;
224 }
225 let mut movable = Vec::new();
226 let mut fixed = Vec::new();
227 for id in unique {
228 match self.component_info(&id) {
229 None => self.push_notice(format!("'{id}' is not an assembly component")),
230 Some(info) if info.fixed => fixed.push(format!("{} ({id})", info.part_name)),
231 Some(_) => movable.push(id),
232 }
233 }
234 if !fixed.is_empty() {
235 let verb = if fixed.len() == 1 { "is" } else { "are" };
236 self.push_notice(format!(
237 "{} {verb} fixed — not moved (unfix to move)",
238 fixed.join(", ")
239 ));
240 }
241 match movable.len() {
242 0 => {}
243 1 => self.component_move_toggle(&movable[0]),
244 _ if self.component_move.group.len() == movable.len()
246 && movable.iter().all(|id| self.component_move.group.contains(id)) =>
247 {
248 self.disarm_transform()
249 }
250 _ => self.component_move_arm_group(movable),
251 }
252 }
253
254 pub fn component_move_unfix_toggle(&mut self, ids: &[String]) {
260 let mut members: Vec<String> = Vec::new();
261 let mut unfix = Vec::new();
262 for id in ids {
263 if members.contains(id) {
264 continue;
265 }
266 match self.component_info(id) {
267 None => self.push_notice(format!("'{id}' is not an assembly component")),
268 Some(info) => {
269 if info.fixed {
270 unfix.push(id.clone());
271 }
272 members.push(id.clone());
273 }
274 }
275 }
276 if members.is_empty() {
277 return;
278 }
279 let same = self.component_move.group.len() == members.len()
280 && members.iter().all(|id| self.component_move.group.contains(id))
281 && self.component_move.unfix.len() == unfix.len();
282 if same {
283 self.disarm_transform();
284 return;
285 }
286 self.component_move_arm_group(members);
287 self.component_move.unfix = unfix;
288 }
289
290 pub fn component_move_toggle(&mut self, feature_id: &str) {
295 let Some(info) = self.component_info(feature_id) else {
296 self.push_notice(format!("'{feature_id}' is not an assembly component"));
297 return;
298 };
299 if info.fixed {
300 self.push_notice(format!(
301 "{} ({feature_id}) is fixed — unfix it to move",
302 info.part_name
303 ));
304 return;
305 }
306 if self.component_move.group == [feature_id] {
308 self.disarm_transform();
309 } else {
310 self.component_move_arm_widget(feature_id);
311 }
312 }
313
314 pub fn component_move_copy(&mut self, source_id: &str) -> Result<String, String> {
323 let Some(index) = self.history.index_of(source_id).filter(|&index| {
324 matches!(
325 self.history.feature_type(index).as_deref(),
326 Some("ACOMP") | Some("ASSEMBLY COMPONENT")
327 )
328 }) else {
329 let error = format!("'{source_id}' is not an assembly component");
330 self.push_notice(error.clone());
331 return Err(error);
332 };
333 let mut params = self
334 .history
335 .feature_params(index)
336 .unwrap_or_else(|| serde_json::json!({}));
337 let id = self.history.next_feature_id("ACOMP");
338 let Some(map) = params.as_object_mut() else {
339 return Err(format!("component '{source_id}': malformed inputParams"));
340 };
341 map.insert("id".into(), serde_json::Value::String(id.clone()));
342 map.insert("isFixed".into(), serde_json::Value::Bool(false));
343 let feature = serde_json::json!({
344 "type": "ACOMP",
345 "inputParams": params,
346 "persistentData": {}
347 });
348 self.add_feature(&feature.to_string())?;
349 self.component_move_arm_widget(&id);
352 if self.run_pending() {
353 self.component_move.select_after_run = true;
354 } else {
355 self.select_component(&id);
356 }
357 Ok(id)
358 }
359
360 pub(super) fn component_move_arm(&mut self, feature_id: &str) {
364 match self.component_info(feature_id) {
365 Some(info) if !info.fixed => self.component_move_arm_widget(feature_id),
366 _ => {}
367 }
368 }
369
370 pub(super) fn component_move_reset(&mut self) {
374 self.component_live_restore();
375 let budget_ms = self.component_move.budget_ms;
376 let solve_cost_override_ms = self.component_move.solve_cost_override_ms;
377 self.component_move =
378 ComponentMoveArm { budget_ms, solve_cost_override_ms, ..ComponentMoveArm::default() };
379 }
380
381 fn component_move_arm_widget(&mut self, feature_id: &str) {
385 self.component_move_arm_group(vec![feature_id.to_string()]);
386 }
387
388 fn component_move_arm_group(&mut self, group: Vec<String>) {
390 self.transform_gizmo.feature_id = None;
393 self.transform_gizmo.mode = GizmoMode::None;
394 self.transform_gizmo.drag = None;
395 self.clear_feature_dimension_overlay();
396
397 self.component_live_restore();
398 let anchor = self.component_group_anchor(&group);
399 self.component_move.feature_id = group.first().cloned();
400 self.component_move.group = group;
401 self.component_move.unfix.clear();
402 self.component_move.select_after_run = false;
403 self.component_move.anchor = anchor;
404 self.component_move.drag = None;
405 self.component_move.typed = None;
406 self.feed_component_widget();
407 self.dirty = true;
408 }
409
410 fn component_group_anchor(&self, group: &[String]) -> [f64; 3] {
413 let mut bbox = crate::camera::Aabb::empty();
414 for id in group {
415 for solid in self.scene.solids() {
416 if super::components::owned_by(&solid.name, id) {
417 bbox.union(&solid.bbox);
418 }
419 }
420 }
421 if !bbox.is_empty() {
422 return bbox.center();
423 }
424 group
425 .first()
426 .and_then(|id| self.component_info(id))
427 .map(|info| info.translate)
428 .unwrap_or([0.0; 3])
429 }
430
431 fn component_move_frame(&self) -> Option<ComponentPose> {
434 let anchor = self.component_move.anchor;
435 if self.component_move.group.len() > 1 {
436 return Some(ComponentPose { translate: anchor, rotate_deg: [0.0; 3], anchor });
437 }
438 let info = self.component_info(self.component_move.feature_id.as_deref()?)?;
439 Some(ComponentPose { translate: info.translate, rotate_deg: info.rotate_deg, anchor })
440 }
441
442 fn feed_component_widget(&mut self) {
444 let Some(pose) = self.component_move_frame() else {
445 return;
446 };
447 let json = component_frame_json(&pose);
448 let _ = self.widgets.set_transform_json(&json);
449 }
450
451 pub(super) fn component_move_sync(&mut self) {
455 if self.component_move.feature_id.is_none() {
456 return;
457 }
458 let unfix = &self.component_move.unfix;
461 let group: Vec<String> = self
462 .component_move
463 .group
464 .iter()
465 .filter(|id| {
466 self.component_info(id)
467 .is_some_and(|info| !info.fixed || unfix.contains(id))
468 })
469 .cloned()
470 .collect();
471 let Some(id) = group.first().cloned() else {
472 self.disarm_transform();
473 return;
474 };
475 if self.component_move.select_after_run && !self.run_pending() {
478 self.component_move.select_after_run = false;
479 self.select_component(&id);
480 }
481 self.component_move.anchor = self.component_group_anchor(&group);
482 self.component_move.feature_id = Some(id);
483 self.component_move.unfix.retain(|id| group.contains(id));
484 self.component_move.group = group;
485 self.feed_component_widget();
486 self.dirty = true;
487 }
488
489 pub fn component_press(&mut self, x: f64, y: f64) -> bool {
492 let Some(start) = self.component_move_frame() else {
493 return false;
494 };
495 let handle = self.transform_pick(x, y);
496 if handle == 0 {
497 return false;
498 }
499 let Some(members) = self.component_move_members() else {
500 return false;
501 };
502 self.widgets.set_transform_active(handle);
503 self.component_move.live_applied.clear();
504 self.component_move.drag = Some(ComponentMoveDrag {
505 handle,
506 sx: x as f32,
507 sy: y as f32,
508 start,
509 members,
510 pending: None,
511 live: LiveSolve::default(),
512 });
513 self.component_move.typed = None;
515 if self.history_has_assembly() {
517 self.ensure_assembly_session();
518 }
519 self.dirty = true;
520 true
521 }
522
523 fn component_move_members(&self) -> Option<Vec<(String, ComponentPose)>> {
526 self.component_move
527 .group
528 .iter()
529 .map(|id| {
530 let info = self.component_info(id)?;
531 let pose = ComponentPose {
532 translate: info.translate,
533 rotate_deg: info.rotate_deg,
534 anchor: self.component_move.anchor,
535 };
536 Some((id.clone(), pose))
537 })
538 .collect()
539 }
540
541 pub fn component_move_typed(&mut self, field: &str, value: f64) -> Result<TypedMove, String> {
550 let (axis, rotate) = typed_axis(field).ok_or_else(|| format!("unknown move field {field:?}"))?;
551 if !value.is_finite() {
552 return Err(format!("{value} is not a number"));
553 }
554 if self.component_move.drag.is_some() {
555 return Err("a drag is in progress".into());
556 }
557 let frame = self.component_move_frame().ok_or("nothing is armed to move")?;
558 let members = self.component_move_members().ok_or("an armed component is gone")?;
559 let mut unit = [0.0; 3];
560 unit[axis] = 1.0;
561 let direction = normalize3(quat_rotate(quat_from_euler_xyz_deg(frame.rotate_deg), unit));
562 let delta = if rotate {
563 TransformDelta::Rotate { axis: direction, radians: value.to_radians() }
564 } else {
565 TransformDelta::Translate([direction[0] * value, direction[1] * value, direction[2] * value])
566 };
567 if self.history_has_assembly() {
568 self.ensure_assembly_session();
569 }
570 let (solved, blocking) = match self.component_solve_explained(&members, &delta, None, true) {
571 Ok(result) => result,
572 Err(error) => {
573 let report = TypedMove { field: field.into(), asked: value, applied: 0.0, blocking: Vec::new(), written: false };
574 self.push_notice(format!("Move: the mates cannot hold any pose for this move ({error})."));
575 self.component_move.typed = Some(report.clone());
576 return Ok(report);
577 }
578 };
579 let (primary, start) = members.first().cloned().ok_or("nothing is armed to move")?;
580 let landed = solved.get(&primary).copied().unwrap_or((start.translate, start.rotate_deg));
581 let applied = if rotate {
582 let q_start = quat_from_euler_xyz_deg(start.rotate_deg);
583 let q = quat_mul(quat_from_euler_xyz_deg(landed.1), [-q_start[0], -q_start[1], -q_start[2], q_start[3]]);
584 let along = q[0] * direction[0] + q[1] * direction[1] + q[2] * direction[2];
585 let mut degrees = (2.0 * along.atan2(q[3])).to_degrees();
586 if degrees > 180.0 {
587 degrees -= 360.0;
588 } else if degrees < -180.0 {
589 degrees += 360.0;
590 }
591 degrees
592 } else {
593 let moved = sub3(rigid_delta_point(&start, landed, start.anchor), start.anchor);
594 moved[0] * direction[0] + moved[1] * direction[1] + moved[2] * direction[2]
595 };
596 let stuck = applied.abs() <= TYPED_TOLERANCE * value.abs().max(1.0);
597 let written = !stuck && self.component_commit_solved(&members, &solved, frame.anchor);
598 if !written {
599 self.feed_component_widget();
600 }
601 let report = TypedMove { field: field.into(), asked: value, applied: if written { applied } else { 0.0 }, blocking, written };
602 if !written {
603 self.push_notice(format!("Move: {}", report.summary()));
604 }
605 self.component_move.typed = Some(report.clone());
606 self.dirty = true;
607 Ok(report)
608 }
609
610 pub fn component_move_typed_report(&self) -> Option<TypedMove> {
612 self.component_move.typed.clone()
613 }
614
615 pub fn component_move_dragging(&self) -> bool {
617 self.component_move.drag.is_some()
618 }
619
620 pub fn component_drag_to(&mut self, cx: f64, cy: f64) {
624 let Some(drag) = self.component_move.drag.clone() else {
625 return;
626 };
627 let cam = gizmo_camera(&self.camera);
628 let frame = component_frame_json(&drag.start);
629 let json = self.widgets.transform_drag_json_with_frame(
630 &cam,
631 &frame,
632 drag.handle,
633 drag.sx,
634 drag.sy,
635 cx as f32,
636 cy as f32,
637 );
638 let Some(delta) = parse_drag_delta(&json) else {
639 return;
640 };
641 let pending = compose_component_delta(&drag.start, &delta);
642 if let Some(live) = self.component_move.drag.as_mut() {
643 live.pending = Some((delta, pending));
644 }
645 let mut live = drag.live.clone();
646 if live.skip > 0 {
647 live.skip -= 1;
648 live.skipped += 1;
649 } else {
650 let started = now_ms();
651 let solved = self.component_drag_solve(&drag.members, &delta, live.good.as_ref());
652 let cost = self.component_move.solve_cost_override_ms.unwrap_or(now_ms() - started);
653 live.solves += 1;
654 match solved {
655 Ok(poses) => {
656 live.good = Some(poses);
657 live.held = false;
658 }
659 Err(_) => live.held = true,
660 }
661 let budget = self.component_move.budget_ms.max(1e-3);
662 if cost > budget {
663 live.skip = ((cost / budget).ceil() as u32).saturating_sub(1).min(DRAG_SOLVE_MAX_SKIP);
664 }
665 }
666 let frame = self.component_live_frame(&drag, &pending, &live);
670 let _ = self.widgets.set_transform_json(&component_frame_json(&frame));
671 self.component_live_pose_displays(&drag.members, &live);
672 if let Some(state) = self.component_move.drag.as_mut() {
673 state.live = live;
674 }
675 self.dirty = true;
676 }
677
678 fn component_drag_solve(
683 &mut self,
684 members: &[(String, ComponentPose)],
685 delta: &TransformDelta,
686 good: Option<&std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>>,
687 ) -> Result<std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>, String> {
688 self.component_solve_explained(members, delta, good, false).map(|(poses, _)| poses)
689 }
690
691 fn component_solve_explained(
695 &mut self,
696 members: &[(String, ComponentPose)],
697 delta: &TransformDelta,
698 good: Option<&std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>>,
699 explain: bool,
700 ) -> Result<(std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>, Vec<String>), String> {
701 let free: std::collections::BTreeMap<String, ([f64; 3], [f64; 3])> = members
702 .iter()
703 .map(|(id, start)| {
704 let pose = compose_component_delta(start, delta);
705 (id.clone(), (pose.translate, pose.rotate_deg))
706 })
707 .collect();
708 if !self.history_has_assembly() {
709 return Ok((free, Vec::new()));
710 }
711 let poses: serde_json::Map<String, serde_json::Value> = free
712 .iter()
713 .map(|(id, (translate, rotate_deg))| {
714 (id.clone(), serde_json::json!({ "translate": translate, "rotateEulerDeg": rotate_deg }))
715 })
716 .collect();
717 let reference: serde_json::Map<String, serde_json::Value> = good
720 .into_iter()
721 .flatten()
722 .map(|(id, (translate, rotate_deg))| {
723 (id.clone(), serde_json::json!({ "translate": translate, "rotateEulerDeg": rotate_deg }))
724 })
725 .collect();
726 let request = serde_json::json!({
727 "poses": poses,
728 "unfix": self.component_move.unfix,
729 "reference": reference,
730 "explain": explain,
731 });
732 let reply = brep_kernel::assembly_drag_solve_impl(&request.to_string())?;
733 let reply: serde_json::Value = serde_json::from_str(&reply).map_err(|error| error.to_string())?;
734 let mut solved = std::collections::BTreeMap::new();
735 for (id, pose) in reply.get("poses").and_then(|poses| poses.as_object()).into_iter().flatten() {
736 let triple = |key: &str| -> Option<[f64; 3]> {
737 let values = pose.get(key)?.as_array()?;
738 Some([values.first()?.as_f64()?, values.get(1)?.as_f64()?, values.get(2)?.as_f64()?])
739 };
740 if let (Some(translate), Some(rotate_deg)) = (triple("translate"), triple("rotateEulerDeg")) {
741 solved.insert(id.clone(), (translate, rotate_deg));
742 }
743 }
744 let blocking = reply
745 .get("blocking")
746 .and_then(|ids| ids.as_array())
747 .into_iter()
748 .flatten()
749 .filter_map(|id| id.as_str().map(str::to_string))
750 .collect();
751 Ok((solved, blocking))
752 }
753
754 fn component_live_frame(
758 &self,
759 drag: &ComponentMoveDrag,
760 pending: &ComponentPose,
761 live: &LiveSolve,
762 ) -> ComponentPose {
763 let Some((id, start)) = drag.members.first() else {
764 return *pending;
765 };
766 let Some(&(translate, rotate_deg)) = live.good.as_ref().and_then(|good| good.get(id)) else {
767 return *pending;
768 };
769 let anchor = rigid_delta_point(start, (translate, rotate_deg), start.anchor);
770 if drag.members.len() > 1 {
771 return ComponentPose { translate: anchor, rotate_deg: [0.0; 3], anchor };
772 }
773 ComponentPose { translate, rotate_deg, anchor }
774 }
775
776 fn component_live_pose_displays(&mut self, members: &[(String, ComponentPose)], live: &LiveSolve) {
779 let Some(good) = live.good.as_ref() else {
780 return;
781 };
782 for (id, &solved) in good {
783 let start = match members.iter().find(|(member, _)| member == id) {
785 Some((_, start)) => *start,
786 None => match self.component_info(id).map(|info| (info.translate, info.rotate_deg)).or_else(|| self.occurrence_world_pose(id)) {
787 Some((translate, rotate_deg)) => ComponentPose { translate, rotate_deg, anchor: [0.0; 3] },
788 None => continue,
789 },
790 };
791 let shown = self
794 .component_move
795 .live_applied
796 .get(id)
797 .copied()
798 .unwrap_or((start.translate, start.rotate_deg));
799 if same_pose(shown, solved) {
800 continue;
801 }
802 self.component_move.live_applied.insert(id.clone(), solved);
803 let names = if id.contains(':') {
806 let prefix = format!("{id}:");
807 self.scene.solids().iter().filter(|solid| solid.name.starts_with(&prefix)).map(|solid| solid.name.clone()).collect()
808 } else {
809 self.member_solid_names(id)
810 };
811 for name in names {
812 let Some(display) = self.scene.solid(&name) else { continue };
813 let original = self
814 .component_move
815 .live_originals
816 .entry(name.clone())
817 .or_insert_with(|| display.clone())
818 .clone();
819 if let Some(display) = self.scene.solid_mut(&name) {
820 *display = original;
821 pose_display_rigid(display, &start, solved);
822 }
823 }
824 }
825 self.dirty = true;
826 }
827
828 fn occurrence_world_pose(&self, path: &str) -> Option<([f64; 3], [f64; 3])> {
835 let (top, rest) = path.split_once(':')?;
836 let record = self.assembly_components().iter().find(|record| record.id == top)?;
837 let local = record.nested_poses.get(rest)?;
838 let (a, b) = (&record.transform, local);
839 let world: [f64; 16] = std::array::from_fn(|k| (0..4).map(|m| a[(k / 4) * 4 + m] * b[m * 4 + k % 4]).sum());
840 let pose = brep_kernel::transform_to_pose_params(&brep_kernel::AffineTransform::new(world).ok()?);
841 let triple = |key: &str| -> Option<[f64; 3]> {
842 let values = pose.get(key)?.as_array()?;
843 Some([values.first()?.as_f64()?, values.get(1)?.as_f64()?, values.get(2)?.as_f64()?])
844 };
845 Some((triple("translate")?, triple("rotateEulerDeg")?))
846 }
847
848 pub(super) fn component_live_restore(&mut self) {
850 self.component_move.live_applied.clear();
851 let originals = std::mem::take(&mut self.component_move.live_originals);
852 if originals.is_empty() {
853 return;
854 }
855 for (name, original) in originals {
856 if let Some(display) = self.scene.solid_mut(&name) {
857 *display = original;
858 }
859 }
860 self.dirty = true;
861 }
862
863 pub fn component_release(&mut self) {
869 let Some(drag) = self.component_move.drag.take() else {
870 return;
871 };
872 self.widgets.set_transform_active(0);
873 self.dirty = true;
874 let Some((delta, frame)) = drag.pending else {
875 self.component_live_restore();
876 return;
877 };
878 let solved = match self.component_drag_solve(&drag.members, &delta, drag.live.good.as_ref()) {
882 Ok(poses) => poses,
883 Err(_) => match drag.live.good.clone() {
884 Some(poses) => poses,
885 None => {
886 self.component_live_restore();
887 self.component_move.unfix.clear();
888 self.feed_component_widget();
889 return;
890 }
891 },
892 };
893 let solved = self.snap_dragged_poses(solved);
894 self.component_commit_solved(&drag.members, &solved, frame.anchor);
895 }
896
897 fn snap_dragged_poses(
901 &self,
902 solved: std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>,
903 ) -> std::collections::BTreeMap<String, ([f64; 3], [f64; 3])> {
904 use crate::formatting::snap_drag_moved;
905 solved
906 .into_iter()
907 .map(|(id, (translate, rotate_deg))| {
908 let snapped = match self.component_info(&id) {
909 Some(info) => (
910 std::array::from_fn(|i| snap_drag_moved(info.translate[i], translate[i])),
911 std::array::from_fn(|i| snap_drag_moved(info.rotate_deg[i], rotate_deg[i])),
912 ),
913 None => (translate, rotate_deg),
914 };
915 (id, snapped)
916 })
917 .collect()
918 }
919
920 fn component_commit_solved(
925 &mut self,
926 members: &[(String, ComponentPose)],
927 solved: &std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>,
928 fallback_anchor: [f64; 3],
929 ) -> bool {
930 self.component_move.anchor = match members.first() {
931 Some((id, start)) => match solved.get(id) {
932 Some(&pose) => rigid_delta_point(start, pose, start.anchor),
933 None => fallback_anchor,
934 },
935 None => fallback_anchor,
936 };
937 let mut edits = Vec::new();
938 for (id, &(translate, rotate_deg)) in solved {
939 let Some(index) = self.history.index_of(id) else {
940 continue;
941 };
942 let unfix = self.component_move.unfix.contains(id);
946 match self.component_info(id) {
947 Some(info) if !unfix && !pose_moved((info.translate, info.rotate_deg), (translate, rotate_deg)) => continue,
948 None => continue,
949 _ => {}
950 }
951 let pose = ComponentPose { translate, rotate_deg, anchor: [0.0; 3] };
952 let mut params = self
953 .history
954 .feature_params(index)
955 .unwrap_or_else(|| serde_json::json!({}));
956 if !params.get("transform").map(|t| t.is_object()).unwrap_or(false) {
957 if let Some(object) = params.as_object_mut() {
958 object.insert("transform".into(), serde_json::json!({}));
959 }
960 }
961 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
962 transform.insert("translate".into(), serde_json::json!(pose.translate));
963 transform.insert("rotateEulerDeg".into(), serde_json::json!(pose.rotate_deg));
964 }
965 if self.component_move.unfix.contains(id) {
966 if let Some(object) = params.as_object_mut() {
967 object.insert("isFixed".into(), serde_json::Value::Bool(false));
968 }
969 }
970 edits.push((id.clone(), params));
971 }
972 self.component_move.unfix.clear();
974 if edits.is_empty() {
975 self.component_live_restore();
976 return false;
977 }
978 self.history.break_coalescing();
987 if let Some(block) = self.mechanical_block_at(solved) {
988 let resolved: Vec<(usize, serde_json::Value)> = edits
989 .iter()
990 .filter_map(|(id, params)| Some((self.history.index_of(id)?, params.clone())))
991 .collect();
992 self.history.set_many_feature_params(&resolved);
995 self.history.set_assembly_block(block, false);
996 self.history.break_coalescing();
997 let _ = self.rerun_history();
998 return true;
999 }
1000 match edits.as_slice() {
1001 [] => {}
1002 [(id, params)] => {
1003 let _ = self.update_feature_params(id, ¶ms.to_string());
1004 }
1005 _ => {
1006 let _ = self.update_many_feature_params(&edits);
1007 }
1008 }
1009 self.history.break_coalescing();
1010 true
1011 }
1012
1013 fn mechanical_block_at(
1017 &mut self,
1018 solved: &std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>,
1019 ) -> Option<serde_json::Value> {
1020 if !self.history_has_assembly() {
1021 return None;
1022 }
1023 let poses: serde_json::Map<String, serde_json::Value> = solved
1024 .iter()
1025 .map(|(id, (translate, rotate_deg))| {
1026 (id.clone(), serde_json::json!({ "translate": translate, "rotateEulerDeg": rotate_deg }))
1027 })
1028 .collect();
1029 let reply = brep_kernel::assembly_rebase_mechanical_impl(&serde_json::json!({ "poses": poses }).to_string()).ok()?;
1030 serde_json::from_str::<serde_json::Value>(&reply).ok().filter(|block| !block.is_null())
1031 }
1032
1033 pub fn component_move_json(&self) -> String {
1036 serde_json::json!({
1037 "armed": self.component_move_armed(),
1038 "feature": self.component_move_armed_feature(),
1039 "group": self.component_move.group,
1040 "unfix": self.component_move.unfix,
1041 "anchor": self.component_move.anchor,
1042 "typed": self.component_move.typed.as_ref().map(|typed| serde_json::json!({
1043 "field": typed.field,
1044 "asked": typed.asked,
1045 "applied": typed.applied,
1046 "blocking": typed.blocking,
1047 "written": typed.written,
1048 "summary": typed.summary(),
1049 })),
1050 "live": self.component_move.drag.as_ref().map(|drag| {
1051 let poses: serde_json::Map<String, serde_json::Value> = drag
1052 .live
1053 .good
1054 .iter()
1055 .flatten()
1056 .map(|(id, (translate, rotate_deg))| {
1057 (id.clone(), serde_json::json!({ "translate": translate, "rotateEulerDeg": rotate_deg }))
1058 })
1059 .collect();
1060 serde_json::json!({
1061 "poses": poses,
1062 "held": drag.live.held,
1063 "solves": drag.live.solves,
1064 "skipped": drag.live.skipped,
1065 })
1066 }),
1067 })
1068 .to_string()
1069 }
1070}
1071
1072fn component_frame_json(pose: &ComponentPose) -> String {
1077 let euler = [
1078 pose.rotate_deg[0].to_radians(),
1079 pose.rotate_deg[1].to_radians(),
1080 pose.rotate_deg[2].to_radians(),
1081 ];
1082 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
1083 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
1084 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
1085 serde_json::json!({
1086 "origin": pose.anchor,
1087 "x": x,
1088 "y": y,
1089 "z": z,
1090 "showCenter": true,
1091 "showAxes": true,
1092 "showRings": true,
1093 })
1094 .to_string()
1095}
1096
1097pub(super) fn compose_component_delta(
1100 start: &ComponentPose,
1101 delta: &TransformDelta,
1102) -> ComponentPose {
1103 match delta {
1104 TransformDelta::Translate(d) => ComponentPose {
1105 translate: add3(start.translate, *d),
1106 rotate_deg: start.rotate_deg,
1107 anchor: add3(start.anchor, *d),
1108 },
1109 TransformDelta::Rotate { axis, radians } => {
1110 let dq = quat_from_axis_angle(*axis, *radians);
1111 let q0 = quat_from_euler_xyz_deg(start.rotate_deg);
1112 let rotate_deg = euler_xyz_deg_from_quat(quat_mul(dq, q0));
1113 let offset = sub3(start.translate, start.anchor);
1114 ComponentPose {
1115 translate: add3(start.anchor, quat_rotate(dq, offset)),
1116 rotate_deg,
1117 anchor: start.anchor,
1118 }
1119 }
1120 }
1121}
1122
1123fn rigid_delta_point(start: &ComponentPose, to: ([f64; 3], [f64; 3]), point: [f64; 3]) -> [f64; 3] {
1126 let q_start = quat_from_euler_xyz_deg(start.rotate_deg);
1127 let q_to = quat_from_euler_xyz_deg(to.1);
1128 let q_delta = quat_mul(q_to, [-q_start[0], -q_start[1], -q_start[2], q_start[3]]);
1129 add3(to.0, quat_rotate(q_delta, sub3(point, start.translate)))
1130}
1131
1132fn pose_moved(a: ([f64; 3], [f64; 3]), b: ([f64; 3], [f64; 3])) -> bool {
1136 (0..3).any(|i| (a.0[i] - b.0[i]).abs() > 1e-6 || (a.1[i] - b.1[i]).abs() > 1e-6)
1137}
1138
1139fn same_pose(a: ([f64; 3], [f64; 3]), b: ([f64; 3], [f64; 3])) -> bool {
1143 (0..3).all(|k| (a.0[k] - b.0[k]).abs() <= 1e-9 && (a.1[k] - b.1[k]).abs() <= 1e-9)
1144}
1145
1146fn pose_display_rigid(display: &mut crate::scene::SolidDisplay, start: &ComponentPose, to: ([f64; 3], [f64; 3])) {
1147 if display.shape_revision == 0 {
1151 display.shape_revision = display.revision;
1152 }
1153 display.pose = None;
1155 display.revision = crate::scene::next_revision();
1156 let q_start = quat_from_euler_xyz_deg(start.rotate_deg);
1157 let q_delta = quat_mul(quat_from_euler_xyz_deg(to.1), [-q_start[0], -q_start[1], -q_start[2], q_start[3]]);
1158 let pose = |p: [f64; 3]| add3(to.0, quat_rotate(q_delta, sub3(p, start.translate)));
1159 let mut bbox = crate::camera::Aabb::empty();
1160 for position in &mut display.mesh.positions {
1161 let posed = pose(position.map(f64::from));
1162 *position = posed.map(|v| v as f32);
1163 bbox.expand(posed);
1164 }
1165 for normal in &mut display.mesh.normals {
1166 *normal = quat_rotate(q_delta, normal.map(f64::from)).map(|v| v as f32);
1167 }
1168 for edge in &mut display.edges {
1169 for point in &mut edge.polyline {
1170 let posed = pose(point.map(f64::from));
1171 *point = posed.map(|v| v as f32);
1172 bbox.expand(posed);
1173 }
1174 }
1175 for vertex in &mut display.vertices {
1176 vertex.position = pose(vertex.position);
1177 bbox.expand(vertex.position);
1178 }
1179 if !bbox.is_empty() {
1180 display.bbox = bbox;
1181 }
1182}
1183
1184#[cfg(not(target_arch = "wasm32"))]
1188pub(super) fn now_ms() -> f64 {
1189 static ORIGIN: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
1190 ORIGIN.get_or_init(std::time::Instant::now).elapsed().as_secs_f64() * 1000.0
1191}
1192
1193#[cfg(target_arch = "wasm32")]
1194pub(super) fn now_ms() -> f64 {
1195 js_sys::Date::now()
1196}
1197
1198pub(super) fn quat_rotate(q: Quat, v: [f64; 3]) -> [f64; 3] {
1200 let u = [q[0], q[1], q[2]];
1201 let w = q[3];
1202 let t = cross(u, add3(cross(u, v), [w * v[0], w * v[1], w * v[2]]));
1203 [v[0] + 2.0 * t[0], v[1] + 2.0 * t[1], v[2] + 2.0 * t[2]]
1204}
1205