mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
Thanks @NOWN-DEV.
This commit is contained in:
@@ -11,6 +11,8 @@ use super::CommandSpec;
|
||||
use crate::params::{bad, bool_, str_, string_list, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
#[path = "component_matrix.rs"]
|
||||
pub(super) mod matrix;
|
||||
#[path = "component_properties.rs"]
|
||||
pub(super) mod properties;
|
||||
#[path = "component_transfer.rs"]
|
||||
@@ -52,7 +54,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.params("component (or occurrence): what to place again; path?: validates selected occurrence; parent?: component (default: the same parent); translate?: [x,y,z]. Copies the selected effective transform, including pending moves"),
|
||||
CommandSpec::new("component.paste_new", "Paste New", paste_new).icon("paste").params("component: independently copy its nested definition graph and history, materials and styles; occurrence?, path?: selected source placement; translate?: [x,y,z]. Reassigned external bodies become base geometry; retained external links and omitted boundary joints are reported"),
|
||||
CommandSpec::new("occurrence.move", "Move Occurrence", move_occurrence).icon("move")
|
||||
.params("occurrence; path?: exact placement; space?: local|world (default local); translate?: [x,y,z]; axis?: [x,y,z], angle?: radians or \"30 deg\", origin?: [x,y,z]; capture?: bool (default true; false keeps it pending until Capture Position). Nested occurrence definitions remain shared"),
|
||||
.params("occurrence; path?: exact placement; space?: local|world (default local); translate?: [x,y,z]; axis?: [x,y,z], angle?: radians or \"30 deg\", origin?: [x,y,z]; matrix?: [12 or 16 numbers] (instead of translate/axis/angle/origin: a rigid move as a row-major 3x4 rotation-and-translation or 4x4 matrix, flat or as rows; one that is a rotation only up to rounding is orthonormalised, a scale, shear or mirror is refused); tolerance?: largest accepted |RᵀR − I| (default 1e-9, at most 1e-3); capture?: bool (default true; false keeps it pending until Capture Position). Nested occurrence definitions remain shared"),
|
||||
CommandSpec::new("occurrence.ground", "Ground", ground).icon("ground").params("occurrence; grounded?: bool (default toggles)"),
|
||||
CommandSpec::new("component.capture_position", "Capture Position", capture).at("SOLID", "POSITION").icon("capture").params("keeps pending occurrence moves"),
|
||||
CommandSpec::new("component.revert_position", "Revert Position", revert).at("SOLID", "POSITION").icon("revert").noundo().params("occurrences?: [ids] (default all); drops selected pending occurrence moves"),
|
||||
@@ -416,21 +418,28 @@ fn move_occurrence(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = s.doc.components.iter().find(|c| c.id == group).map(|c| c.name.clone()).unwrap_or_default();
|
||||
return Err(bad(cmd, format!("{} is held in {name} by a rigid group: move {name}, or Unlock Contents (component.unlock) first", o.name)));
|
||||
}
|
||||
let t = move_vector(p, "translate", Vec3::ZERO, true, cmd)?;
|
||||
let axis = move_vector(p, "axis", Vec3::Z, false, cmd)?;
|
||||
let angle = match p.get("angle") {
|
||||
Some(Value::String(e)) => s.doc.eval(e, solidcraft_doc::expr::Kind::Angle).map_err(|e| bad(cmd, e.to_string()))?,
|
||||
None => 0.0,
|
||||
Some(value) => value.as_f64().ok_or_else(|| bad(cmd, "angle must be a number or angle expression"))?,
|
||||
let delta = if let Some(m) = p.get("matrix") {
|
||||
if let Some(k) = ["translate", "axis", "angle", "origin"].into_iter().find(|k| p.get(*k).is_some()) {
|
||||
return Err(bad(cmd, format!("give either matrix or {k}, not both")));
|
||||
}
|
||||
matrix::rigid(m, matrix::tolerance(p, cmd)?, cmd)?
|
||||
} else {
|
||||
let t = move_vector(p, "translate", Vec3::ZERO, true, cmd)?;
|
||||
let axis = move_vector(p, "axis", Vec3::Z, false, cmd)?;
|
||||
let angle = match p.get("angle") {
|
||||
Some(Value::String(e)) => s.doc.eval(e, solidcraft_doc::expr::Kind::Angle).map_err(|e| bad(cmd, e.to_string()))?,
|
||||
None => 0.0,
|
||||
Some(value) => value.as_f64().ok_or_else(|| bad(cmd, "angle must be a number or angle expression"))?,
|
||||
};
|
||||
if !(t.is_finite() && axis.is_finite() && angle.is_finite()) {
|
||||
return Err(bad(cmd, "non-finite move"));
|
||||
}
|
||||
if angle != 0.0 && axis.normalized().is_none() {
|
||||
return Err(bad(cmd, "rotation axis must have a finite nonzero length"));
|
||||
}
|
||||
let origin = move_vector(p, "origin", Vec3::ZERO, true, cmd)?;
|
||||
solidcraft_doc::rigid(t, origin, axis, angle)
|
||||
};
|
||||
if !(t.is_finite() && axis.is_finite() && angle.is_finite()) {
|
||||
return Err(bad(cmd, "non-finite move"));
|
||||
}
|
||||
if angle != 0.0 && axis.normalized().is_none() {
|
||||
return Err(bad(cmd, "rotation axis must have a finite nonzero length"));
|
||||
}
|
||||
let origin = move_vector(p, "origin", Vec3::ZERO, true, cmd)?;
|
||||
let delta = solidcraft_doc::rigid(t, origin, axis, angle);
|
||||
let space = match p.get("space") {
|
||||
None => "local",
|
||||
Some(Value::String(space)) => space.as_str(),
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Placement given as a matrix: a script's 3×4 (rotation rows and a translation column) or
|
||||
//! 4×4 transform, read as a rigid move. A rotation that is one only up to rounding is
|
||||
//! orthonormalised; a scale, shear or mirror is refused with the reason (a mirror is
|
||||
//! `solid.mirror`'s, not a placement's).
|
||||
|
||||
use serde_json::Value;
|
||||
use solidcraft_doc::{IDENTITY, Mat};
|
||||
|
||||
use crate::Result;
|
||||
use crate::params::bad;
|
||||
|
||||
/// The default largest departure from a rotation a matrix may show: |RᵀR − I| per entry.
|
||||
pub const TOLERANCE: f64 = 1e-9;
|
||||
/// The loosest tolerance a caller may ask for.
|
||||
const MAX_TOLERANCE: f64 = 1e-3;
|
||||
|
||||
/// The `tolerance` parameter: the default, or a finite positive number no looser than 1e-3.
|
||||
pub fn tolerance(p: &Value, cmd: &str) -> Result<f64> {
|
||||
match p.get("tolerance") {
|
||||
None | Some(Value::Null) => Ok(TOLERANCE),
|
||||
Some(v) => v
|
||||
.as_f64()
|
||||
.filter(|t| t.is_finite() && *t > 0.0 && *t <= MAX_TOLERANCE)
|
||||
.ok_or_else(|| bad(cmd, format!("tolerance must be a number above 0 and at most {MAX_TOLERANCE}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `matrix` as a rigid transform. It is row-major: 12 numbers (three rows of rotation
|
||||
/// and translation, `[r00, r01, r02, tx, r10, …, tz]`) or 16 (a fourth row `0, 0, 0, 1`), flat
|
||||
/// or as rows.
|
||||
pub fn rigid(v: &Value, tolerance: f64, cmd: &str) -> Result<Mat> {
|
||||
let numbers = numbers(v).ok_or_else(|| bad(cmd, "matrix must be 12 numbers (3x4, row by row) or 16 (4x4), flat or as rows"))?;
|
||||
if numbers.iter().any(|x| !x.is_finite()) {
|
||||
return Err(bad(cmd, "matrix must contain finite numbers"));
|
||||
}
|
||||
let at = |row: usize, col: usize| numbers.get(row * 4 + col).copied().unwrap_or(0.0);
|
||||
if numbers.len() == 16 {
|
||||
let last = [at(3, 0), at(3, 1), at(3, 2), at(3, 3) - 1.0];
|
||||
if last.iter().any(|x| x.abs() > tolerance) {
|
||||
return Err(bad(cmd, "matrix is not affine: its fourth row must be 0, 0, 0, 1 (a projective transform is not a placement)"));
|
||||
}
|
||||
}
|
||||
if (0..3).any(|r| at(r, 3).abs() > 1e9) {
|
||||
return Err(bad(cmd, "matrix translation exceeds supported numeric range"));
|
||||
}
|
||||
// r[row][col], then |RᵀR − I| measures how far it is from a rotation.
|
||||
let r: [[f64; 3]; 3] = std::array::from_fn(|row| std::array::from_fn(|col| at(row, col)));
|
||||
let deviation = (0..3)
|
||||
.flat_map(|i| (0..3).map(move |j| (i, j)))
|
||||
.map(|(i, j)| {
|
||||
let dot: f64 = (0..3).map(|k| r[k][i] * r[k][j]).sum();
|
||||
(dot - if i == j { 1.0 } else { 0.0 }).abs()
|
||||
})
|
||||
.fold(0.0, f64::max);
|
||||
let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1]) - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
|
||||
+ r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
|
||||
if det < 0.0 {
|
||||
return Err(bad(cmd, "matrix mirrors (its determinant is negative): a placement only turns and moves; mirror bodies with solid.mirror"));
|
||||
}
|
||||
if !deviation.is_finite() || deviation > tolerance {
|
||||
return Err(bad(
|
||||
cmd,
|
||||
format!("matrix is not rigid: it scales or shears by {deviation:.3e} (|RᵀR − I|), above the tolerance {tolerance:.0e}"),
|
||||
));
|
||||
}
|
||||
let rot = orthonormal(r);
|
||||
let mut m = IDENTITY;
|
||||
for (col, column) in m.iter_mut().enumerate().take(3) {
|
||||
for (row, cell) in column.iter_mut().enumerate().take(3) {
|
||||
*cell = rot[row][col];
|
||||
}
|
||||
}
|
||||
m[3] = [at(0, 3), at(1, 3), at(2, 3), 1.0];
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// The matrix's numbers, row by row: 12 or 16 of them, flat or nested as 3 or 4 rows of 4.
|
||||
fn numbers(v: &Value) -> Option<Vec<f64>> {
|
||||
let a = v.as_array()?;
|
||||
let flat: Vec<f64> = if a.iter().all(Value::is_array) {
|
||||
if !matches!(a.len(), 3 | 4) {
|
||||
return None;
|
||||
}
|
||||
let mut out = Vec::with_capacity(16);
|
||||
for row in a {
|
||||
let row = row.as_array().filter(|r| r.len() == 4)?;
|
||||
for x in row {
|
||||
out.push(x.as_f64()?);
|
||||
}
|
||||
}
|
||||
out
|
||||
} else {
|
||||
a.iter().map(Value::as_f64).collect::<Option<_>>()?
|
||||
};
|
||||
matches!(flat.len(), 12 | 16).then_some(flat)
|
||||
}
|
||||
|
||||
/// The rotation nearest to `r` (its polar factor), by Newton's iteration R ← (R + R⁻ᵀ) / 2,
|
||||
/// which converges quadratically from a near-rotation; a few rounds reach machine precision.
|
||||
fn orthonormal(mut r: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
|
||||
for _ in 0..4 {
|
||||
let Some(inv_t) = inverse_transpose(&r) else { break };
|
||||
for (row, inv_row) in r.iter_mut().zip(inv_t) {
|
||||
for (x, y) in row.iter_mut().zip(inv_row) {
|
||||
*x = 0.5 * (*x + y);
|
||||
}
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// (R⁻¹)ᵀ: the cofactor matrix over the determinant.
|
||||
fn inverse_transpose(r: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
|
||||
let a = |i: usize, j: usize| r[i % 3][j % 3];
|
||||
let cof: [[f64; 3]; 3] = std::array::from_fn(|i| std::array::from_fn(|j| a(i + 1, j + 1) * a(i + 2, j + 2) - a(i + 1, j + 2) * a(i + 2, j + 1)));
|
||||
let det: f64 = (0..3).map(|j| r[0][j] * cof[0][j]).sum();
|
||||
if !det.is_finite() || det.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
Some(cof.map(|row| row.map(|x| x / det)))
|
||||
}
|
||||
@@ -24,7 +24,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("file.insert_step", "Insert STEP", insert_step)
|
||||
.at("SOLID", "INSERT")
|
||||
.icon("import")
|
||||
.params("path: STEP, IGES, FreeCAD .FCStd or Rhino .3dm file (its bodies join the design as an Import base feature in a new component named after the file, placed in the active component; an assembly's products become nested components, a repeated part one component with an occurrence per placement, all held as one rigid group until component.unlock → component, component_name, occurrence, subcomponents?, rigid_joints?); name? (the feature's and the component's); up?: z|y (default z: the file's axes are kept; y: the file has Y up, so the import's component is turned a quarter turn about X, Y becoming Z → stand_up: its occurrence); names?: solid|product|instance (what the bodies are named after: the solid's own name, else the product's (default); the name of the product (part) that holds the body; the name of the assembly occurrence (instance), else the product's)"),
|
||||
.params("path: STEP, IGES, FreeCAD .FCStd or Rhino .3dm file (its bodies join the design as an Import base feature in a new component named after the file, placed in the active component; an assembly's products become nested components, a repeated part one component with an occurrence per placement, all held as one rigid group until component.unlock → component, component_name, occurrence, subcomponents?, rigid_joints?); name? (the feature's and the component's); up?: z|y (default z: the file's axes are kept; y: the file has Y up, so the import's component is turned a quarter turn about X, Y becoming Z → stand_up: its occurrence); names?: solid|product|instance (what the bodies are named after: the solid's own name, else the product's (default); the name of the product (part) that holds the body; the name of the assembly occurrence (instance), else the product's); matrix?: [12 or 16 numbers] (where the new component's occurrence sits in the active component, after `up`: a rigid row-major 3x4 rotation-and-translation or 4x4 matrix, flat or as rows, as occurrence.move takes it; a scale, shear or mirror is refused before the file is read); tolerance? (see occurrence.move)"),
|
||||
CommandSpec::new("file.insert_mesh", "Insert Mesh", insert_mesh)
|
||||
.at("SOLID", "INSERT")
|
||||
.icon("import")
|
||||
@@ -218,7 +218,20 @@ fn insert_step(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, "only STEP, IGES, FreeCAD and Rhino files (.step, .stp, .igs, .iges, .fcstd, .3dm) can be inserted"));
|
||||
}
|
||||
let options = step_options(p, cmd)?;
|
||||
add_step(s, path, str_(p, "name"), &options)
|
||||
let placement = match p.get("matrix") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(m) => Some(super::component::matrix::rigid(m, super::component::matrix::tolerance(p, cmd)?, cmd)?),
|
||||
};
|
||||
let mut out = add_step(s, path, str_(p, "name"), &options)?;
|
||||
if let Some(m) = placement {
|
||||
let occ = out["occurrence"].as_u64().ok_or_else(|| EngineError::Other("the import has no placement to move".into()))?;
|
||||
if let Some(o) = s.doc_mut().occurrences.iter_mut().find(|o| o.id == occ) {
|
||||
o.transform = solidcraft_doc::mat_mul(&m, &o.transform);
|
||||
out["transform"] = json!(o.transform);
|
||||
}
|
||||
s.refresh();
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -580,3 +580,85 @@ fn compact_component_list_counts_and_skips_identity_transforms() {
|
||||
// Body names on request.
|
||||
assert_eq!(block(&run(&mut s, "component.list", json!({"compact": true, "bodies": true})))["bodies"], json!(["Cube"]));
|
||||
}
|
||||
|
||||
/// A script's 3x4 placement matrix (rotation rows, then the translation column) moves an
|
||||
/// occurrence exactly as the same turn and move given as an axis, an angle and a translation.
|
||||
#[test]
|
||||
fn matrix_move_turns_a_quarter_and_moves() {
|
||||
let mut s = Session::default();
|
||||
let (_, occ) = created(&mut s, "Block");
|
||||
run(&mut s, "solid.box", json!({"length": 2, "width": 1, "height": 1}));
|
||||
// 90 degrees about Z, then (10, 20, 30): (x, y, z) goes to (10 - y, 20 + x, 30 + z).
|
||||
let r = run(&mut s, "occurrence.move", json!({"occurrence": occ, "matrix": [0, -1, 0, 10, 1, 0, 0, 20, 0, 0, 1, 30]}));
|
||||
let t = &r["transform"];
|
||||
for (col, expected) in [[0.0, 1.0, 0.0, 0.0], [-1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [10.0, 20.0, 30.0, 1.0]].iter().enumerate() {
|
||||
for (row, x) in expected.iter().enumerate() {
|
||||
near(&t[col][row], *x);
|
||||
}
|
||||
}
|
||||
// The box's centre (1, 0.5, 0.5) lands at (9.5, 21, 30.5).
|
||||
let props = run(&mut s, "occurrence.properties", json!({"occurrence": occ}));
|
||||
for (i, x) in [9.5, 21.0, 30.5].into_iter().enumerate() {
|
||||
near(&props["total"]["center_of_mass"][i], x);
|
||||
}
|
||||
// The same as axis/angle/translate, and the 4x4 form as rows says the same.
|
||||
let mut other = Session::default();
|
||||
let (_, o2) = created(&mut other, "Block");
|
||||
let r2 = run(&mut other, "occurrence.move", json!({"occurrence": o2, "axis": [0, 0, 1], "angle": "90 deg", "translate": [10, 20, 30]}));
|
||||
let mut rows = Session::default();
|
||||
let (_, o3) = created(&mut rows, "Block");
|
||||
let r3 = run(&mut rows, "occurrence.move", json!({"occurrence": o3, "matrix": [[0, -1, 0, 10], [1, 0, 0, 20], [0, 0, 1, 30], [0, 0, 0, 1]]}));
|
||||
for col in 0..4 {
|
||||
for row in 0..4 {
|
||||
near(&r2["transform"][col][row], t[col][row].as_f64().unwrap());
|
||||
near(&r3["transform"][col][row], t[col][row].as_f64().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A rotation that is one only up to rounding is orthonormalised; a scale, a shear or a mirror
|
||||
/// is refused with the reason, and the occurrence stays where it was.
|
||||
#[test]
|
||||
fn matrix_move_orthonormalises_rounding_and_refuses_what_is_not_rigid() {
|
||||
let mut s = Session::default();
|
||||
let (_, occ) = created(&mut s, "Block");
|
||||
let (sin, cos) = 30f64.to_radians().sin_cos();
|
||||
let e = 3e-10;
|
||||
let nearly = json!([cos + e, -sin, 0, 5, sin, cos - e, e, 0, 0, -e, 1.0 + e, 0]);
|
||||
let r = run(&mut s, "occurrence.move", json!({"occurrence": occ, "matrix": nearly, "space": "world"}));
|
||||
let m: solidcraft_doc::Mat = serde_json::from_value(r["transform"].clone()).unwrap();
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
let dot: f64 = (0..3).map(|k| m[i][k] * m[j][k]).sum();
|
||||
assert!((dot - f64::from(u8::from(i == j))).abs() < 1e-15, "column {i}·{j} = {dot}");
|
||||
}
|
||||
}
|
||||
assert!((m[0][0] - cos).abs() < 1e-9 && (m[0][1] - sin).abs() < 1e-9 && (m[3][0] - 5.0).abs() < 1e-12, "{m:?}");
|
||||
|
||||
let before = s.doc.occurrences.iter().find(|o| o.id == occ).unwrap().transform;
|
||||
let refused = |s: &mut Session, p: Value| s.execute("occurrence.move", &p).unwrap_err().to_string();
|
||||
let scaled = refused(&mut s, json!({"occurrence": occ, "matrix": [1.001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]}));
|
||||
assert!(scaled.contains("not rigid") && scaled.contains("2.00"), "{scaled}");
|
||||
let sheared = refused(&mut s, json!({"occurrence": occ, "matrix": [1, 0.01, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]}));
|
||||
assert!(sheared.contains("not rigid"), "{sheared}");
|
||||
let mirrored = refused(&mut s, json!({"occurrence": occ, "matrix": [-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]}));
|
||||
assert!(mirrored.contains("solid.mirror"), "{mirrored}");
|
||||
let projective = refused(&mut s, json!({"occurrence": occ, "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.5, 0, 0, 1]}));
|
||||
assert!(projective.contains("fourth row"), "{projective}");
|
||||
// A looser tolerance is the caller's to ask for, up to 1e-3.
|
||||
let rounded = json!([0.866, -0.5, 0, 0, 0.5, 0.866, 0, 0, 0, 0, 1, 0]);
|
||||
assert!(refused(&mut s, json!({"occurrence": occ, "matrix": rounded})).contains("not rigid"));
|
||||
for p in [
|
||||
json!({"occurrence": occ, "matrix": rounded, "tolerance": 1}),
|
||||
json!({"occurrence": occ, "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], "translate": [1, 0, 0]}),
|
||||
json!({"occurrence": occ, "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], "translate": [1, 0, 0]}),
|
||||
json!({"occurrence": occ, "matrix": [1, 0, 0, 1e12, 0, 1, 0, 0, 0, 0, 1, 0]}),
|
||||
json!({"occurrence": occ, "matrix": [[1, 0, 0, 0], [0, 1, 0, 0]]}),
|
||||
json!({"occurrence": occ, "matrix": "identity"}),
|
||||
json!({"occurrence": occ, "matrix": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}),
|
||||
] {
|
||||
assert!(s.execute("occurrence.move", &p).is_err(), "{p}");
|
||||
}
|
||||
assert_eq!(s.doc.occurrences.iter().find(|o| o.id == occ).unwrap().transform, before);
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": occ, "matrix": rounded, "tolerance": 1e-3}));
|
||||
}
|
||||
|
||||
@@ -202,3 +202,29 @@ fn the_bodies_are_named_after_the_solid_the_product_or_the_instance() {
|
||||
run(&mut again, "solid.move", json!({"bodies": ["Post-1"], "translate": [0, 0, 5]}));
|
||||
assert_eq!(body_names(&again), ["Plate-1", "Post-1"]);
|
||||
}
|
||||
|
||||
/// `matrix` places the inserted file's component, after `up`, in the same command (one undo
|
||||
/// step); a matrix that is not rigid is refused before anything is added.
|
||||
#[test]
|
||||
fn a_step_file_can_be_inserted_at_a_matrix() {
|
||||
let path = frame_file("matrix");
|
||||
let quarter = json!([0, -1, 0, 100, 1, 0, 0, 0, 0, 0, 1, 0]);
|
||||
let mut s = Session::default();
|
||||
let r = run(&mut s, "file.insert_step", json!({"path": path, "matrix": quarter}));
|
||||
// (x, y, z) goes to (100 - y, x, z): the frame (0..40, 0..34, 0..20) spans x 66..100, y 0..40.
|
||||
let (lo, hi) = extent(&s);
|
||||
assert!(near(lo, [66.0, 0.0, 0.0]) && near(hi, [100.0, 40.0, 20.0]), "{lo:?} {hi:?} {r}");
|
||||
assert_eq!(r["transform"][3][0], json!(100.0), "{r}");
|
||||
assert_eq!(s.undo.len(), 1);
|
||||
|
||||
// Stood up first ((0..40, -20..0, 0..34)), then turned and moved.
|
||||
let mut d = Session::default();
|
||||
run(&mut d, "file.insert_step", json!({"path": path, "up": "y", "matrix": quarter}));
|
||||
let (lo, hi) = extent(&d);
|
||||
assert!(near(lo, [100.0, 0.0, 0.0]) && near(hi, [120.0, 40.0, 34.0]), "{lo:?} {hi:?}");
|
||||
|
||||
let mut d = Session::default();
|
||||
let e = d.execute("file.insert_step", &json!({"path": path, "matrix": [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0]})).unwrap_err().to_string();
|
||||
assert!(e.contains("not rigid"), "{e}");
|
||||
assert!(d.doc.features.is_empty() && d.doc.occurrences.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user