UI scenarios, batch 2: edit an early sketch from the timeline, components with a revolute joint driven to 90°, sheet metal base and edge flange to flat pattern and DXF, STEP export, new design and STEP insert with a hole on the imported face, parameters table drives the model, sketch constraints workflow (dimensions, delete and undo, fix, construction), rectangular pattern. Harness: widgets found by their text through the accessibility tree, file pickers answered from the scenario's folder, model renders at shot steps (SOLVECRAFT_SCENARIO_SHOTS), query/file checks; ui.at takes a dimension. Fix: a click on a sketch point under a dimension line (a diameter through the centre) selects the point; only the dimension's text takes precedence
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
717b181082
commit
187e9a97b9
13 files changed
+1115
-6
No files matched your search
@@ -246,6 +246,12 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
"ui.at" => {
|
||||
// A scenario point: {world: [x,y,z]} | {sketch: [x,y]} | {plane: "XY"} (the middle of an
|
||||
// origin plane's square) → screen point.
|
||||
if let Some(d) = p.get("dimension").and_then(Value::as_str) {
|
||||
return match crate::dim_view::text_at(d) {
|
||||
Some(q) => ok(json!([q.x, q.y])),
|
||||
None => err(format!("dimension {d} is not shown")),
|
||||
};
|
||||
}
|
||||
let Some(rect) = app.viewport.rect else { return err("no viewport") };
|
||||
let proj = crate::viewport::projection(app, rect);
|
||||
let num = |v: &Value, i: usize| v.get(i).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
|
||||
@@ -65,6 +65,11 @@ fn seg_dist(p: Pos2, a: Pos2, b: Pos2) -> f32 {
|
||||
p.distance(a + ab * t)
|
||||
}
|
||||
|
||||
/// The dimension whose value text is under a screen position (its id).
|
||||
pub fn hit_text(pos: Pos2) -> Option<String> {
|
||||
DRAWN.with(|d| d.borrow().iter().find(|x| x.text.expand(2.0).contains(pos)).map(|x| x.id.clone()))
|
||||
}
|
||||
|
||||
/// The dimension under a screen position (its id).
|
||||
pub fn hit(pos: Pos2) -> Option<String> {
|
||||
DRAWN.with(|d| {
|
||||
@@ -88,6 +93,12 @@ pub fn selected(app: &SolveApp) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where the dimension driven by parameter `param` (or with constraint id `param`) shows its
|
||||
/// value (screen), as drawn last frame.
|
||||
pub fn text_at(param: &str) -> Option<Pos2> {
|
||||
DRAWN.with(|d| d.borrow().iter().find(|x| x.param.as_deref() == Some(param) || x.id == param).map(|x| x.text.center()))
|
||||
}
|
||||
|
||||
/// Dimension text boxes drawn last frame (for box selection).
|
||||
pub fn text_rects() -> Vec<(String, Rect)> {
|
||||
DRAWN.with(|d| d.borrow().iter().map(|x| (x.id.clone(), x.text)).collect())
|
||||
|
||||
@@ -4,28 +4,52 @@
|
||||
//!
|
||||
//! A scenario is a JSON list of steps:
|
||||
//! - `{"start": "Extrude"}`: a toolbar click; `{"key": "E", "shift"?, "cmd"?}`; `{"text": "20"}`
|
||||
//! - `{"widget": "Revolute"}`: click a dialog field, choice or button by its text
|
||||
//! - `{"click": AT, "double"?, "shift"?, "ctrl"?, "button"?}`, `{"move": AT}`,
|
||||
//! `{"drag": [AT, AT], "shift"?, "steps"?}` where AT is `[x, y]` (screen), `{"world": [x,y,z]}`
|
||||
//! or `{"sketch": [x,y]}` (the sketch being edited)
|
||||
//! - `{"call": "ui.view", "params": {…}}`: any other control request (`fail: true` expects an
|
||||
//! error)
|
||||
//! - `{"shot": "name"}`: a screenshot when run in a window (ignored headless); `{"debug": 1}`
|
||||
//! - `{"choose_file": "part.step"}`: the next Open picker returns that file of the scenario's
|
||||
//! folder (Save pickers save there)
|
||||
//! - `{"shot": "name"}`: a screenshot when run in a window; headless, a render of the model
|
||||
//! into `$SOLVECRAFT_SCENARIO_SHOTS` when that is set; `{"debug": 1}`
|
||||
//! prints the UI state
|
||||
//! - AT may also be `{"plane": "XY"}`: the middle of an origin plane's square
|
||||
//! - `{"expect": {…}}`: checks, see [`check`]
|
||||
//!
|
||||
//! The same files drive the live app over the control channel for screenshots.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_engine::Session;
|
||||
|
||||
use crate::control::{ControlRequest, Outcome, handle};
|
||||
use crate::{Services, SolveApp};
|
||||
|
||||
/// A fresh folder for one scenario's files.
|
||||
fn scratch_dir() -> PathBuf {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
static N: AtomicUsize = AtomicUsize::new(0);
|
||||
let d = std::env::temp_dir().join(format!("solvecraft-scenario-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
|
||||
let _ = std::fs::create_dir_all(&d);
|
||||
d
|
||||
}
|
||||
|
||||
/// The app in a headless egui context.
|
||||
pub struct Harness {
|
||||
pub app: SolveApp,
|
||||
pub ctx: egui::Context,
|
||||
/// The last frame's platform output (with its accessibility tree: dialog widgets are found
|
||||
/// by their text there).
|
||||
pub output: egui::PlatformOutput,
|
||||
/// Files the scenario saves and opens live here; file pickers answer from it.
|
||||
pub dir: PathBuf,
|
||||
/// The file the next Open picker returns (a `choose_file` step).
|
||||
next_file: Rc<RefCell<Option<String>>>,
|
||||
time: f64,
|
||||
size: egui::Vec2,
|
||||
}
|
||||
@@ -41,9 +65,19 @@ impl Harness {
|
||||
let mut h = Harness {
|
||||
app: SolveApp::new(Session::default(), Services::default()),
|
||||
ctx: egui::Context::default(),
|
||||
output: Default::default(),
|
||||
dir: scratch_dir(),
|
||||
next_file: Rc::new(RefCell::new(None)),
|
||||
time: 0.0,
|
||||
size: egui::vec2(1600.0, 1000.0),
|
||||
};
|
||||
// File pickers answer like a person would: save into the scenario's folder, open the
|
||||
// file the scenario chose.
|
||||
let dir = h.dir.clone();
|
||||
h.app.services.pick_save = Some(Box::new(move |name, _| Some(dir.join(name).to_string_lossy().into_owned())));
|
||||
let next = h.next_file.clone();
|
||||
h.app.services.pick_open = Some(Box::new(move || next.borrow_mut().take()));
|
||||
h.ctx.enable_accesskit();
|
||||
h.frames(3);
|
||||
h
|
||||
}
|
||||
@@ -65,6 +99,7 @@ impl Harness {
|
||||
});
|
||||
// Nothing is painted: drop the frame's texture changes.
|
||||
out.textures_delta.clear();
|
||||
self.output = out.platform_output;
|
||||
}
|
||||
|
||||
pub fn frames(&mut self, n: usize) {
|
||||
@@ -96,6 +131,22 @@ impl Harness {
|
||||
out
|
||||
}
|
||||
|
||||
/// The centre of the visible widget whose label or value is `text` (the last one drawn wins:
|
||||
/// popups are drawn after what opened them).
|
||||
pub fn widget(&self, text: &str) -> Option<[f64; 2]> {
|
||||
let tree = self.output.accesskit_update.as_ref()?;
|
||||
tree.nodes
|
||||
.iter()
|
||||
.rev()
|
||||
// A menu button's label ends with its shortcut ("Save Ctrl+S").
|
||||
.filter(|(_, n)| {
|
||||
n.label().is_some_and(|l| l == text || l.strip_prefix(text).is_some_and(|r| r.starts_with(' '))) || n.value() == Some(text)
|
||||
})
|
||||
.filter_map(|(_, n)| n.bounds())
|
||||
.find(|b| b.x1 > b.x0 && b.y1 > b.y0)
|
||||
.map(|b| [(b.x0 + b.x1) / 2.0, (b.y0 + b.y1) / 2.0])
|
||||
}
|
||||
|
||||
/// A step's point on screen.
|
||||
pub fn at(&mut self, v: &Value) -> Result<[f64; 2], String> {
|
||||
if v.is_array() {
|
||||
@@ -129,6 +180,11 @@ impl Harness {
|
||||
p["button"] = b.clone();
|
||||
}
|
||||
self.call("ui.click", p)
|
||||
} else if let Some(text) = s.get("widget").and_then(Value::as_str) {
|
||||
// A dialog field, choice or button, found by its text.
|
||||
let [x, y] = self.widget(text).ok_or_else(|| format!("no widget `{text}` on screen"))?;
|
||||
self.call("ui.move", json!({"x": x, "y": y}));
|
||||
self.call("ui.click", json!({"x": x, "y": y}))
|
||||
} else if let Some(at) = s.get("move") {
|
||||
let [x, y] = self.at(at)?;
|
||||
self.call("ui.move", json!({"x": x, "y": y}))
|
||||
@@ -139,7 +195,19 @@ impl Harness {
|
||||
self.call("ui.drag", json!({"x0": x0, "y0": y0, "x1": x1, "y1": y1, "steps": steps, "shift": flag("shift")}))
|
||||
} else if let Some(m) = s.get("call").and_then(Value::as_str) {
|
||||
self.call(m, s.get("params").cloned().unwrap_or(json!({})))
|
||||
} else if s.get("shot").is_some() {
|
||||
} else if let Some(name) = s.get("shot").and_then(Value::as_str) {
|
||||
// With SOLVECRAFT_SCENARIO_SHOTS set: a render of the model as the camera sees it.
|
||||
if let Ok(dir) = std::env::var("SOLVECRAFT_SCENARIO_SHOTS") {
|
||||
let path = std::path::Path::new(&dir).join(format!("{name}.png"));
|
||||
self.call("ui.render", json!({"path": path.to_string_lossy(), "width": 1000, "height": 700}));
|
||||
}
|
||||
return Ok(());
|
||||
} else if let Some(f) = s.get("choose_file").and_then(Value::as_str) {
|
||||
*self.next_file.borrow_mut() = Some(self.dir.join(f).to_string_lossy().into_owned());
|
||||
return Ok(());
|
||||
} else if let Some(m) = s.get("print").and_then(Value::as_str) {
|
||||
let r = self.call(m, s.get("params").cloned().unwrap_or(json!({})));
|
||||
eprintln!("print {m}: {r}");
|
||||
return Ok(());
|
||||
} else if s.get("debug").is_some() {
|
||||
let ui = self.call("ui.inspect", json!({}));
|
||||
@@ -148,6 +216,16 @@ impl Harness {
|
||||
"debug: dialog {} tool {} hover {}\nselection {}\nstatus {:?}",
|
||||
ui["result"]["dialog"], ui["result"]["tool"], ui["result"]["hover"], sel["result"]["selection"], self.app.status
|
||||
);
|
||||
if s["debug"] == json!("widgets") {
|
||||
let texts: Vec<String> = self
|
||||
.output
|
||||
.accesskit_update
|
||||
.iter()
|
||||
.flat_map(|t| t.nodes.iter())
|
||||
.filter_map(|(_, n)| n.label().or(n.value()).map(|l| format!("{:?} {l}", n.role())))
|
||||
.collect();
|
||||
eprintln!("widgets: {texts:?}");
|
||||
}
|
||||
return Ok(());
|
||||
} else if let Some(e) = s.get("expect") {
|
||||
return check(self, e);
|
||||
@@ -181,7 +259,8 @@ fn approx(got: f64, want: &Value, what: &str) -> Result<(), String> {
|
||||
/// Checks: `bodies` (count), `volume` / `area` (total, number or [value, rel tol]), `faces`,
|
||||
/// `features` (count), `errors` (timeline features in error, default 0), `dialog` (null: none
|
||||
/// open, or a word its kind must contain), `tool` (null or command id), `sketching` (bool),
|
||||
/// `selection` (count), `param` ({name: value}), `sketch_status` (solved|…), `dof`.
|
||||
/// `selection` (count), `param` ({name: value}), `sketch_status` (solved|…), `dof`, `query`
|
||||
/// ({command, params?, path, value}: a command's result).
|
||||
pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
|
||||
let doc = h.call("document.inspect", json!({"measure": true}))["result"].clone();
|
||||
let ui = h.call("ui.inspect", json!({}))["result"].clone();
|
||||
@@ -262,6 +341,34 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
|
||||
return Err(format!("{k}: got {}, want {v}", si[key]));
|
||||
}
|
||||
}
|
||||
"file" => {
|
||||
// A file the scenario saved: it exists and is not empty.
|
||||
let p = h.dir.join(v.as_str().unwrap_or_default());
|
||||
if std::fs::metadata(&p).map_or(0, |m| m.len()) == 0 {
|
||||
return Err(format!("no file {}", p.display()));
|
||||
}
|
||||
}
|
||||
"query" => {
|
||||
// {command, params?, path: [keys or indices], value}: a command's result at path.
|
||||
let cmd = v["command"].as_str().ok_or("query needs `command`")?;
|
||||
let r = h.call("engine.execute", json!({"command": cmd, "params": v.get("params").cloned().unwrap_or(json!({}))}));
|
||||
let mut at = &r["result"];
|
||||
for k in v["path"].as_array().into_iter().flatten() {
|
||||
at = match k {
|
||||
Value::Number(n) => &at[n.as_u64().unwrap_or(0) as usize],
|
||||
Value::String(s) => &at[s.as_str()],
|
||||
_ => &Value::Null,
|
||||
};
|
||||
}
|
||||
let want = &v["value"];
|
||||
let ok = match (at.as_f64(), want.as_f64()) {
|
||||
(Some(a), Some(w)) => (a - w).abs() <= 1e-6 * w.abs().max(1.0),
|
||||
_ => at == want,
|
||||
};
|
||||
if !ok {
|
||||
return Err(format!("query {cmd} {}: got {at}, want {want}", v["path"]));
|
||||
}
|
||||
}
|
||||
other => return Err(format!("unknown check `{other}`")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,7 +1170,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// Dimensions and constraint glyphs sit on top of the geometry.
|
||||
let off = |k: &str| app.ui.pick_off.iter().any(|x| x == k);
|
||||
let sketching = app.session.active_sketch.is_some();
|
||||
let on_dim = crate::dim_view::hit(p).filter(|_| sketching && !off("dimensions"));
|
||||
// A dimension's text wins; its lines only where no sketch point or curve is (a
|
||||
// diameter's line runs through the centre point).
|
||||
let on_geometry = matches!(cand.as_ref().map(|c| &c.0), Some(Hit::SketchPoint { .. } | Hit::SketchCurve { .. }));
|
||||
let on_dim = crate::dim_view::hit_text(p)
|
||||
.or_else(|| if on_geometry { None } else { crate::dim_view::hit(p) })
|
||||
.filter(|_| sketching && !off("dimensions"));
|
||||
let on_glyph = crate::sketch_tools::glyph_at(p).filter(|_| sketching && !off("constraints"));
|
||||
if !crate::sketch3d::click(app, &proj, p) {
|
||||
match on_dim.or(on_glyph) {
|
||||
|
||||
@@ -22,4 +22,17 @@ macro_rules! scenarios {
|
||||
};
|
||||
}
|
||||
|
||||
scenarios!(s01_box_fillet_hole_shell, s02_revolve_on_xz, s03_sketch_on_face_cut, s04_mirror_body, s05_delete_and_undo);
|
||||
scenarios!(
|
||||
s01_box_fillet_hole_shell,
|
||||
s02_revolve_on_xz,
|
||||
s03_sketch_on_face_cut,
|
||||
s04_mirror_body,
|
||||
s05_delete_and_undo,
|
||||
s06_edit_early_feature,
|
||||
s07_components_joint_drive,
|
||||
s08_sheet_metal_flat_dxf,
|
||||
s09_step_roundtrip_hole,
|
||||
s10_parameters_drive_model,
|
||||
s11_sketch_constraints,
|
||||
s12_pattern_bodies
|
||||
);
|
||||
@@ -0,0 +1,145 @@
|
||||
[
|
||||
{
|
||||
"start": "SketchCreate"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"plane": "XY"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ShapeRectangleTwoPoint"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
40,
|
||||
30
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchDimension"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
20,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "40"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "Extrude"
|
||||
},
|
||||
{
|
||||
"text": "10"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"start": "FusionFilletEdgesCommand"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"world": [
|
||||
40,
|
||||
15,
|
||||
10
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "2"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 1,
|
||||
"errors": 0,
|
||||
"features": 3,
|
||||
"volume": [
|
||||
11974.25,
|
||||
1e-05
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"call": "ui.editFeature",
|
||||
"params": {
|
||||
"feature": "Sketch1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"sketching": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"dimension": "d1"
|
||||
},
|
||||
"double": true
|
||||
},
|
||||
{
|
||||
"text": "60"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"param": {
|
||||
"d1": 60
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s06_edit_sketch"
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"sketching": false,
|
||||
"errors": 0,
|
||||
"bodies": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s06_updated"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,224 @@
|
||||
[
|
||||
{
|
||||
"start": "FusionCreateNewComponentCommand"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"start": "SketchCreate"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"plane": "XY"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ShapeRectangleTwoPoint"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
20,
|
||||
20
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "Extrude"
|
||||
},
|
||||
{
|
||||
"text": "10"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"call": "ui.menu",
|
||||
"params": {
|
||||
"x": 100,
|
||||
"y": 170,
|
||||
"target": {
|
||||
"type": "component",
|
||||
"id": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"call": "ui.menuPick",
|
||||
"params": {
|
||||
"item": "Activate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "FusionCreateNewComponentCommand"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"start": "SketchCreate"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"plane": "XZ"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ShapeRectangleTwoPoint"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
30,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
50,
|
||||
10
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "Extrude"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"world": [
|
||||
40,
|
||||
0,
|
||||
5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "20"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 2,
|
||||
"errors": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "JointAssembleCmdNew"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"world": [
|
||||
10,
|
||||
10,
|
||||
10
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"world": [
|
||||
40,
|
||||
0,
|
||||
5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget": "Rigid"
|
||||
},
|
||||
{
|
||||
"widget": "Revolute"
|
||||
},
|
||||
{
|
||||
"shot": "s07_joint"
|
||||
},
|
||||
{
|
||||
"widget": "OK"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dialog": null,
|
||||
"errors": 0,
|
||||
"query": {
|
||||
"command": "joint.list",
|
||||
"path": [
|
||||
"joints",
|
||||
0,
|
||||
"type"
|
||||
],
|
||||
"value": "revolute"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "FusionMoveJointsCommand"
|
||||
},
|
||||
{
|
||||
"widget": "0 deg"
|
||||
},
|
||||
{
|
||||
"key": "A",
|
||||
"cmd": true
|
||||
},
|
||||
{
|
||||
"text": "90"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"query": {
|
||||
"command": "joint.list",
|
||||
"path": [
|
||||
"joints",
|
||||
0,
|
||||
"values",
|
||||
0,
|
||||
"value"
|
||||
],
|
||||
"value": 90
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s07_driven"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,116 @@
|
||||
[
|
||||
{
|
||||
"call": "ui.set",
|
||||
"params": {
|
||||
"tab": "SHEET METAL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "SketchCreate"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"plane": "XY"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ShapeRectangleTwoPoint"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
60,
|
||||
40
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "FusionSheetMetalFlangeCommand"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"world": [
|
||||
30,
|
||||
20,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 1,
|
||||
"errors": 0,
|
||||
"dialog": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"volume": 6000
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s08_base"
|
||||
},
|
||||
{
|
||||
"start": "FusionSheetMetalFlangeCommand"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"world": [
|
||||
30,
|
||||
0,
|
||||
2.5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 1,
|
||||
"errors": 0,
|
||||
"dialog": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s08_flange"
|
||||
},
|
||||
{
|
||||
"start": "FusionSheetMetalFlatPatternCmd"
|
||||
},
|
||||
{
|
||||
"widget": "Export DXF\u2026"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"file": "Untitled-Body1.dxf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s08_flat"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,126 @@
|
||||
[
|
||||
{
|
||||
"start": "SketchCreate"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"plane": "XY"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ShapeRectangleTwoPoint"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
40,
|
||||
30
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"start": "Extrude"
|
||||
},
|
||||
{
|
||||
"text": "15"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 1,
|
||||
"volume": 18000
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": [
|
||||
127,
|
||||
16
|
||||
]
|
||||
},
|
||||
{
|
||||
"widget": "Export STEP\u2026"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"file": "Untitled.step"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "N",
|
||||
"cmd": true
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 0,
|
||||
"features": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"choose_file": "Untitled.step"
|
||||
},
|
||||
{
|
||||
"click": [
|
||||
127,
|
||||
16
|
||||
]
|
||||
},
|
||||
{
|
||||
"widget": "Insert STEP\u2026"
|
||||
},
|
||||
{
|
||||
"debug": 1
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 1,
|
||||
"volume": 18000
|
||||
}
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "FusionHoleCommand"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"world": [
|
||||
20,
|
||||
15,
|
||||
15
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"bodies": 1,
|
||||
"errors": 0,
|
||||
"dialog": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s09_hole_on_import"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,111 @@
|
||||
[
|
||||
{
|
||||
"start": "SketchCreate"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"plane": "XY"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ShapeRectangleTwoPoint"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
40,
|
||||
30
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchDimension"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
20,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "40"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"start": "Extrude"
|
||||
},
|
||||
{
|
||||
"text": "10"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"volume": 12000
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ChangeParameterCommand"
|
||||
},
|
||||
{
|
||||
"shot": "s10_params"
|
||||
},
|
||||
{
|
||||
"widget": "40"
|
||||
},
|
||||
{
|
||||
"key": "A",
|
||||
"cmd": true
|
||||
},
|
||||
{
|
||||
"text": "55"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"debug": 1
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"param": {
|
||||
"d1": 55
|
||||
},
|
||||
"volume": 16500,
|
||||
"errors": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget": "Close window"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dialog": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s10_updated"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,222 @@
|
||||
[
|
||||
{
|
||||
"start": "SketchCreate"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"plane": "XY"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "DrawPolyline"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
40,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
40,
|
||||
25
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
25
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dof": 2,
|
||||
"sketch_status": "solved"
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "CircleCenterRadius"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
20,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
25,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"start": "SketchDimension"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
20,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "40"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
40,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "25"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
25,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": "10"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dof": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
20,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"selection": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Delete",
|
||||
"fail": false
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dof": 0,
|
||||
"selection": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Z",
|
||||
"cmd": true
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dof": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "ConstraintFix"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
20,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dof": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s11_constrained"
|
||||
},
|
||||
{
|
||||
"click": {
|
||||
"sketch": [
|
||||
0,
|
||||
12
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "X"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"dof": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"start": "SketchStop"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"sketching": false,
|
||||
"errors": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"shot": "s11_done"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{"start": "SketchCreate"},
|
||||
{"click": {"plane": "XY"}},
|
||||
{"start": "CircleCenterRadius"},
|
||||
{"click": {"sketch": [10, 10]}},
|
||||
{"click": {"sketch": [14, 10]}},
|
||||
{"key": "Escape"},
|
||||
{"start": "SketchStop"},
|
||||
{"call": "ui.view", "params": {"view": "iso"}},
|
||||
{"start": "Extrude"},
|
||||
{"text": "8"},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"bodies": 1}},
|
||||
{"start": "PatternRectangular"},
|
||||
{"click": {"world": [10, 10, 8]}},
|
||||
{"call": "ui.dialogInput", "params": {"index": 1}},
|
||||
{"click": {"axis": "X"}},
|
||||
{"debug": "widgets"},
|
||||
{"key": "Enter"},
|
||||
{"debug": 1},
|
||||
{"expect": {"errors": 0, "dialog": null, "volume": [1206.37, 1e-3]}},
|
||||
{"shot": "s12_pattern"}
|
||||
]
|
||||
@@ -36,7 +36,7 @@ Commands never open dialogs when run this way. `solvecraft-cli commands` prints
|
||||
| `ui.triad` | | screen points of the 3D sketch move triad's handles (`x`, `y`, `z`, `plane`) |
|
||||
| `ui.sketchToScreen` | `points`: `[[x, y]…]` in active-sketch coordinates | their screen points (null when behind the camera) |
|
||||
| `ui.dialogInput` | `index` | what clicking an input's Select chip does: picks go to that input |
|
||||
| `ui.at` | `world: [x,y,z]` \| `sketch: [x,y]` \| `plane: "XY"` \| `axis: "Z"` | the screen point (a scenario step's target; see `crates/ui-egui/src/scenario.rs`) |
|
||||
| `ui.at` | `world: [x,y,z]` \| `sketch: [x,y]` \| `plane: "XY"` \| `axis: "Z"` \| `dimension: "d1"` (its value text) | the screen point (a scenario step's target; see `crates/ui-egui/src/scenario.rs`) |
|
||||
| `ui.editFeature` | `feature` (id or name) | what double-clicking a timeline item does: rolls back to it and opens its dialog filled in (sketches open in sketch mode) |
|
||||
| `ui.scroll` | `x`, `y`, `delta?` | wheel zoom at the cursor |
|
||||
| `ui.key` | `key` (egui key name, `Enter`, `Escape`…), `cmd?`, `shift?` | |
|
||||
|
||||
Reference in new issue
Block a user