UI scenarios, batch 3: plastic enclosure (shell, boss, lip, snap fit), appearance then STEP and 3MF export with the colour, start page sample edited, saved and reopened, two design tabs with their own undo, the S toolbox and V visibility, 3D sketch lines snapped to the model then a pipe along them, canvas inserted and calibrated, section analysis with a joint driven. Harness: until (waits for background work), make_image, widget near a label, checks for files' text, start page, open designs, design name, hidden bodies, canvas width and section. Fixes: Enter in the S command search runs the first match (it read the focus after taking it back); Sketch Palette toggles are checkboxes with their labels (click the text to toggle); a dialog test no longer panics dropping unpainted textures

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 11:04:35 +00:00
1 parent 4c3a5d9853
commit 829c4a2811
13 files changed
+1148 -76

No files matched your search

+4 -1
View File
@@ -191,7 +191,10 @@ fn ok_click_survives_a_busy_preview() {
let frame = |app: &mut SolveApp, busy: bool, events: Vec<egui::Event>| {
app.preview.busy = busy;
let input = egui::RawInput { screen_rect: Some(screen), events, ..Default::default() };
ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx()))
let mut out = ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx()));
// Nothing is painted: drop the texture changes (an unapplied delta panics on drop).
out.textures_delta.clear();
out
};
frame(&mut app, false, vec![]);
let out = frame(&mut app, false, vec![]);
+3 -1
View File
@@ -103,9 +103,11 @@ pub fn popup(app: &mut SolveApp, ctx: &egui::Context) {
.open(&mut open)
.show(ctx, |ui| {
let r = ui.add(egui::TextEdit::singleline(&mut app.palette.text).hint_text("command name").desired_width(380.0));
// Read Enter before taking the focus back (lost_focus reads the focus as it is now).
let entered = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
r.request_focus();
let list = matches(&app.palette.text);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
if entered {
picked = list.first().map(|x| x.0);
}
egui::ScrollArea::vertical().max_height(280.0).show(ui, |ui| {
+166 -71
View File
@@ -4,19 +4,21 @@
//!
//! 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
//! - `{"widget": "Revolute", "near"?: "Type"}`: click a dialog field, choice or button by its
//! text (with `near`: the one in that label's row)
//! - `{"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)
//! - `{"make_image": "pic.png"}`: a picture in the scenario's folder
//! - `{"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`]
//! - `{"expect": {…}}`: checks, see [`check`]; `{"until": {…}}` waits (frames) until they pass
//!
//! The same files drive the live app over the control channel for screenshots.
@@ -60,6 +62,15 @@ impl Default for Harness {
}
}
impl Drop for Harness {
/// The scenario's files go with it (kept with SOLVECRAFT_SCENARIO_KEEP set).
fn drop(&mut self) {
if std::env::var_os("SOLVECRAFT_SCENARIO_KEEP").is_none() {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
}
impl Harness {
pub fn new() -> Harness {
let mut h = Harness {
@@ -134,7 +145,21 @@ impl Harness {
/// 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()?;
self.widgets(text).into_iter().next()
}
/// The widget showing `text` nearest the label `near` (the field in a label's row).
pub fn widget_near(&self, text: &str, near: &str) -> Option<[f64; 2]> {
let [lx, ly] = self.widgets(near).into_iter().next()?;
self.widgets(text).into_iter().filter(|[x, _]| *x > lx).min_by(|a, b| {
let d = |p: &[f64; 2]| (p[1] - ly).abs() * 4.0 + (p[0] - lx).abs();
d(a).total_cmp(&d(b))
})
}
/// Centres of the visible widgets whose label or value is `text`, last drawn first.
fn widgets(&self, text: &str) -> Vec<[f64; 2]> {
let Some(tree) = self.output.accesskit_update.as_ref() else { return Vec::new() };
tree.nodes
.iter()
.rev()
@@ -143,8 +168,9 @@ impl Harness {
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)
.filter(|b| b.x1 > b.x0 && b.y1 > b.y0)
.map(|b| [(b.x0 + b.x1) / 2.0, (b.y0 + b.y1) / 2.0])
.collect()
}
/// A step's point on screen.
@@ -165,73 +191,100 @@ impl Harness {
/// Run one step.
pub fn step(&mut self, s: &Value) -> Result<(), String> {
let flag = |k: &str| s.get(k).and_then(Value::as_bool).unwrap_or(false);
let res = if let Some(id) = s.get("start").and_then(Value::as_str) {
self.call("ui.start", json!({ "command": id }))
} else if let Some(k) = s.get("key").and_then(Value::as_str) {
self.call("ui.key", json!({"key": k, "shift": flag("shift"), "cmd": flag("cmd")}))
} else if let Some(t) = s.get("text").and_then(Value::as_str) {
self.call("ui.text", json!({ "text": t }))
} else if let Some(at) = s.get("click") {
let [x, y] = self.at(at)?;
// Hover first, as a hand does.
self.call("ui.move", json!({"x": x, "y": y}));
let mut p = json!({"x": x, "y": y, "double": flag("double"), "shift": flag("shift"), "ctrl": flag("ctrl")});
if let Some(b) = s.get("button") {
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}))
} else if let Some(d) = s.get("drag") {
let [x0, y0] = self.at(d.get(0).unwrap_or(&Value::Null))?;
let [x1, y1] = self.at(d.get(1).unwrap_or(&Value::Null))?;
let steps = s.get("steps").cloned().unwrap_or(json!(8));
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 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!({}));
let sel = self.call("ui.selection", json!({}));
eprintln!(
"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);
} else {
return Err(format!("unknown step {s}"));
};
let res =
if let Some(id) = s.get("start").and_then(Value::as_str) {
self.call("ui.start", json!({ "command": id }))
} else if let Some(k) = s.get("key").and_then(Value::as_str) {
self.call("ui.key", json!({"key": k, "shift": flag("shift"), "cmd": flag("cmd")}))
} else if let Some(t) = s.get("text").and_then(Value::as_str) {
self.call("ui.text", json!({ "text": t }))
} else if let Some(at) = s.get("click") {
let [x, y] = self.at(at)?;
// Hover first, as a hand does.
self.call("ui.move", json!({"x": x, "y": y}));
let mut p = json!({"x": x, "y": y, "double": flag("double"), "shift": flag("shift"), "ctrl": flag("ctrl")});
if let Some(b) = s.get("button") {
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 found = match s.get("near").and_then(Value::as_str) {
Some(near) => self.widget_near(text, near),
None => self.widget(text),
};
let [x, y] = found.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}))
} else if let Some(d) = s.get("drag") {
let [x0, y0] = self.at(d.get(0).unwrap_or(&Value::Null))?;
let [x1, y1] = self.at(d.get(1).unwrap_or(&Value::Null))?;
let steps = s.get("steps").cloned().unwrap_or(json!(8));
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 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("make_image").and_then(Value::as_str) {
// A picture to insert (a 200 x 100 checkerboard PNG in the scenario's folder).
let img = image::RgbImage::from_fn(200, 100, |x, y| {
if (x / 20 + y / 20) % 2 == 0 { image::Rgb([230, 230, 230]) } else { image::Rgb([60, 90, 160]) }
});
img.save(self.dir.join(f)).map_err(|e| format!("make_image: {e}"))?;
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!({}));
let sel = self.call("ui.selection", json!({}));
eprintln!(
"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)| {
let at = n.bounds().map(|b| format!(" @{:.0},{:.0}", (b.x0 + b.x1) / 2.0, (b.y0 + b.y1) / 2.0)).unwrap_or_default();
n.label().or(n.value()).map(|l| format!("{:?} {l}{at}", n.role()))
})
.collect();
eprintln!("widgets: {texts:?}");
}
return Ok(());
} else if let Some(e) = s.get("until") {
// Background work (a sample being built, a preview): frames until the checks pass.
let mut last = String::new();
for _ in 0..600 {
match check(self, e) {
Ok(()) => return Ok(()),
Err(m) => last = m,
}
std::thread::sleep(std::time::Duration::from_millis(20));
self.frames(2);
}
return Err(format!("timed out: {last}"));
} else if let Some(e) = s.get("expect") {
return check(self, e);
} else {
return Err(format!("unknown step {s}"));
};
let failed = res["ok"] != json!(true);
if failed != flag("fail") {
return Err(format!("{s}: {res}"));
@@ -348,6 +401,48 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
return Err(format!("no file {}", p.display()));
}
}
"file_contains" => {
// {file, text}: a saved text file holds the text.
let p = h.dir.join(v["file"].as_str().unwrap_or_default());
let body = std::fs::read_to_string(&p).unwrap_or_default();
let t = v["text"].as_str().unwrap_or_default();
if !body.contains(t) {
return Err(format!("{} does not contain `{t}`", p.display()));
}
}
"hidden" => {
// Bodies hidden in the view.
let h = &ui["ui"]["hidden_bodies"];
let h = if h.is_null() { &ui["ui"]["hiddenBodies"] } else { h };
if h != v {
return Err(format!("hidden bodies: got {h}, want {v}"));
}
}
"canvas_width" => {
// The first canvas's width (mm).
let w = h.app.session.doc.canvases.first().map(|c| c.width).unwrap_or(f64::NAN);
approx(w, v, "canvas width")?;
}
"section" => {
if h.app.session.section.is_some() != v.as_bool().unwrap_or(false) {
return Err(format!("section view: want {v}"));
}
}
"home" => {
if ui["home"] != *v {
return Err(format!("start page open: got {}, want {v}", ui["home"]));
}
}
"documents" => {
if ui["documents"] != *v {
return Err(format!("open designs: got {}, want {v}", ui["documents"]));
}
}
"name" => {
if doc["name"] != *v {
return Err(format!("design name: got {}, want {v}", doc["name"]));
}
}
"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`")?;
+2 -2
View File
@@ -167,9 +167,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
ui.end_row();
for (k, label) in TOGGLES {
ui.label(label);
// The label is part of the checkbox: clicking it toggles too.
let mut on = shown(k);
if ui.checkbox(&mut on, "").changed() {
if ui.checkbox(&mut on, label).changed() {
set = Some((k.to_string(), on));
}
ui.end_row();
+9 -1
View File
@@ -34,5 +34,13 @@ scenarios!(
s09_step_roundtrip_hole,
s10_parameters_drive_model,
s11_sketch_constraints,
s12_pattern_bodies
s12_pattern_bodies,
s13_plastic_enclosure,
s14_appearance_export,
s15_start_page_sample_save_reopen,
s16_tabs_separate_undo,
s17_toolbox_and_visibility,
s18_3d_sketch_pipe,
s19_canvas_calibrate,
s20_section_with_joint_drive
);
@@ -0,0 +1,187 @@
[
{
"call": "ui.set",
"params": {
"tab": "PLASTIC"
}
},
{
"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": "Extrude"
},
{
"text": "30"
},
{
"key": "Enter"
},
{
"expect": {
"bodies": 1,
"volume": 72000
}
},
{
"start": "FusionShellBodyCommand"
},
{
"click": {
"world": [
30,
20,
30
]
}
},
{
"text": "2"
},
{
"key": "Enter"
},
{
"expect": {
"bodies": 1,
"errors": 0,
"dialog": null,
"volume": [
15552,
0.0001
]
}
},
{
"shot": "s13_shell"
},
{
"call": "ui.view",
"params": {
"view": "top"
}
},
{
"start": "FusionBossCommand"
},
{
"click": {
"world": [
30,
28,
2
]
}
},
{
"key": "Enter"
},
{
"expect": {
"errors": 0,
"dialog": null,
"volume": [
15764.04,
0.0001
]
}
},
{
"shot": "s13_boss"
},
{
"start": "FusionLipCommand"
},
{
"click": {
"world": [
1,
20,
30
]
}
},
{
"key": "Enter"
},
{
"expect": {
"errors": 0,
"dialog": null,
"volume": [
16334.04,
0.0001
]
}
},
{
"shot": "s13_lip"
},
{
"call": "ui.view",
"params": {
"view": "iso"
}
},
{
"start": "FusionSnapFitCommand"
},
{
"click": {
"world": [
58,
20,
15
]
}
},
{
"key": "Enter"
},
{
"expect": {
"errors": 0,
"dialog": null,
"bodies": 1
}
},
{
"shot": "s13_snapfit"
}
]
@@ -0,0 +1,111 @@
[
{
"start": "SketchCreate"
},
{
"click": {
"plane": "XY"
}
},
{
"start": "ShapeRectangleTwoPoint"
},
{
"click": {
"sketch": [
0,
0
]
}
},
{
"click": {
"sketch": [
30,
20
]
}
},
{
"key": "Escape"
},
{
"start": "SketchStop"
},
{
"call": "ui.view",
"params": {
"view": "iso"
}
},
{
"start": "Extrude"
},
{
"text": "10"
},
{
"key": "Enter"
},
{
"click": {
"world": [
15,
10,
10
]
}
},
{
"key": "A"
},
{
"expect": {
"dialog": "Appearance"
}
},
{
"shot": "s14_appearance"
},
{
"widget": "OK"
},
{
"expect": {
"dialog": null,
"errors": 0
}
},
{
"click": [
127,
16
]
},
{
"widget": "Export STEP\u2026"
},
{
"expect": {
"file": "Untitled.step",
"file_contains": {
"file": "Untitled.step",
"text": "COLOUR_RGB('',0.7686"
}
}
},
{
"click": [
127,
16
]
},
{
"widget": "Export 3MF\u2026"
},
{
"expect": {
"file": "Untitled.3mf"
}
}
]
@@ -0,0 +1,24 @@
[
{"click": [248, 17]},
{"expect": {"home": true}},
{"call": "ui.home", "params": {"sample": 0}},
{"until": {"home": false}},
{"expect": {"errors": 0, "bodies": 1, "name": "Sample Plate", "param": {"width": 80}}},
{"shot": "s15_sample"},
{"start": "ChangeParameterCommand"},
{"widget": "8 mm"},
{"key": "A", "cmd": true},
{"text": "10 mm"},
{"key": "Enter"},
{"widget": "Close window"},
{"expect": {"param": {"thickness": 10}, "errors": 0}},
{"key": "S", "cmd": true},
{"expect": {"file": "Sample Plate.solvecraft"}},
{"key": "N", "cmd": true},
{"expect": {"bodies": 0}},
{"choose_file": "Sample Plate.solvecraft"},
{"key": "O", "cmd": true},
{"debug": 1},
{"expect": {"bodies": 1, "errors": 0, "param": {"thickness": 10}}},
{"shot": "s15_reopened"}
]
@@ -0,0 +1,39 @@
[
{"start": "SketchCreate"},
{"click": {"plane": "XY"}},
{"start": "ShapeRectangleTwoPoint"},
{"click": {"sketch": [0, 0]}},
{"click": {"sketch": [30, 20]}},
{"key": "Escape"},
{"start": "SketchStop"},
{"start": "Extrude"},
{"text": "10"},
{"key": "Enter"},
{"expect": {"bodies": 1, "volume": 6000, "documents": 1}},
{"key": "N", "cmd": true},
{"expect": {"bodies": 0, "documents": 2, "features": 0}},
{"start": "SketchCreate"},
{"click": {"plane": "XY"}},
{"start": "CircleCenterRadius"},
{"click": {"sketch": [0, 0]}},
{"click": {"sketch": [10, 0]}},
{"key": "Escape"},
{"start": "SketchStop"},
{"start": "Extrude"},
{"text": "5"},
{"key": "Enter"},
{"expect": {"bodies": 1, "volume": [1570.8, 1e-3]}},
{"key": "Z", "cmd": true},
{"expect": {"bodies": 0}},
{"call": "ui.documents", "params": {"action": "switch", "index": 0}},
{"expect": {"bodies": 1, "volume": 6000}},
{"key": "Z", "cmd": true},
{"expect": {"bodies": 0}},
{"key": "Y", "cmd": true},
{"expect": {"bodies": 1, "volume": 6000}},
{"call": "ui.documents", "params": {"action": "switch", "index": 1}},
{"expect": {"bodies": 0}},
{"key": "Y", "cmd": true},
{"expect": {"bodies": 1, "volume": [1570.8, 1e-3]}},
{"shot": "s16_second_tab"}
]
@@ -0,0 +1,114 @@
[
{
"key": "S"
},
{
"text": "Create Sketch"
},
{
"key": "Enter"
},
{
"click": {
"plane": "XY"
}
},
{
"expect": {
"sketching": true
}
},
{
"start": "ShapeRectangleTwoPoint"
},
{
"click": {
"sketch": [
0,
0
]
}
},
{
"click": {
"sketch": [
30,
20
]
}
},
{
"key": "Escape"
},
{
"key": "S"
},
{
"text": "Finish Sketch"
},
{
"key": "Enter"
},
{
"expect": {
"sketching": false
}
},
{
"key": "S"
},
{
"text": "Extrude"
},
{
"key": "Enter"
},
{
"text": "10"
},
{
"key": "Enter"
},
{
"expect": {
"bodies": 1,
"volume": 6000
}
},
{
"call": "ui.view",
"params": {
"view": "iso"
}
},
{
"click": {
"world": [
15,
10,
10
]
}
},
{
"key": "V"
},
{
"expect": {
"hidden": [
"Body1"
]
}
},
{
"shot": "s17_hidden"
},
{
"key": "V"
},
{
"expect": {
"hidden": []
}
}
]
@@ -0,0 +1,168 @@
[
{
"start": "SketchCreate"
},
{
"click": {
"plane": "XY"
}
},
{
"start": "ShapeRectangleTwoPoint"
},
{
"click": {
"sketch": [
0,
0
]
}
},
{
"click": {
"sketch": [
30,
20
]
}
},
{
"key": "Escape"
},
{
"start": "SketchStop"
},
{
"call": "ui.view",
"params": {
"view": "iso"
}
},
{
"start": "Extrude"
},
{
"text": "15"
},
{
"key": "Enter"
},
{
"start": "SketchCreate"
},
{
"click": {
"world": [
15,
0,
7
]
}
},
{
"expect": {
"sketching": true
}
},
{
"widget": "3D Sketch"
},
{
"call": "ui.view",
"params": {
"view": "iso"
}
},
{
"start": "DrawPolyline"
},
{
"click": {
"world": [
30,
0,
15
]
}
},
{
"click": {
"world": [
30,
20,
15
]
}
},
{
"click": {
"world": [
0,
20,
15
]
}
},
{
"key": "Escape"
},
{
"key": "Escape"
},
{
"shot": "s18_3d_lines"
},
{
"expect": {
"query": {
"command": "sketch.inspect",
"path": [
"wires",
1,
"id"
],
"value": "w2"
}
}
},
{
"start": "SketchStop"
},
{
"start": "PrimitivePipe"
},
{
"click": {
"world": [
30,
10,
15
]
}
},
{
"click": {
"world": [
15,
20,
15
]
}
},
{
"text": "3"
},
{
"key": "Enter"
},
{
"expect": {
"bodies": 2,
"errors": 0,
"dialog": null
}
},
{
"shot": "s18_pipe"
}
]
@@ -0,0 +1,72 @@
[
{
"make_image": "pic.png"
},
{
"choose_file": "pic.png"
},
{
"start": "FusionAddCanvasCommand"
},
{
"click": {
"plane": "XY"
}
},
{
"key": "Escape"
},
{
"expect": {
"canvas_width": [
100,
0.5
]
}
},
{
"call": "ui.menu",
"params": {
"x": 100,
"y": 260,
"target": {
"type": "canvas",
"id": 1
}
}
},
{
"call": "ui.menuPick",
"params": {
"item": "Calibrate"
}
},
{
"widget": "100.0 mm",
"near": "Distance"
},
{
"key": "A",
"cmd": true
},
{
"text": "50"
},
{
"key": "Enter"
},
{
"widget": "Calibrate"
},
{
"expect": {
"canvas_width": [
50,
0.01
]
}
},
{
"shot": "s19_canvas"
}
]
@@ -0,0 +1,249 @@
[
{
"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"
},
{
"widget": "OK"
},
{
"expect": {
"dialog": null,
"errors": 0,
"query": {
"command": "joint.list",
"path": [
"joints",
0,
"type"
],
"value": "revolute"
}
}
},
{
"start": "FusionHalfSectionViewCommand"
},
{
"click": {
"plane": "XZ"
}
},
{
"debug": 1
},
{
"key": "Enter"
},
{
"expect": {
"section": true,
"dialog": null
}
},
{
"shot": "s20_section"
},
{
"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
}
}
},
{
"expect": {
"section": true
}
},
{
"shot": "s20_section_driven"
}
]