use super::*;
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub enum GizmoMode {
#[default]
None,
Transform,
Dimension,
}
#[derive(Default)]
pub struct TransformArm {
pub(super) feature_id: Option<String>,
pub(super) mode: GizmoMode,
pub(super) drag: Option<TransformDrag>,
pub(super) anchor: Option<usize>,
pub(super) port: Option<String>,
}
#[derive(Clone, Copy)]
pub(super) struct TransformDrag {
handle: u32,
sx: f32,
sy: f32,
start: TransformPose,
last: Option<TransformDelta>,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(super) struct TransformPose {
pub(super) position: [f64; 3],
pub(super) rotation_deg: [f64; 3],
pub(super) scale: [f64; 3],
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(super) enum TransformDelta {
Translate([f64; 3]),
Rotate { axis: [f64; 3], radians: f64 },
}
pub(super) fn parse_drag_delta(json: &str) -> Option<TransformDelta> {
let value: serde_json::Value = serde_json::from_str(json).ok()?;
match value.get("kind").and_then(|k| k.as_str()) {
Some("translate") => Some(TransformDelta::Translate(
crate::json_support::vec3_or(value.get("world"), [0.0; 3]),
)),
Some("rotate") => Some(TransformDelta::Rotate {
axis: crate::json_support::vec3_or(value.get("axisWorld"), [0.0; 3]),
radians: value.get("radians").and_then(|n| n.as_f64()).unwrap_or(0.0),
}),
_ => None,
}
}
impl EngineState {
pub fn transform_armed(&self) -> bool {
matches!(self.transform_gizmo.mode, GizmoMode::Transform)
}
pub fn transform_armed_feature(&self) -> String {
if self.transform_armed() {
self.transform_gizmo.feature_id.clone().unwrap_or_default()
} else {
String::new()
}
}
pub fn gizmo_state_json(&self) -> String {
let mode = match self.transform_gizmo.mode {
GizmoMode::None => "none",
GizmoMode::Transform => "transform",
GizmoMode::Dimension => "dimension",
};
serde_json::json!({
"mode": mode,
"armed": self.transform_armed(),
"feature": self.transform_gizmo.feature_id,
"anchor": self.transform_gizmo.anchor,
"portPoint": self.transform_gizmo.port,
"origin": self.widgets.transform_origin(),
"pivot": self.armed_face_transform_pivot(),
})
.to_string()
}
pub fn armed_face_transform_pivot(&self) -> Option<[f64; 3]> {
let id = self.transform_gizmo.feature_id.as_deref()?;
let index = self.history.index_of(id)?;
if !self.is_face_transform_feature(index) {
return None;
}
let params = self.history.feature_params(index)?;
let env = brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).ok();
self.face_transform_pivot(index, ¶ms, env.as_ref())
}
pub fn transform_readout_json(&self) -> String {
let quiet = serde_json::json!({ "active": false }).to_string();
if !self.transform_armed() {
return quiet;
}
let Some(origin) = self.widgets.transform_origin() else {
return quiet;
};
let refusal = self.armed_face_transform_refusal();
let drag = self.transform_gizmo.drag.as_ref();
let delta = drag.and_then(|d| d.last);
if delta.is_none() && refusal.is_none() {
return quiet;
}
let (kind, label, value, unit, text) = match delta {
Some(TransformDelta::Translate(world)) => {
let distance = (world[0] * world[0] + world[1] * world[1] + world[2] * world[2]).sqrt();
let axis = dominant_axis_label(&world);
let shown = crate::formatting::compact_decimal(distance, 4);
(
"translate",
axis.to_string(),
distance,
"mm",
format!("{axis} {shown} mm"),
)
}
Some(TransformDelta::Rotate { axis, radians }) => {
let degrees = radians.to_degrees();
let label = dominant_axis_label(&axis);
let shown = crate::formatting::compact_decimal(degrees, 4);
(
"rotate",
label.to_string(),
degrees,
"deg",
format!("{label} {shown}\u{00b0}"),
)
}
None => ("none", String::new(), 0.0, "", String::new()),
};
let mut out = serde_json::json!({
"active": true,
"kind": kind,
"axis": label,
"value": value,
"unit": unit,
"text": text,
"origin": origin,
"dragging": drag.is_some(),
"refused": refusal.is_some(),
});
if let Some(refusal) = refusal {
out["motion"] = serde_json::json!(refusal.motion);
out["reason"] = serde_json::json!(refusal.reason);
out["hint"] = serde_json::json!(refusal.hint);
out["kernel"] = serde_json::json!(refusal.kernel);
}
out.to_string()
}
fn armed_face_transform_refusal(&self) -> Option<crate::face_transform_help::FaceTransformRefusal> {
let id = self.transform_gizmo.feature_id.as_deref()?;
let index = self.history.index_of(id)?;
if !self.is_face_transform_feature(index) {
return None;
}
let report: serde_json::Value = serde_json::from_str(&self.history_report).ok()?;
let prefix = format!("{id}: ");
let message = report
.get("featureErrors")?
.as_array()?
.iter()
.filter_map(serde_json::Value::as_str)
.find(|entry| entry.starts_with(&prefix))
.map(|entry| &entry[prefix.len()..])?;
let params = self.history.feature_params(index)?;
let faces = brep_kernel::reference_names(params.get("faces"));
let facts = crate::face_transform_help::RefusalFacts::from_report(&report, id);
Some(crate::face_transform_help::explain_refusal(message, facts, &faces))
}
pub fn transform_armed_for(&self, feature_id: &str) -> bool {
self.transform_armed() && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
}
pub fn arm_transform(&mut self, feature_id: &str) {
let is_acomp = self
.history
.index_of(feature_id)
.and_then(|index| self.history.feature_type(index))
.is_some_and(|ty| super::components::is_acomp_feature_type(&ty));
if is_acomp {
self.component_move_arm(feature_id);
return;
}
self.component_move_reset();
self.transform_gizmo.feature_id = Some(feature_id.to_string());
self.transform_gizmo.mode = GizmoMode::Transform;
self.transform_gizmo.drag = None;
self.transform_gizmo.anchor = None;
self.transform_gizmo.port = None;
self.clear_feature_dimension_overlay();
self.sync_transform_gizmo();
}
pub fn disarm_transform(&mut self) {
self.component_move_reset();
self.transform_gizmo.feature_id = None;
self.transform_gizmo.mode = GizmoMode::None;
self.transform_gizmo.drag = None;
self.transform_gizmo.anchor = None;
self.transform_gizmo.port = None;
let _ = self.widgets.set_transform_json("null");
self.clear_feature_dimension_overlay();
self.dirty = true;
}
fn armed_pose(&self) -> Option<TransformPose> {
if let Some(address) = self.transform_gizmo.port.clone() {
return self.port_point_pose(&address);
}
let id = self.transform_gizmo.feature_id.as_deref()?;
let index = self.history.index_of(id)?;
if let Some(anchor) = self.transform_gizmo.anchor {
let (position, rotation_deg) = self.spline_anchor_pose(index, anchor)?;
return Some(TransformPose {
position,
rotation_deg,
scale: [1.0, 1.0, 1.0],
});
}
let params = self.history.feature_params(index)?;
let transform = params.get("transform");
let env = brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator())
.ok();
let env = env.as_ref();
let pivot = if self.is_xform_feature(index) {
self.xform_pivot(index, ¶ms)
} else if self.is_face_transform_feature(index) {
self.face_transform_pivot(index, ¶ms, env)?
} else {
[0.0; 3]
};
let position = read_pose_vec3(env, transform, "position", [0.0; 3]);
Some(TransformPose {
position: std::array::from_fn(|axis| position[axis] + pivot[axis]),
rotation_deg: read_pose_vec3(env, transform, "rotationEuler", [0.0, 0.0, 0.0]),
scale: read_pose_vec3(env, transform, "scale", [1.0, 1.0, 1.0]),
})
}
fn is_xform_feature(&self, index: usize) -> bool {
self.history.feature_type(index).is_some_and(|ty| {
ty.eq_ignore_ascii_case("XFORM") || ty.eq_ignore_ascii_case("TRANSFORM")
})
}
pub fn is_face_transform_feature(&self, index: usize) -> bool {
self.history
.feature_type(index)
.is_some_and(|ty| is_face_transform_type(&ty))
}
fn face_transform_pivot(
&self,
index: usize,
params: &serde_json::Value,
env: Option<&brep_kernel::Env>,
) -> Option<[f64; 3]> {
match params.get("pivot") {
Some(pivot) if !pivot.is_null() => Some(read_pose_vec3(env, Some(params), "pivot", [0.0; 3])),
_ => self.face_transform_default_pivot(index, params),
}
}
pub fn face_transform_has_pivot(&self, feature_id: &str) -> bool {
let Some(index) = self.history.index_of(feature_id) else {
return false;
};
let Some(params) = self.history.feature_params(index) else {
return false;
};
self.is_face_transform_feature(index) && self.face_transform_pivot(index, ¶ms, None).is_some()
}
fn face_transform_default_pivot(
&self,
index: usize,
params: &serde_json::Value,
) -> Option<[f64; 3]> {
let names = brep_kernel::reference_names(params.get("faces"));
if names.is_empty() {
return None;
}
let request: HistoryRequest =
serde_json::from_value(self.history.request_before(index)?).ok()?;
let _trace = crate::run_trace::span("face_transform_pivot");
let result = brep_kernel::execute_history(&request);
brep_kernel::face_transform_pivot(&result.results, &names)
}
pub(crate) fn stamp_face_transform_pivot(&self, index: usize, params: &mut serde_json::Value) {
if !self.is_face_transform_feature(index) {
return;
}
let before = self.history.feature_params(index).unwrap_or(serde_json::Value::Null);
let faces_changed = brep_kernel::reference_names(before.get("faces"))
!= brep_kernel::reference_names(params.get("faces"));
let pivot_set_here = before.get("pivot") != params.get("pivot");
let unset = params.get("pivot").is_none_or(serde_json::Value::is_null);
if !(unset || (faces_changed && !pivot_set_here)) {
return;
}
let pivot = self.face_transform_default_pivot(index, params);
if let Some(object) = params.as_object_mut() {
object.insert(
"pivot".into(),
pivot.map_or(serde_json::Value::Null, |pivot| serde_json::json!(pivot)),
);
}
}
fn xform_pivot(&self, index: usize, params: &serde_json::Value) -> [f64; 3] {
if !params["pivot"]
.as_str()
.is_some_and(|p| p.eq_ignore_ascii_case("BBOX_CENTER"))
{
return [0.0; 3];
}
let Ok(request) = serde_json::from_value::<HistoryRequest>(self.run_request_value())
else {
return [0.0; 3];
};
let _trace = crate::run_trace::span("xform_pivot");
let result = brep_kernel::execute_history(&request);
let mut handles = HashMap::new();
for feature in result.results.iter().take(index) {
for name in &feature.removed {
handles.remove(name);
}
for solid in &feature.added {
handles.insert(solid.name.clone(), solid.handle);
}
}
let Some(refs) = params["solids"].as_array() else {
return [0.0; 3];
};
for reference in refs {
let Some(name) = reference.as_str().or_else(|| reference["name"].as_str()) else {
continue;
};
let Some(handle) = handles.get(name.trim()) else {
continue;
};
if let Ok(center) = brep_kernel::transform_pivot_native(*handle) {
return center;
}
}
[0.0; 3]
}
pub fn sync_transform_gizmo(&mut self) {
if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
return;
}
let Some(pose) = self.armed_pose() else {
self.disarm_transform();
return;
};
let _ = self
.widgets
.set_transform_json(&transform_frame_json(&pose));
self.dirty = true;
}
pub fn feature_has_transform(&self, feature_id: &str) -> bool {
self.history
.index_of(feature_id)
.and_then(|i| self.history.feature_type(i))
.and_then(|ty| crate::features::feature_schema(&ty))
.is_some_and(|schema| schema["inputParamsSchema"]["transform"]["type"] == "transform")
}
pub fn transform_gizmo_anchor(&self) -> Option<(f64, f64)> {
let pose = self.armed_pose()?;
let (sx, sy, _) = self.camera.project(pose.position);
self.camera.projectable(pose.position).then_some((sx, sy))
}
pub fn transform_axis_labels_json(&self) -> String {
if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
return "[]".to_string();
}
let Some(pose) = self.armed_pose() else {
return "[]".to_string();
};
let euler = [
pose.rotation_deg[0].to_radians(),
pose.rotation_deg[1].to_radians(),
pose.rotation_deg[2].to_radians(),
];
let axes = [
normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler)),
normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler)),
normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler)),
];
let gap_px = 12.0_f64;
let dist =
(brep_gizmos::transform::PX_AXIS_LEN as f64 + gap_px) * self.camera.world_per_pixel();
let labels: [(&str, [f32; 3]); 3] = [
("XC", [0.92, 0.26, 0.28]), ("YC", [0.30, 0.78, 0.36]), ("ZC", [0.30, 0.52, 0.98]), ];
let out: Vec<serde_json::Value> = (0..3)
.map(|i| {
let o = pose.position;
let a = axes[i];
serde_json::json!({
"text": labels[i].0,
"rgb": labels[i].1,
"world": [o[0] + a[0] * dist, o[1] + a[1] * dist, o[2] + a[2] * dist],
})
})
.collect();
serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
}
pub fn transform_handle_points(&self) -> Vec<(&'static str, (f64, f64))> {
use brep_gizmos::hit_region::HitShape;
use brep_gizmos::transform as gizmo;
let cam = gizmo_camera(&self.camera);
self.widgets
.transform_hit_regions(&cam)
.into_iter()
.filter_map(|(handle, shape)| {
let key = match handle {
gizmo::HANDLE_AXIS_X => "axis:x",
gizmo::HANDLE_AXIS_Y => "axis:y",
gizmo::HANDLE_AXIS_Z => "axis:z",
gizmo::HANDLE_RING_X => "ring:x",
gizmo::HANDLE_RING_Y => "ring:y",
gizmo::HANDLE_RING_Z => "ring:z",
_ => return None,
};
let [x, y] = match shape {
HitShape::Capsule { a, b, .. } => {
[a[0] + 0.75 * (b[0] - a[0]), a[1] + 0.75 * (b[1] - a[1])]
}
HitShape::Circle { c, .. } => c,
};
let point = (f64::from(x), f64::from(y));
(self.transform_pick(point.0, point.1) == handle).then_some((key, point))
})
.collect()
}
pub fn transform_hit_areas_json(&self) -> String {
let cam = gizmo_camera(&self.camera);
let regions = self.widgets.transform_hit_regions(&cam);
super::camera_widgets::hit_shapes_json(regions.iter().map(|(_, shape)| shape))
}
pub fn transform_press(&mut self, x: f64, y: f64) -> bool {
if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
return false;
}
let handle = self.transform_pick(x, y);
if handle == 0 {
return false;
}
let Some(pose) = self.armed_pose() else {
return false;
};
self.widgets.set_transform_active(handle);
self.transform_gizmo.drag = Some(TransformDrag {
handle,
sx: x as f32,
sy: y as f32,
start: pose,
last: None,
});
self.dirty = true;
true
}
pub fn transform_dragging(&self) -> bool {
self.transform_gizmo.drag.is_some()
}
pub fn transform_drag_to(&mut self, cx: f64, cy: f64) {
let Some(drag) = self.transform_gizmo.drag else {
return;
};
let Some(delta) = self.resolve_transform_delta(&drag, cx, cy) else {
return;
};
let pose = apply_transform_delta(&drag.start, &delta);
if let Some(live) = self.transform_gizmo.drag.as_mut() {
live.last = Some(delta);
}
self.write_armed_pose(&pose);
self.sync_transform_gizmo();
}
pub fn transform_release(&mut self) {
if self.transform_gizmo.drag.take().is_some() {
self.widgets.set_transform_active(0);
self.history.break_coalescing();
self.sync_transform_gizmo();
}
}
fn resolve_transform_delta(
&self,
drag: &TransformDrag,
cx: f64,
cy: f64,
) -> Option<TransformDelta> {
let cam = gizmo_camera(&self.camera);
let frame_json = transform_frame_json(&drag.start);
let json = self.widgets.transform_drag_json_with_frame(
&cam,
&frame_json,
drag.handle,
drag.sx,
drag.sy,
cx as f32,
cy as f32,
);
parse_drag_delta(&json)
}
fn write_armed_pose(&mut self, pose: &TransformPose) {
if let Some(address) = self.transform_gizmo.port.clone() {
self.write_port_point_pose(&address, pose);
return;
}
let Some(id) = self.transform_gizmo.feature_id.clone() else {
return;
};
let Some(index) = self.history.index_of(&id) else {
return;
};
if let Some(anchor) = self.transform_gizmo.anchor {
self.write_spline_anchor_pose(&id, anchor, pose.position, pose.rotation_deg);
return;
}
let mut params = self
.history
.feature_params(index)
.unwrap_or_else(|| serde_json::json!({}));
let pivot = if self.is_xform_feature(index) {
self.xform_pivot(index, ¶ms)
} else if self.is_face_transform_feature(index) {
let env =
brep_kernel::Env::build(&self.history.expressions(), &self.history.configurator()).ok();
let Some(pivot) = self.face_transform_pivot(index, ¶ms, env.as_ref()) else {
return;
};
pivot
} else {
[0.0; 3]
};
let position: [f64; 3] = std::array::from_fn(|axis| pose.position[axis] - pivot[axis]);
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("position".into(), serde_json::json!(position));
transform.insert("rotationEuler".into(), serde_json::json!(pose.rotation_deg));
}
let _ = self.update_feature_params(&id, ¶ms.to_string());
}
}
fn dominant_axis_label(v: &[f64; 3]) -> &'static str {
let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if length < 1e-12 {
return "\u{0394}";
}
const NAMES: [&str; 3] = ["X", "Y", "Z"];
for axis in 0..3 {
if v[axis].abs() >= length * 0.999 {
return NAMES[axis];
}
}
"\u{0394}"
}
pub fn is_face_transform_type(feature_type: &str) -> bool {
matches!(
feature_type.trim().to_ascii_uppercase().as_str(),
"TF" | "TRANSFORM FACE" | "TRANSFORMFACE"
)
}
fn apply_transform_delta(start: &TransformPose, delta: &TransformDelta) -> TransformPose {
match delta {
TransformDelta::Translate(world) => TransformPose {
position: [
start.position[0] + world[0],
start.position[1] + world[1],
start.position[2] + world[2],
],
..*start
},
TransformDelta::Rotate { axis, radians } => {
let q0 = quat_from_euler_xyz_deg(start.rotation_deg);
let dq = quat_from_axis_angle(*axis, *radians);
let nq = quat_mul(dq, q0);
TransformPose {
rotation_deg: euler_xyz_deg_from_quat(nq),
..*start
}
}
}
}
fn read_pose_vec3(
env: Option<&brep_kernel::Env>,
transform: Option<&serde_json::Value>,
key: &str,
default: [f64; 3],
) -> [f64; 3] {
let array = transform
.and_then(|t| t.get(key))
.and_then(|v| v.as_array());
let mut out = default;
if let Some(array) = array {
for (index, slot) in out.iter_mut().enumerate() {
match array.get(index) {
Some(serde_json::Value::Number(number)) => {
if let Some(number) = number.as_f64() {
*slot = number;
}
}
Some(serde_json::Value::String(source)) => {
if let Some(number) = env
.and_then(|env| env.eval(source).ok())
.filter(|number| number.is_finite())
{
*slot = number;
}
}
_ => {}
}
}
}
out
}
fn transform_frame_json(pose: &TransformPose) -> String {
let euler = [
pose.rotation_deg[0].to_radians(),
pose.rotation_deg[1].to_radians(),
pose.rotation_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.position,
"x": x,
"y": y,
"z": z,
"showCenter": true,
})
.to_string()
}
pub(super) fn normalize3(v: [f64; 3]) -> [f64; 3] {
let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if length < 1e-12 {
[0.0, 0.0, 1.0]
} else {
[v[0] / length, v[1] / length, v[2] / length]
}
}
pub(crate) fn rotate_euler_xyz_f64(v: [f64; 3], euler: [f64; 3]) -> [f64; 3] {
let (c1, s1) = (euler[0].cos(), euler[0].sin());
let (c2, s2) = (euler[1].cos(), euler[1].sin());
let (c3, s3) = (euler[2].cos(), euler[2].sin());
let m00 = c2 * c3;
let m01 = -c2 * s3;
let m02 = s2;
let m10 = c1 * s3 + c3 * s1 * s2;
let m11 = c1 * c3 - s1 * s2 * s3;
let m12 = -c2 * s1;
let m20 = s1 * s3 - c1 * c3 * s2;
let m21 = c3 * s1 + c1 * s2 * s3;
let m22 = c1 * c2;
[
m00 * v[0] + m01 * v[1] + m02 * v[2],
m10 * v[0] + m11 * v[1] + m12 * v[2],
m20 * v[0] + m21 * v[1] + m22 * v[2],
]
}
pub(super) type Quat = [f64; 4];
pub(super) fn quat_from_axis_angle(axis: [f64; 3], angle: f64) -> Quat {
let n = normalize3(axis);
let half = angle * 0.5;
let s = half.sin();
[n[0] * s, n[1] * s, n[2] * s, half.cos()]
}
pub(super) fn quat_from_euler_xyz_deg(deg: [f64; 3]) -> Quat {
let (c1, s1) = (
(deg[0].to_radians() * 0.5).cos(),
(deg[0].to_radians() * 0.5).sin(),
);
let (c2, s2) = (
(deg[1].to_radians() * 0.5).cos(),
(deg[1].to_radians() * 0.5).sin(),
);
let (c3, s3) = (
(deg[2].to_radians() * 0.5).cos(),
(deg[2].to_radians() * 0.5).sin(),
);
[
s1 * c2 * c3 + c1 * s2 * s3,
c1 * s2 * c3 - s1 * c2 * s3,
c1 * c2 * s3 + s1 * s2 * c3,
c1 * c2 * c3 - s1 * s2 * s3,
]
}
pub(super) fn quat_mul(a: Quat, b: Quat) -> Quat {
[
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
]
}
pub(super) fn euler_xyz_deg_from_quat(q: Quat) -> [f64; 3] {
let [x, y, z, w] = q;
let (x2, y2, z2) = (x + x, y + y, z + z);
let (xx, xy, xz) = (x * x2, x * y2, x * z2);
let (yy, yz, zz) = (y * y2, y * z2, z * z2);
let (wx, wy, wz) = (w * x2, w * y2, w * z2);
let m11 = 1.0 - (yy + zz);
let m12 = xy - wz;
let m13 = xz + wy;
let m22 = 1.0 - (xx + zz);
let m23 = yz - wx;
let m32 = yz + wx;
let m33 = 1.0 - (xx + yy);
let ey = m13.clamp(-1.0, 1.0).asin();
let (ex, ez) = if m13.abs() < 0.9999999 {
((-m23).atan2(m33), (-m12).atan2(m11))
} else {
(m32.atan2(m22), 0.0)
};
[ex.to_degrees(), ey.to_degrees(), ez.to_degrees()]
}
impl EngineState {
pub fn transform_armed_port_point(&self) -> Option<&str> {
self.transform_armed()
.then(|| self.transform_gizmo.port.as_deref())
.flatten()
}
pub fn arm_transform_for_port_point(&mut self, address: &str) -> bool {
if let Some(reason) = self.port_point_gizmo_refusal(address) {
self.push_notice(format!("no handles on {address}: {reason}"));
return false;
}
self.component_move_reset();
self.transform_gizmo.feature_id = None;
self.transform_gizmo.anchor = None;
self.transform_gizmo.port = Some(address.to_string());
self.transform_gizmo.mode = GizmoMode::Transform;
self.transform_gizmo.drag = None;
self.clear_feature_dimension_overlay();
self.sync_transform_gizmo();
true
}
pub fn port_point_gizmo_refusal(&self, address: &str) -> Option<String> {
let point = self.declared_point_value(address)?;
if let Some(target) = point
.get("mapsTo")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|target| !target.is_empty())
{
return Some(format!(
"it maps to '{target}', and that point's placement is this one's"
));
}
let transform = point.get("transform")?;
for key in ["position", "rotationEuler"] {
let Some(components) = transform.get(key).and_then(serde_json::Value::as_array) else {
continue;
};
for component in components {
if let Some(expression) = component.as_str() {
return Some(format!(
"'{expression}' places it, and a drag writes numbers"
));
}
}
}
None
}
fn port_point_seat(&self, address: &str) -> brep_kernel::Frame {
self.port_point_row(address)
.map(|row| row.seat.to_frame())
.unwrap_or_else(brep_kernel::world_seat)
}
pub(super) fn port_point_pose(&self, address: &str) -> Option<TransformPose> {
let point = self.declared_point_value(address)?;
let transform = point.get("transform");
let local_position = read_pose_vec3(None, transform, "position", [0.0; 3]);
let local_rotation = read_pose_vec3(None, transform, "rotationEuler", [0.0; 3]);
let seat = self.port_point_seat(address);
let world_axes = super::spline_edit::axes_from_euler_deg(local_rotation)
.map(|axis| seat_axis(&seat, axis));
Some(TransformPose {
position: array3(brep_kernel::seat_point(&seat, vec3(local_position))),
rotation_deg: super::spline_edit::euler_xyz_deg_from_axes(
world_axes[0],
world_axes[1],
world_axes[2],
),
scale: [1.0, 1.0, 1.0],
})
}
pub(super) fn write_port_point_pose(&mut self, address: &str, pose: &TransformPose) {
let seat = self.port_point_seat(address);
let position = array3(brep_kernel::unseat_point(&seat, vec3(pose.position)));
let local_axes = super::spline_edit::axes_from_euler_deg(pose.rotation_deg)
.map(|axis| unseat_axis(&seat, axis));
let rotation = super::spline_edit::euler_xyz_deg_from_axes(
local_axes[0],
local_axes[1],
local_axes[2],
);
let key = format!("port-point-pose:{address}");
self.edit_port_point(address, Some(&key), |point| {
point.insert(
"transform".into(),
serde_json::json!({ "position": position, "rotationEuler": rotation }),
);
});
}
}
fn vec3(a: [f64; 3]) -> brep_kernel::Vec3 {
brep_kernel::Vec3::new(a[0], a[1], a[2])
}
fn array3(v: brep_kernel::Vec3) -> [f64; 3] {
[v.x, v.y, v.z]
}
fn seat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
array3(brep_kernel::seat_vector(seat, vec3(axis)))
}
fn unseat_axis(seat: &brep_kernel::Frame, axis: [f64; 3]) -> [f64; 3] {
array3(brep_kernel::unseat_vector(seat, vec3(axis)))
}