commit
4af2d06007
8 files changed
+135
-4
No files matched your search
+1
-1
@@ -12,7 +12,7 @@ needed most.
|
||||
|
||||
## Get it running (about 10 minutes on a fast machine)
|
||||
|
||||
You need stable Rust 1.90 or newer. For the web
|
||||
You need stable Rust 1.95 or newer (required by egui/eframe 0.36). For the web
|
||||
build you also need `rustup target add wasm32-unknown-unknown` and [`trunk`](https://trunkrs.dev).
|
||||
|
||||
```sh
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ exclude = ["vendor"]
|
||||
version = "0.2.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.90"
|
||||
rust-version = "1.95"
|
||||
authors = ["SolidCraft contributors"]
|
||||
repository = "https://github.com/bherbruck/solidcraft"
|
||||
|
||||
|
||||
@@ -53,8 +53,17 @@ fn main() -> ExitCode {
|
||||
Some("snapshot") => cmd_snapshot(&args[1..]),
|
||||
Some("exec") => cmd_exec(&args[1..]),
|
||||
Some("commands") => {
|
||||
use std::io::Write as _;
|
||||
let mut s = Session::default();
|
||||
s.execute("engine.commands", &json!({})).map(|v| println!("{}", pretty(&v))).map_err(|e| e.to_string())
|
||||
s.execute("engine.commands", &json!({})).map_err(|e| e.to_string()).and_then(|v| {
|
||||
// A pipeline consumer may stop early (e.g. `commands | head`).
|
||||
// Unlike println!, writing explicitly lets us handle that without a panic.
|
||||
match writeln!(std::io::stdout().lock(), "{}", pretty(&v)) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
|
||||
Err(e) => Err(format!("stdout: {e}")),
|
||||
}
|
||||
})
|
||||
}
|
||||
Some("recipe") => args.get(1).ok_or_else(|| USAGE.to_string()).and_then(|p| {
|
||||
let v = read_json(p)?;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//! The registry remains valid JSON and a closed pipeline reader must not cause a panic.
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[test]
|
||||
fn registry_output_is_complete_json() {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).arg("commands").output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
let registry: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
let commands = registry.as_array().unwrap();
|
||||
assert!(commands.iter().any(|command| command["id"] == "solid.box"));
|
||||
assert!(commands.iter().any(|command| command["id"] == "engine.commands"));
|
||||
assert!(output.stdout.ends_with(b"\n"));
|
||||
assert!(output.stderr.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_registry_pipe_exits_without_panicking() {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).arg("commands").stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
||||
// Close our read end before waiting. No sleeps or timing threshold are needed:
|
||||
// the registry is larger than the pipe buffer, so it cannot all be written first.
|
||||
drop(child.stdout.take());
|
||||
let output = child.wait_with_output().unwrap();
|
||||
assert!(output.status.success(), "status: {}; stderr: {}", output.status, String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.stderr.is_empty(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
@@ -327,6 +327,8 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Result<Vec<String>, String> {
|
||||
pub(crate) fn constraint_preselection(command: &str, selection: &[solvecraft_engine::Sel]) -> Option<Value> {
|
||||
use solvecraft_engine::Sel::{SketchCurve, SketchPoint};
|
||||
match (command, selection) {
|
||||
("sketch.constraint.horizontal_vertical", [SketchCurve { id }]) => Some(json!({"line":id})),
|
||||
("sketch.constraint.horizontal_vertical", [SketchPoint { id: a }, SketchPoint { id: b }]) if a != b => Some(json!({"points":[a,b]})),
|
||||
("sketch.constraint.coincident", [a, b]) => {
|
||||
let entity = |s: &solvecraft_engine::Sel| match s {
|
||||
SketchPoint { id } | SketchCurve { id } => Some(id.clone()),
|
||||
|
||||
@@ -4,6 +4,97 @@ use serde_json::json;
|
||||
use solvecraft_engine::sketch::ConstraintKind;
|
||||
use solvecraft_ui_egui::scenario::Harness;
|
||||
|
||||
#[test]
|
||||
fn horizontal_vertical_uses_preselected_line_and_preserves_rejection() {
|
||||
for fixed in [false, true] {
|
||||
let mut h = Harness::new();
|
||||
h.run(&json!([
|
||||
{"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}},
|
||||
{"call":"engine.execute","params":{"command":"sketch.line","params":{"points":[[10,10],[40,13]],"infer":false}}},
|
||||
{"call":"ui.view","params":{"view":"top"}}
|
||||
]))
|
||||
.unwrap();
|
||||
if fixed {
|
||||
h.app.session.execute("sketch.constraint.fix", &json!({"entity":"l1"})).unwrap();
|
||||
}
|
||||
h.run(&json!([{"click":{"sketch":[25,11.5]}}])).unwrap();
|
||||
assert_eq!(h.app.session.selection.len(), 1);
|
||||
let selection = h.app.session.selection.clone();
|
||||
let before = serde_json::to_value(&h.app.session.doc).unwrap();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.run(&json!([{"start":"sketch.constraint.horizontal_vertical"}])).unwrap();
|
||||
assert_eq!(h.app.status.as_ref().is_some_and(|(_, _, error)| *error), fixed);
|
||||
if fixed {
|
||||
assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), before);
|
||||
assert_eq!(h.app.session.selection, selection);
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
} else {
|
||||
let sk = &h.app.session.model.state().sketches[0].sketch;
|
||||
let a = sk.point(sk.resolve_point("l1.start").unwrap()).unwrap();
|
||||
let b = sk.point(sk.resolve_point("l1.end").unwrap()).unwrap();
|
||||
assert!((a.y - b.y).abs() < 1e-6);
|
||||
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||
assert!(h.app.session.selection.is_empty());
|
||||
}
|
||||
assert!(h.app.tool.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_vertical_leaves_ambiguous_preselection_to_pick_tool() {
|
||||
use solvecraft_engine::Sel;
|
||||
for selection in [
|
||||
vec![Sel::SketchPoint { id: "origin".into() }],
|
||||
vec![Sel::SketchPoint { id: "origin".into() }, Sel::SketchCurve { id: "c1".into() }],
|
||||
vec![Sel::SketchPoint { id: "origin".into() }, Sel::SketchPoint { id: "c1.center".into() }, Sel::SketchCurve { id: "c1".into() }],
|
||||
] {
|
||||
let mut h = Harness::new();
|
||||
h.app.session.execute("sketch.create", &json!({"plane":"XY"})).unwrap();
|
||||
h.app.session.execute("sketch.circle.center", &json!({"center":[20,3],"radius":5})).unwrap();
|
||||
h.app.session.selection = selection.clone();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.run(&json!([{"start":"sketch.constraint.horizontal_vertical"}])).unwrap();
|
||||
assert!(h.app.tool.as_ref().unwrap().picks.is_empty());
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
assert_eq!(h.app.session.selection, selection);
|
||||
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_vertical_uses_preselected_points() {
|
||||
for center in [[20.0, 3.0], [3.0, 20.0]] {
|
||||
for reverse in [false, true] {
|
||||
let mut h = Harness::new();
|
||||
let (first, second) = if reverse { (center, [0.0, 0.0]) } else { ([0.0, 0.0], center) };
|
||||
h.run(&json!([
|
||||
{"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}},
|
||||
{"call":"engine.execute","params":{"command":"sketch.circle.center","params":{"center":center,"radius":5}}},
|
||||
{"call":"ui.view","params":{"view":"top"}},
|
||||
{"click":{"sketch":first}},
|
||||
{"click":{"sketch":second},"shift":true}
|
||||
]))
|
||||
.unwrap();
|
||||
assert_eq!(h.app.session.selection.len(), 2);
|
||||
let before = serde_json::to_value(&h.app.session.doc).unwrap();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.run(&json!([{"start":"sketch.constraint.horizontal_vertical"}])).unwrap();
|
||||
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error));
|
||||
let sk = &h.app.session.model.state().sketches[0].sketch;
|
||||
let p = sk.point(sk.resolve_point("c1.center").unwrap()).unwrap();
|
||||
assert!((if center[0] > center[1] { p.y } else { p.x }).abs() < 1e-6);
|
||||
assert!(h.app.tool.is_none());
|
||||
assert!(h.app.session.selection.is_empty());
|
||||
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||
let after = serde_json::to_value(&h.app.session.doc).unwrap();
|
||||
h.app.session.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), before);
|
||||
h.app.session.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_vertical_aligns_origin_and_circle_center() {
|
||||
for (center, horizontal) in [([20.0, 3.0], true), ([3.0, 20.0], false)] {
|
||||
|
||||
@@ -8,7 +8,7 @@ were released under the SolveCraft name.
|
||||
|
||||
Download the latest build from the
|
||||
[releases page](https://github.com/bherbruck/solidcraft/releases), or build it yourself with
|
||||
stable Rust (1.90 or newer):
|
||||
stable Rust (1.95 or newer):
|
||||
|
||||
```sh
|
||||
git clone https://github.com/bherbruck/solidcraft && cd solidcraft
|
||||
|
||||
@@ -96,6 +96,9 @@ distinct straight lines. Two lines join their closest pair of endpoints; the fir
|
||||
picked line stays in place when the existing constraints allow it. Pick endpoints
|
||||
explicitly when you want to connect a different pair.
|
||||
|
||||
Horizontal/Vertical also applies immediately to one selected line or two distinct selected
|
||||
points. Shift-click to add the second point before choosing the constraint.
|
||||
|
||||
For Parallel, Perpendicular, Equal, Tangent, Concentric and Collinear, select exactly two
|
||||
sketch curves (Shift-click to add the second) and choose the constraint to apply it immediately.
|
||||
You can also start the constraint first and then pick its curves. An incomplete or mixed
|
||||
|
||||
Reference in new issue
Block a user