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 self.component_commit_solved(&drag.members, &solved, frame.anchor);
894 }
895
896 fn component_commit_solved(
901 &mut self,
902 members: &[(String, ComponentPose)],
903 solved: &std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>,
904 fallback_anchor: [f64; 3],
905 ) -> bool {
906 self.component_move.anchor = match members.first() {
907 Some((id, start)) => match solved.get(id) {
908 Some(&pose) => rigid_delta_point(start, pose, start.anchor),
909 None => fallback_anchor,
910 },
911 None => fallback_anchor,
912 };
913 let mut edits = Vec::new();
914 for (id, &(translate, rotate_deg)) in solved {
915 let Some(index) = self.history.index_of(id) else {
916 continue;
917 };
918 let unfix = self.component_move.unfix.contains(id);
922 match self.component_info(id) {
923 Some(info) if !unfix && !pose_moved((info.translate, info.rotate_deg), (translate, rotate_deg)) => continue,
924 None => continue,
925 _ => {}
926 }
927 let pose = ComponentPose { translate, rotate_deg, anchor: [0.0; 3] };
928 let mut params = self
929 .history
930 .feature_params(index)
931 .unwrap_or_else(|| serde_json::json!({}));
932 if !params.get("transform").map(|t| t.is_object()).unwrap_or(false) {
933 if let Some(object) = params.as_object_mut() {
934 object.insert("transform".into(), serde_json::json!({}));
935 }
936 }
937 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
938 transform.insert("translate".into(), serde_json::json!(pose.translate));
939 transform.insert("rotateEulerDeg".into(), serde_json::json!(pose.rotate_deg));
940 }
941 if self.component_move.unfix.contains(id) {
942 if let Some(object) = params.as_object_mut() {
943 object.insert("isFixed".into(), serde_json::Value::Bool(false));
944 }
945 }
946 edits.push((id.clone(), params));
947 }
948 self.component_move.unfix.clear();
950 if edits.is_empty() {
951 self.component_live_restore();
952 return false;
953 }
954 self.history.break_coalescing();
963 if let Some(block) = self.mechanical_block_at(solved) {
964 let resolved: Vec<(usize, serde_json::Value)> = edits
965 .iter()
966 .filter_map(|(id, params)| Some((self.history.index_of(id)?, params.clone())))
967 .collect();
968 self.history.set_many_feature_params(&resolved);
971 self.history.set_assembly_block(block, false);
972 self.history.break_coalescing();
973 let _ = self.rerun_history();
974 return true;
975 }
976 match edits.as_slice() {
977 [] => {}
978 [(id, params)] => {
979 let _ = self.update_feature_params(id, ¶ms.to_string());
980 }
981 _ => {
982 let _ = self.update_many_feature_params(&edits);
983 }
984 }
985 self.history.break_coalescing();
986 true
987 }
988
989 fn mechanical_block_at(
993 &mut self,
994 solved: &std::collections::BTreeMap<String, ([f64; 3], [f64; 3])>,
995 ) -> Option<serde_json::Value> {
996 if !self.history_has_assembly() {
997 return None;
998 }
999 let poses: serde_json::Map<String, serde_json::Value> = solved
1000 .iter()
1001 .map(|(id, (translate, rotate_deg))| {
1002 (id.clone(), serde_json::json!({ "translate": translate, "rotateEulerDeg": rotate_deg }))
1003 })
1004 .collect();
1005 let reply = brep_kernel::assembly_rebase_mechanical_impl(&serde_json::json!({ "poses": poses }).to_string()).ok()?;
1006 serde_json::from_str::<serde_json::Value>(&reply).ok().filter(|block| !block.is_null())
1007 }
1008
1009 pub fn component_move_json(&self) -> String {
1012 serde_json::json!({
1013 "armed": self.component_move_armed(),
1014 "feature": self.component_move_armed_feature(),
1015 "group": self.component_move.group,
1016 "unfix": self.component_move.unfix,
1017 "anchor": self.component_move.anchor,
1018 "typed": self.component_move.typed.as_ref().map(|typed| serde_json::json!({
1019 "field": typed.field,
1020 "asked": typed.asked,
1021 "applied": typed.applied,
1022 "blocking": typed.blocking,
1023 "written": typed.written,
1024 "summary": typed.summary(),
1025 })),
1026 "live": self.component_move.drag.as_ref().map(|drag| {
1027 let poses: serde_json::Map<String, serde_json::Value> = drag
1028 .live
1029 .good
1030 .iter()
1031 .flatten()
1032 .map(|(id, (translate, rotate_deg))| {
1033 (id.clone(), serde_json::json!({ "translate": translate, "rotateEulerDeg": rotate_deg }))
1034 })
1035 .collect();
1036 serde_json::json!({
1037 "poses": poses,
1038 "held": drag.live.held,
1039 "solves": drag.live.solves,
1040 "skipped": drag.live.skipped,
1041 })
1042 }),
1043 })
1044 .to_string()
1045 }
1046}
1047
1048fn component_frame_json(pose: &ComponentPose) -> String {
1053 let euler = [
1054 pose.rotate_deg[0].to_radians(),
1055 pose.rotate_deg[1].to_radians(),
1056 pose.rotate_deg[2].to_radians(),
1057 ];
1058 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
1059 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
1060 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
1061 serde_json::json!({
1062 "origin": pose.anchor,
1063 "x": x,
1064 "y": y,
1065 "z": z,
1066 "showCenter": true,
1067 "showAxes": true,
1068 "showRings": true,
1069 })
1070 .to_string()
1071}
1072
1073pub(super) fn compose_component_delta(
1076 start: &ComponentPose,
1077 delta: &TransformDelta,
1078) -> ComponentPose {
1079 match delta {
1080 TransformDelta::Translate(d) => ComponentPose {
1081 translate: add3(start.translate, *d),
1082 rotate_deg: start.rotate_deg,
1083 anchor: add3(start.anchor, *d),
1084 },
1085 TransformDelta::Rotate { axis, radians } => {
1086 let dq = quat_from_axis_angle(*axis, *radians);
1087 let q0 = quat_from_euler_xyz_deg(start.rotate_deg);
1088 let rotate_deg = euler_xyz_deg_from_quat(quat_mul(dq, q0));
1089 let offset = sub3(start.translate, start.anchor);
1090 ComponentPose {
1091 translate: add3(start.anchor, quat_rotate(dq, offset)),
1092 rotate_deg,
1093 anchor: start.anchor,
1094 }
1095 }
1096 }
1097}
1098
1099fn rigid_delta_point(start: &ComponentPose, to: ([f64; 3], [f64; 3]), point: [f64; 3]) -> [f64; 3] {
1102 let q_start = quat_from_euler_xyz_deg(start.rotate_deg);
1103 let q_to = quat_from_euler_xyz_deg(to.1);
1104 let q_delta = quat_mul(q_to, [-q_start[0], -q_start[1], -q_start[2], q_start[3]]);
1105 add3(to.0, quat_rotate(q_delta, sub3(point, start.translate)))
1106}
1107
1108fn pose_moved(a: ([f64; 3], [f64; 3]), b: ([f64; 3], [f64; 3])) -> bool {
1112 (0..3).any(|i| (a.0[i] - b.0[i]).abs() > 1e-6 || (a.1[i] - b.1[i]).abs() > 1e-6)
1113}
1114
1115fn same_pose(a: ([f64; 3], [f64; 3]), b: ([f64; 3], [f64; 3])) -> bool {
1119 (0..3).all(|k| (a.0[k] - b.0[k]).abs() <= 1e-9 && (a.1[k] - b.1[k]).abs() <= 1e-9)
1120}
1121
1122fn pose_display_rigid(display: &mut crate::scene::SolidDisplay, start: &ComponentPose, to: ([f64; 3], [f64; 3])) {
1123 if display.shape_revision == 0 {
1127 display.shape_revision = display.revision;
1128 }
1129 display.pose = None;
1131 display.revision = crate::scene::next_revision();
1132 let q_start = quat_from_euler_xyz_deg(start.rotate_deg);
1133 let q_delta = quat_mul(quat_from_euler_xyz_deg(to.1), [-q_start[0], -q_start[1], -q_start[2], q_start[3]]);
1134 let pose = |p: [f64; 3]| add3(to.0, quat_rotate(q_delta, sub3(p, start.translate)));
1135 let mut bbox = crate::camera::Aabb::empty();
1136 for position in &mut display.mesh.positions {
1137 let posed = pose(position.map(f64::from));
1138 *position = posed.map(|v| v as f32);
1139 bbox.expand(posed);
1140 }
1141 for normal in &mut display.mesh.normals {
1142 *normal = quat_rotate(q_delta, normal.map(f64::from)).map(|v| v as f32);
1143 }
1144 for edge in &mut display.edges {
1145 for point in &mut edge.polyline {
1146 let posed = pose(point.map(f64::from));
1147 *point = posed.map(|v| v as f32);
1148 bbox.expand(posed);
1149 }
1150 }
1151 for vertex in &mut display.vertices {
1152 vertex.position = pose(vertex.position);
1153 bbox.expand(vertex.position);
1154 }
1155 if !bbox.is_empty() {
1156 display.bbox = bbox;
1157 }
1158}
1159
1160#[cfg(not(target_arch = "wasm32"))]
1164pub(super) fn now_ms() -> f64 {
1165 static ORIGIN: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
1166 ORIGIN.get_or_init(std::time::Instant::now).elapsed().as_secs_f64() * 1000.0
1167}
1168
1169#[cfg(target_arch = "wasm32")]
1170pub(super) fn now_ms() -> f64 {
1171 js_sys::Date::now()
1172}
1173
1174pub(super) fn quat_rotate(q: Quat, v: [f64; 3]) -> [f64; 3] {
1176 let u = [q[0], q[1], q[2]];
1177 let w = q[3];
1178 let t = cross(u, add3(cross(u, v), [w * v[0], w * v[1], w * v[2]]));
1179 [v[0] + 2.0 * t[0], v[1] + 2.0 * t[1], v[2] + 2.0 * t[2]]
1180}
1181