use crate::geometry3d::{add3, cross3 as cross, sub3};
use super::transform_gizmo::{
euler_xyz_deg_from_quat, normalize3, quat_from_axis_angle, quat_from_euler_xyz_deg, quat_mul,
parse_drag_delta, Quat, TransformDelta,
};
use super::*;
#[derive(Default)]
pub struct ComponentMoveArm {
pub(super) feature_id: Option<String>,
pub(super) anchor: [f64; 3],
pub(super) drag: Option<ComponentMoveDrag>,
}
#[derive(Clone)]
pub(super) struct ComponentMoveDrag {
handle: u32,
sx: f32,
sy: f32,
start: ComponentPose,
pending: Option<ComponentPose>,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(super) struct ComponentPose {
pub translate: [f64; 3],
pub rotate_deg: [f64; 3],
pub anchor: [f64; 3],
}
impl EngineState {
pub fn component_move_armed(&self) -> bool {
self.component_move.feature_id.is_some()
}
pub fn component_move_armed_feature(&self) -> String {
self.component_move.feature_id.clone().unwrap_or_default()
}
pub fn component_move_toggle(&mut self, feature_id: &str) {
let Some(info) = self.component_info(feature_id) else {
self.push_notice(format!("'{feature_id}' is not an assembly component"));
return;
};
if info.fixed {
self.push_notice(format!(
"{} ({feature_id}) is fixed — unfix it to move",
info.part_name
));
return;
}
if self.component_move.feature_id.as_deref() == Some(feature_id) {
self.disarm_transform();
} else {
self.component_move_arm_widget(feature_id);
}
}
pub(super) fn component_move_arm(&mut self, feature_id: &str) {
match self.component_info(feature_id) {
Some(info) if !info.fixed => self.component_move_arm_widget(feature_id),
_ => {}
}
}
pub(super) fn component_move_reset(&mut self) {
self.component_move = ComponentMoveArm::default();
}
fn component_move_arm_widget(&mut self, feature_id: &str) {
self.transform_gizmo.feature_id = None;
self.transform_gizmo.mode = GizmoMode::None;
self.transform_gizmo.drag = None;
self.clear_feature_dimension_overlay();
let anchor = self
.component_bbox_center(feature_id)
.or_else(|| self.component_info(feature_id).map(|info| info.translate))
.unwrap_or([0.0; 3]);
self.component_move.feature_id = Some(feature_id.to_string());
self.component_move.anchor = anchor;
self.component_move.drag = None;
self.feed_component_widget();
self.dirty = true;
}
fn feed_component_widget(&mut self) {
let Some(id) = self.component_move.feature_id.clone() else {
return;
};
let Some(info) = self.component_info(&id) else {
return;
};
let pose = ComponentPose {
translate: info.translate,
rotate_deg: info.rotate_deg,
anchor: self.component_move.anchor,
};
let json = component_frame_json(&pose);
let _ = self.widgets.set_transform_json(&json);
}
pub(super) fn component_move_sync(&mut self) {
let Some(id) = self.component_move.feature_id.clone() else {
return;
};
match self.component_info(&id) {
Some(info) if !info.fixed => {
self.component_move.anchor =
self.component_bbox_center(&id).unwrap_or(info.translate);
self.feed_component_widget();
self.dirty = true;
}
_ => self.disarm_transform(),
}
}
pub fn component_press(&mut self, x: f64, y: f64) -> bool {
let Some(id) = self.component_move.feature_id.clone() else {
return false;
};
let handle = self.transform_pick(x, y);
if handle == 0 {
return false;
}
let Some(info) = self.component_info(&id) else {
return false;
};
self.widgets.set_transform_active(handle);
self.component_move.drag = Some(ComponentMoveDrag {
handle,
sx: x as f32,
sy: y as f32,
start: ComponentPose {
translate: info.translate,
rotate_deg: info.rotate_deg,
anchor: self.component_move.anchor,
},
pending: None,
});
self.dirty = true;
true
}
pub fn component_move_dragging(&self) -> bool {
self.component_move.drag.is_some()
}
pub fn component_drag_to(&mut self, cx: f64, cy: f64) {
let Some(drag) = self.component_move.drag.clone() else {
return;
};
let cam = gizmo_camera(&self.camera);
let frame = component_frame_json(&drag.start);
let json = self.widgets.transform_drag_json_with_frame(
&cam,
&frame,
drag.handle,
drag.sx,
drag.sy,
cx as f32,
cy as f32,
);
let Some(delta) = parse_drag_delta(&json) else {
return;
};
let pending = compose_component_delta(&drag.start, &delta);
let json = component_frame_json(&pending);
let _ = self.widgets.set_transform_json(&json);
if let Some(live) = self.component_move.drag.as_mut() {
live.pending = Some(pending);
}
self.dirty = true;
}
pub fn component_release(&mut self) {
let Some(drag) = self.component_move.drag.take() else {
return;
};
self.widgets.set_transform_active(0);
self.dirty = true;
let Some(pending) = drag.pending else {
return;
};
let Some(id) = self.component_move.feature_id.clone() else {
return;
};
self.component_move.anchor = pending.anchor;
let Some(index) = self.history.index_of(&id) else {
return;
};
let mut params = self
.history
.feature_params(index)
.unwrap_or_else(|| serde_json::json!({}));
if !params.get("transform").map(|t| t.is_object()).unwrap_or(false) {
if let Some(object) = params.as_object_mut() {
object.insert("transform".into(), serde_json::json!({}));
}
}
if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
transform.insert("translate".into(), serde_json::json!(pending.translate));
transform.insert("rotateEulerDeg".into(), serde_json::json!(pending.rotate_deg));
}
let _ = self.update_feature_params(&id, ¶ms.to_string());
}
pub fn component_move_json(&self) -> String {
serde_json::json!({
"armed": self.component_move_armed(),
"feature": self.component_move_armed_feature(),
"anchor": self.component_move.anchor,
})
.to_string()
}
}
fn component_frame_json(pose: &ComponentPose) -> String {
let euler = [
pose.rotate_deg[0].to_radians(),
pose.rotate_deg[1].to_radians(),
pose.rotate_deg[2].to_radians(),
];
let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
serde_json::json!({
"origin": pose.anchor,
"x": x,
"y": y,
"z": z,
"showCenter": true,
"showAxes": true,
"showRings": true,
})
.to_string()
}
pub(super) fn compose_component_delta(
start: &ComponentPose,
delta: &TransformDelta,
) -> ComponentPose {
match delta {
TransformDelta::Translate(d) => ComponentPose {
translate: add3(start.translate, *d),
rotate_deg: start.rotate_deg,
anchor: add3(start.anchor, *d),
},
TransformDelta::Rotate { axis, radians } => {
let dq = quat_from_axis_angle(*axis, *radians);
let q0 = quat_from_euler_xyz_deg(start.rotate_deg);
let rotate_deg = euler_xyz_deg_from_quat(quat_mul(dq, q0));
let offset = sub3(start.translate, start.anchor);
ComponentPose {
translate: add3(start.anchor, quat_rotate(dq, offset)),
rotate_deg,
anchor: start.anchor,
}
}
}
}
fn quat_rotate(q: Quat, v: [f64; 3]) -> [f64; 3] {
let u = [q[0], q[1], q[2]];
let w = q[3];
let t = cross(u, add3(cross(u, v), [w * v[0], w * v[1], w * v[2]]));
[v[0] + 2.0 * t[0], v[1] + 2.0 * t[1], v[2] + 2.0 * t[2]]
}