Move/Copy like Fusion: opens with nothing moved and a full triad at the bodies' centre (X/Y/Z arrows, plane squares, rotation rings, free-drag centre ball; snapped drags, Alt/Ctrl free), Move Type (Free Move, Translate, Rotate, Point to Point, Point to Position), X/Y/Z distances and angles with live preview, a pivot input; the triad is one shared module (gizmo.rs); UI scenario s27
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
fba397ff65
commit
2b9ed16cc8
8 files changed
+794
-28
No files matched your search
@@ -52,7 +52,6 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
|
||||
| Kind::OffsetFaces { .. } => (at, normal),
|
||||
// Shell thickness grows into the body.
|
||||
Kind::Shell { .. } => (at, normal.map(|n| -n)),
|
||||
Kind::Move { .. } => (at, Some(Vec3::Z)),
|
||||
Kind::Sheet(_) => (at, crate::dialogs_sheet::arrow(app, d).or(normal)),
|
||||
Kind::Plastic(_) => (at, normal),
|
||||
Kind::PatternRect { .. } => (at, d.inputs.get(1).and_then(|i| i.items.first()).and_then(|x| crate::dialogs::axis_of(app, x)).map(|a| a.1)),
|
||||
@@ -96,6 +95,16 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj
|
||||
app.dialog = Some(d);
|
||||
return;
|
||||
}
|
||||
// Move: the triad at the pivot; dragging it sets the distances and angles.
|
||||
if matches!(d.kind, Kind::Move { .. }) {
|
||||
if let Some(t) = crate::dialogs_move::triad(app, &d)
|
||||
&& let Some(g) = crate::gizmo::show(ui, painter, proj, egui::Id::new("sc_move_triad"), &t, snap_step(app.cam.half_height()))
|
||||
{
|
||||
crate::dialogs_move::drag(app, &mut d, g);
|
||||
}
|
||||
app.dialog = Some(d);
|
||||
return;
|
||||
}
|
||||
if let Some((base, dir)) = anchor(app, &d)
|
||||
&& let Some(bs) = proj.to_screen(base)
|
||||
{
|
||||
|
||||
@@ -280,6 +280,11 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
// Along an origin axis, on its solid part.
|
||||
let size = crate::selection::origin_size(app.cam.half_height());
|
||||
crate::viewport::origin_axes(app).into_iter().find(|(name, _)| *name == n).map(|(_, d)| d * (size * 0.8))
|
||||
} else if let Some(n) = p.get("gizmo").and_then(Value::as_str) {
|
||||
// A handle of the move triad ("x", "xy", "rz", "ball"), moved `by` [x,y,z] mm or
|
||||
// turned `turn` degrees (rings).
|
||||
let by = p.get("by").map(|b| solvecraft_engine::geom::Vec3::new(num(b, 0), num(b, 1), num(b, 2))).unwrap_or_default();
|
||||
crate::gizmo::handle_point(n, by, p.get("turn").and_then(Value::as_f64).unwrap_or(0.0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -156,10 +156,17 @@ pub enum Kind {
|
||||
of: Vec<Sel>,
|
||||
},
|
||||
/// Move bodies by a distance along X, Y and Z.
|
||||
/// Move/Copy (see `dialogs_move`): the move type, X/Y/Z distances (Point to Position: the
|
||||
/// target), X/Y/Z angles (Free Move) and the angle (Rotate).
|
||||
Move {
|
||||
mode: usize,
|
||||
x: String,
|
||||
y: String,
|
||||
z: String,
|
||||
rx: String,
|
||||
ry: String,
|
||||
rz: String,
|
||||
angle: String,
|
||||
},
|
||||
Hole {
|
||||
diameter: String,
|
||||
@@ -367,9 +374,7 @@ impl Dialog {
|
||||
Kind::Sweep { operation: usize::from(has_bodies) },
|
||||
vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Path", CURVES, true)],
|
||||
),
|
||||
"FusionMoveCommand" => {
|
||||
Dialog::new(Kind::Move { x: "0 mm".into(), y: "0 mm".into(), z: "10 mm".into() }, vec![SelInput::new("Bodies", BODIES, true)])
|
||||
}
|
||||
"FusionMoveCommand" => Dialog::new(crate::dialogs_move::kind(0), crate::dialogs_move::inputs(0)),
|
||||
"FusionHoleCommand" => Dialog::new(hole_defaults(), vec![SelInput::new("Face", PLANAR_FACES, true)]),
|
||||
"PrimitiveBox" => Dialog::new(
|
||||
Kind::Primitive {
|
||||
@@ -550,7 +555,7 @@ impl Dialog {
|
||||
Kind::Shell { thickness } => ("Thickness", ValueKind::Length, thickness),
|
||||
Kind::Draft { angle } => ("Angle", ValueKind::Angle, angle),
|
||||
Kind::Hole { diameter, .. } => ("Diameter", ValueKind::Length, diameter),
|
||||
Kind::Move { z, .. } => ("Z", ValueKind::Length, z),
|
||||
Kind::Move { x, .. } => ("X", ValueKind::Length, x),
|
||||
Kind::PatternRect { spacing, .. } => ("Spacing", ValueKind::Length, spacing),
|
||||
Kind::PatternCirc { angle, .. } => ("Angle", ValueKind::Angle, angle),
|
||||
Kind::Section { offset, .. } => ("Distance", ValueKind::Length, offset),
|
||||
@@ -891,6 +896,10 @@ fn object_type_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||
/// Rows above the selection inputs: the pattern type and object type, the Hole placement (one
|
||||
/// face point, or sketch points).
|
||||
fn placement_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||
if matches!(d.kind, Kind::Move { .. }) {
|
||||
crate::dialogs_move::type_row(d, ui);
|
||||
return;
|
||||
}
|
||||
let current = match d.kind {
|
||||
Kind::PatternRect { .. } => Some(0),
|
||||
Kind::PatternCirc { .. } => Some(1),
|
||||
@@ -1370,13 +1379,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
combo(ui, "sw_op", &OP_LABELS, operation);
|
||||
ui.end_row();
|
||||
}
|
||||
Kind::Move { x, y, z } => {
|
||||
for (l, v) in [("X distance", x), ("Y distance", y), ("Z distance", z)] {
|
||||
row_label(ui, l);
|
||||
enter |= field(ui, v);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
k @ Kind::Move { .. } => enter |= crate::dialogs_move::rows(ui, k),
|
||||
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } => {
|
||||
row_label(ui, "Extents");
|
||||
let mut ext = usize::from(opts.all);
|
||||
@@ -2031,10 +2034,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
};
|
||||
("FusionHalfSectionViewCommand", json!({"plane": plane, "offset": offset, "flip": flip}))
|
||||
}
|
||||
Kind::Move { x, y, z } => {
|
||||
need(0, "bodies")?;
|
||||
("FusionMoveCommand", json!({"bodies": body_names(0), "translate": [x, y, z]}))
|
||||
}
|
||||
Kind::Move { .. } => return Ok(vec![("FusionMoveCommand".into(), crate::dialogs_move::params(app, d)?)]),
|
||||
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } => {
|
||||
need(0, if opts.multiple { "sketch points" } else { "a face position" })?;
|
||||
let ty = match *kind {
|
||||
@@ -2709,17 +2709,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
}
|
||||
FeatureKind::Move { bodies, translate, rotate_axis, angle } => {
|
||||
let mut d = start("FusionMoveCommand")?;
|
||||
let [x, y, z] = translate.clone();
|
||||
d.kind = Kind::Move { x, y, z };
|
||||
if let Some(inp) = d.inputs.first_mut() {
|
||||
inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect();
|
||||
}
|
||||
if let Some(a) = rotate_axis {
|
||||
d.extra.insert("axis".into(), pt3(*a));
|
||||
}
|
||||
if let Some(a) = angle {
|
||||
d.extra.insert("angle".into(), json!(a));
|
||||
}
|
||||
crate::dialogs_move::for_feature(&mut d, bodies, translate, *rotate_axis, angle.as_ref());
|
||||
d
|
||||
}
|
||||
other => {
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
//! Move/Copy: the move types (Free Move, Translate, Rotate, Point to Point, Point to Position),
|
||||
//! their rows and inputs, the triad on the canvas and the FusionMoveCommand they make. Nothing
|
||||
//! moves until a value is typed or the triad is dragged.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_engine::Sel;
|
||||
use solvecraft_engine::doc::expr::Kind as ValueKind;
|
||||
use solvecraft_engine::geom::Vec3;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{Dialog, Kind, combo, field, row_label};
|
||||
use crate::gizmo::{self, Change, Drag};
|
||||
use crate::selection::{AXES, BODIES, EDGES, FACES, SelInput, VERTICES};
|
||||
|
||||
pub const TYPES: [&str; 5] = ["Free Move", "Translate", "Rotate", "Point to Point", "Point to Position"];
|
||||
const FREE: usize = 0;
|
||||
const TRANSLATE: usize = 1;
|
||||
const ROTATE: usize = 2;
|
||||
const POINT_TO_POINT: usize = 3;
|
||||
const POINT_TO_POSITION: usize = 4;
|
||||
const POINTS: u16 = VERTICES | FACES | EDGES;
|
||||
|
||||
/// A fresh Move dialog of a type (all values zero).
|
||||
pub fn kind(mode: usize) -> Kind {
|
||||
let z = || "0 mm".to_string();
|
||||
let a = || "0 deg".to_string();
|
||||
Kind::Move { mode, x: z(), y: z(), z: z(), rx: a(), ry: a(), rz: a(), angle: a() }
|
||||
}
|
||||
|
||||
/// The selection inputs of a move type (the bodies first).
|
||||
pub fn inputs(mode: usize) -> Vec<SelInput> {
|
||||
let mut v = vec![SelInput::new("Bodies", BODIES, true)];
|
||||
match mode {
|
||||
FREE => v.push(SelInput::new("Pivot", POINTS, false)),
|
||||
ROTATE => v.push(SelInput::new("Axis", AXES | EDGES, false)),
|
||||
POINT_TO_POINT => {
|
||||
v.push(SelInput::new("From", POINTS, false));
|
||||
v.push(SelInput::new("To", POINTS, false));
|
||||
}
|
||||
POINT_TO_POSITION => v.push(SelInput::new("Point", POINTS, false)),
|
||||
_ => {}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// The Move Type row (above the inputs); switching keeps the bodies and resets the values.
|
||||
pub fn type_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||
let Kind::Move { mode, .. } = d.kind else { return };
|
||||
row_label(ui, "Move Type");
|
||||
let mut m = mode;
|
||||
combo(ui, "move_type", &TYPES, &mut m);
|
||||
ui.end_row();
|
||||
if m != mode {
|
||||
let bodies = d.inputs.first().map(|i| i.items.clone()).unwrap_or_default();
|
||||
d.kind = kind(m);
|
||||
d.inputs = inputs(m);
|
||||
if let Some(i) = d.inputs.first_mut() {
|
||||
i.items = bodies;
|
||||
}
|
||||
d.extra.remove("pivot");
|
||||
d.active = if bodies_empty(d) { 0 } else { 1.min(d.inputs.len() - 1) };
|
||||
}
|
||||
}
|
||||
|
||||
fn bodies_empty(d: &Dialog) -> bool {
|
||||
d.inputs.first().is_none_or(|i| i.items.is_empty())
|
||||
}
|
||||
|
||||
/// The value rows after the inputs; true when Enter was pressed in one.
|
||||
pub fn rows(ui: &mut egui::Ui, k: &mut Kind) -> bool {
|
||||
let Kind::Move { mode, x, y, z, rx, ry, rz, angle } = k else { return false };
|
||||
let mut enter = false;
|
||||
let mut row = |ui: &mut egui::Ui, l: &str, v: &mut String| {
|
||||
row_label(ui, l);
|
||||
enter |= field(ui, v);
|
||||
ui.end_row();
|
||||
};
|
||||
match *mode {
|
||||
FREE | TRANSLATE => {
|
||||
row(ui, "X Distance", x);
|
||||
row(ui, "Y Distance", y);
|
||||
row(ui, "Z Distance", z);
|
||||
if *mode == FREE {
|
||||
row(ui, "X Angle", rx);
|
||||
row(ui, "Y Angle", ry);
|
||||
row(ui, "Z Angle", rz);
|
||||
}
|
||||
}
|
||||
ROTATE => row(ui, "Angle", angle),
|
||||
POINT_TO_POSITION => {
|
||||
row(ui, "X", x);
|
||||
row(ui, "Y", y);
|
||||
row(ui, "Z", z);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
enter
|
||||
}
|
||||
|
||||
fn sel_point(s: Option<&Sel>) -> Option<Vec3> {
|
||||
match s? {
|
||||
Sel::Vertex { point, .. } | Sel::Face { point, .. } | Sel::Edge { point, .. } => Some(*point),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn items(d: &Dialog, i: usize) -> &[Sel] {
|
||||
d.inputs.get(i).map(|x| x.items.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn body_names(d: &Dialog) -> Vec<String> {
|
||||
items(d, 0).iter().filter_map(|x| if let Sel::Body { name } = x { Some(name.clone()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// The centre of the picked bodies where they are before this move.
|
||||
fn bodies_center(app: &SolveApp, d: &Dialog) -> Option<Vec3> {
|
||||
let st = match d.editing {
|
||||
Some((id, _)) => app.session.model.state_before(id),
|
||||
None => app.session.model.state(),
|
||||
};
|
||||
let mut bb: Option<(Vec3, Vec3)> = None;
|
||||
for n in body_names(d) {
|
||||
let Some(b) = st.body(&n) else { continue };
|
||||
let x = b.mesh().bounds();
|
||||
bb = Some(match bb {
|
||||
None => (x.min, x.max),
|
||||
Some((lo, hi)) => (
|
||||
Vec3::new(lo.x.min(x.min.x), lo.y.min(x.min.y), lo.z.min(x.min.z)),
|
||||
Vec3::new(hi.x.max(x.max.x), hi.y.max(x.max.y), hi.z.max(x.max.z)),
|
||||
),
|
||||
});
|
||||
}
|
||||
bb.map(|(lo, hi)| (lo + hi) * 0.5)
|
||||
}
|
||||
|
||||
/// What a Free Move turns about: the picked pivot, else the kept one (editing), else the
|
||||
/// bodies' centre.
|
||||
fn pivot(app: &SolveApp, d: &Dialog) -> Option<Vec3> {
|
||||
if let Some(p) = sel_point(items(d, 1).first()) {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(a) = d.extra.get("pivot").and_then(|v| serde_json::from_value::<[f64; 3]>(v.clone()).ok()) {
|
||||
return Some(Vec3::new(a[0], a[1], a[2]));
|
||||
}
|
||||
bodies_center(app, d)
|
||||
}
|
||||
|
||||
fn len(app: &SolveApp, s: &str) -> Result<f64, String> {
|
||||
app.session.doc.eval(s, ValueKind::Length).map_err(|e| e.to_string()).and_then(|v| if v.is_finite() { Ok(v) } else { Err("not a number".into()) })
|
||||
}
|
||||
|
||||
fn ang(app: &SolveApp, s: &str) -> Result<f64, String> {
|
||||
app.session.doc.eval(s, ValueKind::Angle).map_err(|e| e.to_string()).and_then(|v| if v.is_finite() { Ok(v) } else { Err("not a number".into()) })
|
||||
}
|
||||
|
||||
fn mm(v: f64) -> String {
|
||||
let s = format!("{v:.6}");
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.');
|
||||
format!("{} mm", if s == "-0" || s.is_empty() { "0" } else { s })
|
||||
}
|
||||
|
||||
fn deg(v: f64) -> String {
|
||||
let s = format!("{:.4}", v.to_degrees());
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.');
|
||||
format!("{} deg", if s == "-0" || s.is_empty() { "0" } else { s })
|
||||
}
|
||||
|
||||
/// The FusionMoveCommand parameters (bodies, translate, axis, angle).
|
||||
pub fn params(app: &SolveApp, d: &Dialog) -> Result<Value, String> {
|
||||
let Kind::Move { mode, x, y, z, rx, ry, rz, angle } = &d.kind else { return Err("not a move".into()) };
|
||||
let bodies = body_names(d);
|
||||
if bodies.is_empty() {
|
||||
return Err("select bodies first".into());
|
||||
}
|
||||
let need_point = |i: usize, what: &str| sel_point(items(d, i).first()).ok_or(format!("select {what} first"));
|
||||
let num = |t: Vec3| json!([mm(t.x), mm(t.y), mm(t.z)]);
|
||||
let mut p = json!({ "bodies": bodies });
|
||||
match *mode {
|
||||
TRANSLATE => p["translate"] = json!([x, y, z]),
|
||||
FREE => {
|
||||
let a = [ang(app, rx)?, ang(app, ry)?, ang(app, rz)?];
|
||||
let turned: Vec<usize> = (0..3).filter(|i| a[*i].abs() > 1e-12).collect();
|
||||
if turned.is_empty() {
|
||||
p["translate"] = json!([x, y, z]);
|
||||
// A rotation kept from an edited move about another axis.
|
||||
for k in ["axis", "angle"] {
|
||||
if let Some(v) = d.extra.get(k) {
|
||||
p[k] = v.clone();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let piv = pivot(app, d).ok_or("select bodies first")?;
|
||||
let r = gizmo::mul(&gizmo::mul(&gizmo::rotation(Vec3::Z, a[2]), &gizmo::rotation(Vec3::Y, a[1])), &gizmo::rotation(Vec3::X, a[0]));
|
||||
let (axis, th) = gizmo::axis_angle(&r);
|
||||
match turned.as_slice() {
|
||||
// One turn about the origin: the typed values stay as they are.
|
||||
[i] if piv.len() < 1e-9 => {
|
||||
p["translate"] = json!([x, y, z]);
|
||||
p["axis"] = json!([[1, 0, 0], [0, 1, 0], [0, 0, 1]][*i]);
|
||||
p["angle"] = json!([rx, ry, rz][*i]);
|
||||
}
|
||||
_ => {
|
||||
let t = Vec3::new(len(app, x)?, len(app, y)?, len(app, z)?);
|
||||
p["translate"] = num(piv - gizmo::apply(&r, piv) + t);
|
||||
p["axis"] = json!([axis.x, axis.y, axis.z]);
|
||||
p["angle"] = json!(deg(th));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ROTATE => {
|
||||
let (o, a) = items(d, 1).first().and_then(|s| axis_line(app, s)).ok_or("select an axis or a straight edge first")?;
|
||||
let th = ang(app, angle)?;
|
||||
let r = gizmo::rotation(a, th);
|
||||
p["translate"] = num(o - gizmo::apply(&r, o));
|
||||
p["axis"] = json!([a.x, a.y, a.z]);
|
||||
p["angle"] = json!(angle);
|
||||
}
|
||||
POINT_TO_POINT => {
|
||||
let (from, to) = (need_point(1, "the point to move from")?, need_point(2, "the point to move to")?);
|
||||
p["translate"] = num(to - from);
|
||||
}
|
||||
_ => {
|
||||
let from = need_point(1, "the point to move")?;
|
||||
let at = Vec3::new(len(app, x)?, len(app, y)?, len(app, z)?);
|
||||
p["translate"] = num(at - from);
|
||||
}
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// An axis line: origin axes and sketch lines, or a straight body edge.
|
||||
fn axis_line(app: &SolveApp, s: &Sel) -> Option<(Vec3, Vec3)> {
|
||||
if let Some(a) = crate::dialogs::axis_of(app, s) {
|
||||
return Some(a);
|
||||
}
|
||||
let Sel::Edge { body, index, .. } = s else { return None };
|
||||
let st = app.session.model.state();
|
||||
let m = st.body(body)?.mesh();
|
||||
let e = m.edges.get(*index)?;
|
||||
let (a, b) = (*e.first()?, *e.last()?);
|
||||
let dir = (b - a).normalized()?;
|
||||
// Straight: every point on the chord.
|
||||
e.iter().all(|p| (*p - a).cross(dir).len() < 1e-6 * (1.0 + (b - a).len())).then_some((a, dir))
|
||||
}
|
||||
|
||||
/// Where the triad sits: the pivot carried by the move so far (Free Move and Translate only).
|
||||
pub fn triad(app: &SolveApp, d: &Dialog) -> Option<gizmo::Triad> {
|
||||
let Kind::Move { mode, x, y, z, .. } = &d.kind else { return None };
|
||||
if !matches!(*mode, FREE | TRANSLATE) || bodies_empty(d) {
|
||||
return None;
|
||||
}
|
||||
let t = Vec3::new(len(app, x).unwrap_or(0.0), len(app, y).unwrap_or(0.0), len(app, z).unwrap_or(0.0));
|
||||
Some(gizmo::Triad { center: pivot(app, d)? + t, translate: true, rotate: *mode == FREE })
|
||||
}
|
||||
|
||||
/// Apply a triad drag to the dialog's values (added to the values when the drag began).
|
||||
pub fn drag(app: &SolveApp, d: &mut Dialog, g: Drag) {
|
||||
let Kind::Move { x, y, z, rx, ry, rz, .. } = &d.kind else { return };
|
||||
if g.started || !d.extra.contains_key("drag0") {
|
||||
let v = [len(app, x), len(app, y), len(app, z), ang(app, rx), ang(app, ry), ang(app, rz)].map(|r| r.unwrap_or(0.0));
|
||||
d.extra.insert("drag0".into(), json!(v));
|
||||
}
|
||||
let v0: [f64; 6] = d.extra.get("drag0").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
|
||||
let Kind::Move { x, y, z, rx, ry, rz, .. } = &mut d.kind else { return };
|
||||
match g.change {
|
||||
Change::Translate(t) => {
|
||||
*x = mm(v0[0] + t.x);
|
||||
*y = mm(v0[1] + t.y);
|
||||
*z = mm(v0[2] + t.z);
|
||||
}
|
||||
Change::Rotate { axis, angle } => {
|
||||
let slot = match axis {
|
||||
0 => rx,
|
||||
1 => ry,
|
||||
_ => rz,
|
||||
};
|
||||
*slot = deg(v0.get(3 + axis.min(2)).copied().unwrap_or(0.0) + angle);
|
||||
}
|
||||
}
|
||||
if g.done {
|
||||
d.extra.remove("drag0");
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog for editing a move: Free Move with its translation and, about a world axis
|
||||
/// through the origin, its angle (another axis is kept as it is).
|
||||
pub fn for_feature(d: &mut Dialog, bodies: &[String], translate: &[String; 3], axis: Option<Vec3>, angle: Option<&String>) {
|
||||
let [x, y, z] = translate.clone();
|
||||
let mut k = kind(FREE);
|
||||
if let Kind::Move { x: kx, y: ky, z: kz, rx, ry, rz, .. } = &mut k {
|
||||
(*kx, *ky, *kz) = (x, y, z);
|
||||
if let Some(a) = angle {
|
||||
let dir = axis.unwrap_or(Vec3::Z);
|
||||
let world = [Vec3::X, Vec3::Y, Vec3::Z].iter().position(|w| dir.cross(*w).len() < 1e-9 && dir.dot(*w) > 0.0);
|
||||
match world {
|
||||
Some(0) => *rx = a.clone(),
|
||||
Some(1) => *ry = a.clone(),
|
||||
Some(_) => *rz = a.clone(),
|
||||
None => {
|
||||
d.extra.insert("axis".into(), json!([dir.x, dir.y, dir.z]));
|
||||
d.extra.insert("angle".into(), json!(a));
|
||||
}
|
||||
}
|
||||
// The stored move turns about the origin.
|
||||
d.extra.insert("pivot".into(), json!([0.0, 0.0, 0.0]));
|
||||
}
|
||||
}
|
||||
d.kind = k;
|
||||
d.inputs = inputs(FREE);
|
||||
if let Some(inp) = d.inputs.first_mut() {
|
||||
inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
//! The move/rotate triad shared by Move/Copy and occurrence moves: X/Y/Z arrows, XY/YZ/XZ plane
|
||||
//! squares, X/Y/Z rotation rings and a centre ball for a free drag in the view plane. It is a
|
||||
//! constant size on screen. A drag reports its change since the drag began (distances snap to
|
||||
//! round steps at the current zoom, angles to 5°; hold Alt or Ctrl for a free drag), so the caller
|
||||
//! adds it to the values it had when the drag started.
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use egui::{Color32, Pos2, Rect, Sense, Shape, Stroke, vec2};
|
||||
use solvecraft_engine::geom::Vec3;
|
||||
|
||||
use crate::theme::Tokens;
|
||||
use crate::viewport::Proj;
|
||||
|
||||
/// A part of the triad.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Handle {
|
||||
/// Arrow along X, Y or Z (0, 1, 2).
|
||||
Axis(usize),
|
||||
/// Square in the plane across that axis (Plane(2) is XY).
|
||||
Plane(usize),
|
||||
/// Ring turning about that axis.
|
||||
Ring(usize),
|
||||
/// Centre ball: free drag in the view plane.
|
||||
Ball,
|
||||
}
|
||||
|
||||
/// What a drag has done so far.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Change {
|
||||
Translate(Vec3),
|
||||
/// About X, Y or Z through the centre, in radians.
|
||||
Rotate {
|
||||
axis: usize,
|
||||
angle: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Drag {
|
||||
pub handle: Handle,
|
||||
pub change: Change,
|
||||
/// The first frame of the drag (take a snapshot of the values now).
|
||||
pub started: bool,
|
||||
/// The drag ended this frame.
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
/// Where the triad is and which handles it shows.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Triad {
|
||||
pub center: Vec3,
|
||||
pub translate: bool,
|
||||
pub rotate: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct State {
|
||||
handle: Handle,
|
||||
center: Vec3,
|
||||
/// Grab point on the plane (planes, ball) or parameter along the axis.
|
||||
grab: Vec3,
|
||||
t0: f64,
|
||||
/// Ring: last angle seen and the angle turned so far.
|
||||
last: f64,
|
||||
turned: f64,
|
||||
}
|
||||
|
||||
const DIRS: [Vec3; 3] = [Vec3::X, Vec3::Y, Vec3::Z];
|
||||
/// Arrow length on screen (px).
|
||||
const LEN_PX: f64 = 110.0;
|
||||
|
||||
thread_local! {
|
||||
/// The last triad drawn: centre and arrow length in mm (for scenarios and tests).
|
||||
static LAST: RefCell<Option<(Vec3, f64)>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// A world point on a handle of the last triad drawn: "x"/"y"/"z" on an arrow, "xy"/"yz"/"xz"
|
||||
/// in a square, "rx"/"ry"/"rz" on a ring, "ball". `by` moves it, `turn` (degrees) turns a ring
|
||||
/// point about its axis.
|
||||
pub fn handle_point(name: &str, by: Vec3, turn: f64) -> Option<Vec3> {
|
||||
let (c, l) = LAST.with(|x| *x.borrow())?;
|
||||
let axis = |s: &str| match s {
|
||||
"x" => Some(0),
|
||||
"y" => Some(1),
|
||||
"z" => Some(2),
|
||||
_ => None,
|
||||
};
|
||||
let p = match name {
|
||||
"ball" => c,
|
||||
"xy" => c + (Vec3::X + Vec3::Y) * (0.35 * l),
|
||||
"yz" => c + (Vec3::Y + Vec3::Z) * (0.35 * l),
|
||||
"xz" => c + (Vec3::X + Vec3::Z) * (0.35 * l),
|
||||
r if r.starts_with('r') => {
|
||||
let i = axis(&r[1..])?;
|
||||
let (e1, e2) = ring_basis(i);
|
||||
let th = turn.to_radians();
|
||||
return Some(c + (e1 * th.cos() + e2 * th.sin()) * (RING * l) + by);
|
||||
}
|
||||
a => c + DIRS[axis(a)?] * (0.75 * l),
|
||||
};
|
||||
Some(p + by)
|
||||
}
|
||||
|
||||
/// Ring radius as a share of the arrow length.
|
||||
const RING: f64 = 0.62;
|
||||
|
||||
/// In-plane directions of the ring about an axis (right-handed: e1 × e2 = axis).
|
||||
fn ring_basis(i: usize) -> (Vec3, Vec3) {
|
||||
match i {
|
||||
0 => (Vec3::Y, Vec3::Z),
|
||||
1 => (Vec3::Z, Vec3::X),
|
||||
_ => (Vec3::X, Vec3::Y),
|
||||
}
|
||||
}
|
||||
|
||||
fn axis_color(i: usize) -> Color32 {
|
||||
match i {
|
||||
0 => Color32::from_rgb(225, 70, 60),
|
||||
1 => Color32::from_rgb(80, 190, 80),
|
||||
_ => Color32::from_rgb(70, 130, 240),
|
||||
}
|
||||
}
|
||||
|
||||
/// Distance from `p` to the screen segment a–b.
|
||||
fn seg_dist(p: Pos2, a: Pos2, b: Pos2) -> f32 {
|
||||
let ab = b - a;
|
||||
let t = ((p - a).dot(ab) / ab.length_sq().max(1e-6)).clamp(0.0, 1.0);
|
||||
(a + ab * t).distance(p)
|
||||
}
|
||||
|
||||
fn in_quad(p: Pos2, q: &[Pos2]) -> bool {
|
||||
if q.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
let mut sign = 0.0f32;
|
||||
for i in 0..q.len() {
|
||||
let (a, b) = (q[i], q[(i + 1) % q.len()]);
|
||||
let c = (b - a).x * (p - a).y - (b - a).y * (p - a).x;
|
||||
if c.abs() < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
if sign == 0.0 {
|
||||
sign = c.signum();
|
||||
} else if c.signum() != sign {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Where the view ray through `pos` meets the plane through `o` with normal `n`.
|
||||
fn on_plane(proj: &Proj, pos: Pos2, o: Vec3, n: Vec3) -> Option<Vec3> {
|
||||
let (ro, rd) = proj.ray(pos);
|
||||
let den = rd.dot(n);
|
||||
if den.abs() < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
Some(ro + rd * ((o - ro).dot(n) / den))
|
||||
}
|
||||
|
||||
/// Parameter along the line `o + d t` closest to the view ray through `pos`.
|
||||
fn along(proj: &Proj, pos: Pos2, o: Vec3, d: Vec3) -> Option<f64> {
|
||||
let (ro, r) = proj.ray(pos);
|
||||
let w0 = o - ro;
|
||||
let (b, dd, e) = (d.dot(r), d.dot(w0), r.dot(w0));
|
||||
let den = 1.0 - b * b;
|
||||
if den.abs() < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
Some((b * e - dd) / den)
|
||||
}
|
||||
|
||||
/// The triad's shapes: (handle, screen polygon or polyline, is it a filled area).
|
||||
struct Shapes {
|
||||
arrows: Vec<(usize, Pos2, Pos2)>,
|
||||
planes: Vec<(usize, Vec<Pos2>)>,
|
||||
rings: Vec<(usize, Vec<Pos2>)>,
|
||||
ball: Pos2,
|
||||
}
|
||||
|
||||
fn shapes(proj: &Proj, t: &Triad, l: f64) -> Option<Shapes> {
|
||||
let c = t.center;
|
||||
let ball = proj.to_screen(c)?;
|
||||
let mut s = Shapes { arrows: Vec::new(), planes: Vec::new(), rings: Vec::new(), ball };
|
||||
if t.translate {
|
||||
for (i, d) in DIRS.iter().enumerate() {
|
||||
if let (Some(a), Some(b)) = (proj.to_screen(c + *d * (0.18 * l)), proj.to_screen(c + *d * l)) {
|
||||
s.arrows.push((i, a, b));
|
||||
}
|
||||
}
|
||||
for i in 0..3 {
|
||||
let (e1, e2) = ring_basis(i);
|
||||
let q: Vec<Pos2> = [(0.25, 0.25), (0.45, 0.25), (0.45, 0.45), (0.25, 0.45)]
|
||||
.iter()
|
||||
.filter_map(|(a, b)| proj.to_screen(c + e1 * (a * l) + e2 * (b * l)))
|
||||
.collect();
|
||||
if q.len() == 4 {
|
||||
s.planes.push((i, q));
|
||||
}
|
||||
}
|
||||
}
|
||||
if t.rotate {
|
||||
for i in 0..3 {
|
||||
let (e1, e2) = ring_basis(i);
|
||||
let pts: Vec<Pos2> = (0..=64)
|
||||
.map(|k| k as f64 / 64.0 * std::f64::consts::TAU)
|
||||
.filter_map(|th| proj.to_screen(c + (e1 * th.cos() + e2 * th.sin()) * (RING * l)))
|
||||
.collect();
|
||||
if pts.len() > 2 {
|
||||
s.rings.push((i, pts));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(s)
|
||||
}
|
||||
|
||||
fn hit(s: &Shapes, p: Pos2) -> Option<Handle> {
|
||||
if s.ball.distance(p) < 9.0 {
|
||||
return Some(Handle::Ball);
|
||||
}
|
||||
if let Some((i, ..)) = s.arrows.iter().find(|(_, a, b)| seg_dist(p, *a, *b) < 6.0) {
|
||||
return Some(Handle::Axis(*i));
|
||||
}
|
||||
if let Some((i, _)) = s.planes.iter().find(|(_, q)| in_quad(p, q)) {
|
||||
return Some(Handle::Plane(*i));
|
||||
}
|
||||
s.rings.iter().find(|(_, pts)| pts.windows(2).any(|w| seg_dist(p, w[0], w[1]) < 5.0)).map(|(i, _)| Handle::Ring(*i))
|
||||
}
|
||||
|
||||
/// Millimetres per screen pixel at a point.
|
||||
fn mm_per_px(proj: &Proj, c: Vec3) -> Option<f64> {
|
||||
let (_, view) = proj.ray(proj.to_screen(c)?);
|
||||
let u = view.any_perp().normalized()?;
|
||||
let (a, b) = (proj.to_screen(c)?, proj.to_screen(c + u)?);
|
||||
let px = f64::from(a.distance(b));
|
||||
(px > 1e-9).then(|| 1.0 / px)
|
||||
}
|
||||
|
||||
fn snap(v: f64, step: f64, free: bool) -> f64 {
|
||||
if free || step <= 0.0 { v } else { (v / step).round() * step }
|
||||
}
|
||||
|
||||
/// Draw the triad and run a drag on it. `step` is the distance snap step.
|
||||
pub fn show(ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj, id: egui::Id, t: &Triad, step: f64) -> Option<Drag> {
|
||||
let tk = Tokens::get();
|
||||
let l = mm_per_px(proj, t.center)? * LEN_PX;
|
||||
LAST.with(|x| *x.borrow_mut() = Some((t.center, l)));
|
||||
let sh = shapes(proj, t, l)?;
|
||||
let state: Option<State> = ui.data(|d| d.get_temp(id));
|
||||
let pointer = ui.input(|i| i.pointer.hover_pos());
|
||||
let hovered = if state.is_none() { pointer.filter(|p| proj.rect.contains(*p)).and_then(|p| hit(&sh, p)) } else { None };
|
||||
let active = state.map(|s| s.handle).or(hovered);
|
||||
let hot = |h: Handle| active == Some(h);
|
||||
let col = |h: Handle, i: usize| if hot(h) { tk.accent } else { axis_color(i) };
|
||||
for (i, pts) in &sh.rings {
|
||||
painter.add(Shape::line(pts.clone(), Stroke::new(if hot(Handle::Ring(*i)) { 3.0 } else { 1.8 }, col(Handle::Ring(*i), *i))));
|
||||
}
|
||||
for (i, q) in &sh.planes {
|
||||
let c = col(Handle::Plane(*i), *i);
|
||||
painter.add(Shape::convex_polygon(q.clone(), c.gamma_multiply(if hot(Handle::Plane(*i)) { 0.7 } else { 0.35 }), Stroke::new(1.0, c)));
|
||||
}
|
||||
for (i, a, b) in &sh.arrows {
|
||||
arrow(painter, *a, *b, col(Handle::Axis(*i), *i), hot(Handle::Axis(*i)));
|
||||
}
|
||||
let bc = if hot(Handle::Ball) { tk.accent } else { Color32::from_gray(235) };
|
||||
painter.circle(sh.ball, 6.0, bc, Stroke::new(1.5, Color32::from_gray(60)));
|
||||
let p = pointer?;
|
||||
active?;
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
|
||||
let r = ui.interact(Rect::from_center_size(p, vec2(24.0, 24.0)), id.with("hit"), Sense::drag());
|
||||
let free = ui.input(|i| i.modifiers.alt || i.modifiers.ctrl || i.modifiers.command);
|
||||
let view = proj.ray(sh.ball).1;
|
||||
let plane_normal = |h: Handle| match h {
|
||||
Handle::Plane(i) | Handle::Ring(i) => DIRS[i],
|
||||
_ => view,
|
||||
};
|
||||
let mut started = false;
|
||||
let mut st = match state {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
if !r.drag_started() {
|
||||
return None;
|
||||
}
|
||||
let h = hovered?;
|
||||
let from = ui.input(|i| i.pointer.press_origin()).unwrap_or(p);
|
||||
let c = t.center;
|
||||
let mut s = State { handle: h, center: c, grab: c, t0: 0.0, last: 0.0, turned: 0.0 };
|
||||
match h {
|
||||
Handle::Axis(i) => s.t0 = along(proj, from, c, DIRS[i])?,
|
||||
Handle::Ring(i) => {
|
||||
let q = on_plane(proj, from, c, DIRS[i])? - c;
|
||||
let (e1, e2) = ring_basis(i);
|
||||
s.last = q.dot(e2).atan2(q.dot(e1));
|
||||
}
|
||||
_ => s.grab = on_plane(proj, from, c, plane_normal(h))?,
|
||||
}
|
||||
started = true;
|
||||
s
|
||||
}
|
||||
};
|
||||
let change = match st.handle {
|
||||
Handle::Axis(i) => {
|
||||
let v = along(proj, p, st.center, DIRS[i]).map(|v| v - st.t0).unwrap_or(0.0);
|
||||
Change::Translate(DIRS[i] * snap(v, step, free))
|
||||
}
|
||||
Handle::Ring(i) => {
|
||||
if let Some(q) = on_plane(proj, p, st.center, DIRS[i]).map(|q| q - st.center) {
|
||||
let (e1, e2) = ring_basis(i);
|
||||
let th = q.dot(e2).atan2(q.dot(e1));
|
||||
let mut d = th - st.last;
|
||||
while d > std::f64::consts::PI {
|
||||
d -= std::f64::consts::TAU;
|
||||
}
|
||||
while d < -std::f64::consts::PI {
|
||||
d += std::f64::consts::TAU;
|
||||
}
|
||||
st.turned += d;
|
||||
st.last = th;
|
||||
}
|
||||
Change::Rotate { axis: i, angle: snap(st.turned, 5f64.to_radians(), free) }
|
||||
}
|
||||
h => {
|
||||
let d = on_plane(proj, p, st.center, plane_normal(h)).map(|q| q - st.grab).unwrap_or(Vec3::ZERO);
|
||||
Change::Translate(Vec3::new(snap(d.x, step, free), snap(d.y, step, free), snap(d.z, step, free)))
|
||||
}
|
||||
};
|
||||
let done = r.drag_stopped() || !r.dragged() && !started;
|
||||
if done {
|
||||
ui.data_mut(|d| d.remove::<State>(id));
|
||||
} else {
|
||||
ui.data_mut(|d| d.insert_temp(id, st));
|
||||
}
|
||||
Some(Drag { handle: st.handle, change, started, done })
|
||||
}
|
||||
|
||||
fn arrow(painter: &egui::Painter, a: Pos2, b: Pos2, col: Color32, hot: bool) {
|
||||
let v = b - a;
|
||||
let len = v.length();
|
||||
if len < 1.0 {
|
||||
return;
|
||||
}
|
||||
let u = v / len;
|
||||
let n = vec2(-u.y, u.x);
|
||||
painter.line_segment([a, b], Stroke::new(if hot { 3.0 } else { 2.0 }, col));
|
||||
let head = 12.0f32.min(len * 0.5);
|
||||
painter.add(Shape::convex_polygon(vec![b + u * 2.0, b - u * head + n * head * 0.4, b - u * head - n * head * 0.4], col, Stroke::NONE));
|
||||
}
|
||||
|
||||
/// A 3×3 rotation (rows) about a unit axis.
|
||||
pub fn rotation(axis: Vec3, angle: f64) -> [[f64; 3]; 3] {
|
||||
let (c, s) = (angle.cos(), angle.sin());
|
||||
let t = 1.0 - c;
|
||||
let (x, y, z) = (axis.x, axis.y, axis.z);
|
||||
[
|
||||
[t * x * x + c, t * x * y - s * z, t * x * z + s * y],
|
||||
[t * x * y + s * z, t * y * y + c, t * y * z - s * x],
|
||||
[t * x * z - s * y, t * y * z + s * x, t * z * z + c],
|
||||
]
|
||||
}
|
||||
|
||||
pub fn mul(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
|
||||
let mut m = [[0.0; 3]; 3];
|
||||
for (i, row) in m.iter_mut().enumerate() {
|
||||
for (j, v) in row.iter_mut().enumerate() {
|
||||
*v = (0..3).map(|k| a[i][k] * b[k][j]).sum();
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
pub fn apply(m: &[[f64; 3]; 3], p: Vec3) -> Vec3 {
|
||||
Vec3::new(
|
||||
m[0][0] * p.x + m[0][1] * p.y + m[0][2] * p.z,
|
||||
m[1][0] * p.x + m[1][1] * p.y + m[1][2] * p.z,
|
||||
m[2][0] * p.x + m[2][1] * p.y + m[2][2] * p.z,
|
||||
)
|
||||
}
|
||||
|
||||
/// The axis and angle of a rotation (angle 0 for none).
|
||||
pub fn axis_angle(m: &[[f64; 3]; 3]) -> (Vec3, f64) {
|
||||
let tr = m[0][0] + m[1][1] + m[2][2];
|
||||
let angle = ((tr - 1.0) / 2.0).clamp(-1.0, 1.0).acos();
|
||||
if angle < 1e-12 {
|
||||
return (Vec3::Z, 0.0);
|
||||
}
|
||||
let v = Vec3::new(m[2][1] - m[1][2], m[0][2] - m[2][0], m[1][0] - m[0][1]);
|
||||
if let Some(a) = v.normalized().filter(|_| v.len() > 1e-6) {
|
||||
return (a, angle);
|
||||
}
|
||||
// Half a turn: the axis from the diagonal.
|
||||
let d = [((m[0][0] + 1.0) / 2.0).max(0.0).sqrt(), ((m[1][1] + 1.0) / 2.0).max(0.0).sqrt(), ((m[2][2] + 1.0) / 2.0).max(0.0).sqrt()];
|
||||
let (x, mut y, mut z) = (d[0], d[1], d[2]);
|
||||
if m[0][1] + m[1][0] < 0.0 && x > 1e-6 {
|
||||
y = -y;
|
||||
}
|
||||
if m[0][2] + m[2][0] < 0.0 && x > 1e-6 {
|
||||
z = -z;
|
||||
}
|
||||
if x <= 1e-6 && m[1][2] + m[2][1] < 0.0 {
|
||||
z = -z;
|
||||
}
|
||||
(Vec3::new(x, y, z).normalized().unwrap_or(Vec3::Z), angle)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rotations_compose_to_one_axis_angle() {
|
||||
let r = mul(&rotation(Vec3::Z, 0.7), &rotation(Vec3::X, 0.4));
|
||||
let (a, ang) = axis_angle(&r);
|
||||
let back = rotation(a, ang);
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
assert!((back[i][j] - r[i][j]).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
let h = rotation(Vec3::Y, std::f64::consts::PI);
|
||||
let (a, ang) = axis_angle(&h);
|
||||
assert!((ang - std::f64::consts::PI).abs() < 1e-9 && (a.y.abs() - 1.0).abs() < 1e-9);
|
||||
assert!((apply(&rotation(Vec3::Z, std::f64::consts::FRAC_PI_2), Vec3::X) - Vec3::Y).len() < 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,12 @@ pub mod dialogs;
|
||||
pub mod dialogs_appearance;
|
||||
pub mod dialogs_assembly;
|
||||
pub mod dialogs_motion;
|
||||
pub mod dialogs_move;
|
||||
pub mod dialogs_plastic;
|
||||
pub mod dialogs_sheet;
|
||||
pub mod dim_view;
|
||||
pub mod documents;
|
||||
pub mod gizmo;
|
||||
pub mod gpu;
|
||||
pub mod help;
|
||||
pub mod home;
|
||||
|
||||
@@ -48,7 +48,8 @@ scenarios!(
|
||||
s22_sheet_metal_fold,
|
||||
s23_configurations,
|
||||
s24_motion_study,
|
||||
s25_autosave_recovery
|
||||
s25_autosave_recovery,
|
||||
s27_move_triad
|
||||
);
|
||||
|
||||
/// Scenarios written ahead of their fixes or features (the user's open bugs): run with
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{"note": "Move opens with nothing moved; the triad's X arrow moves +20 X only; a ring turns 90 degrees"},
|
||||
{"call": "engine.execute", "params": {"command": "PrimitiveBox", "params": {"length": 20, "width": 30, "height": 10}}},
|
||||
{"call": "ui.view", "params": {"view": "iso"}},
|
||||
{"start": "FusionMoveCommand"},
|
||||
{"click": {"world": [10, 15, 10]}},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"errors": 0, "dialog": null, "bbox": {"body": "Body1", "min": [0, 0, 0], "max": [20, 30, 10]}}},
|
||||
{"start": "FusionMoveCommand"},
|
||||
{"click": {"world": [10, 15, 10]}},
|
||||
{"drag": [{"gizmo": "x"}, {"gizmo": "x", "by": [20, 0, 0]}]},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"errors": 0, "dialog": null, "bbox": {"body": "Body1", "min": [20, 0, 0], "max": [40, 30, 10]}}},
|
||||
{"start": "FusionMoveCommand"},
|
||||
{"click": {"world": [30, 15, 10]}},
|
||||
{"drag": [{"gizmo": "rz", "turn": 60}, {"gizmo": "rz", "turn": 150}], "steps": 12},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"errors": 0, "dialog": null, "bbox": {"body": "Body1", "min": [15, 5, 0], "max": [45, 25, 10]}}},
|
||||
{"shot": "s27_move_triad"}
|
||||
]
|
||||
Reference in new issue
Block a user