New designs save as .sc; .solidcraft files still open (#7325)
* claim: new designs save as .sc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * New designs save as .sc; .solidcraft files still open and keep their name on Save DESIGN_EXT is now "sc"; DESIGN_EXTS lists sc, solidcraft and solvecraft and every open path (dialogs, drag-drop, recent files, CLI, MCP, links, the web picker) accepts all three. Save of an opened .solidcraft file writes that file; Save As, autosave, recovery and crash reports use .sc. Windows, macOS and Linux register .sc alongside the older extensions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Update UI tests for the .sc default extension Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
613b468ef5
commit
03b8f4138b
105 files changed
+493
-274
No files matched your search
@@ -1,3 +1,6 @@
|
||||
# Preserve canonical source receipts and the runtime icon-discovery snapshot.
|
||||
.gitattributes text eol=lf
|
||||
/assets/icons/ui/design/requested_icons.txt text eol=lf
|
||||
# SolidCraft designs (.sc): a JSON document or a ZIP container, never Scala source, so they
|
||||
# stay out of GitHub's language statistics. Git's own text/binary detection handles both kinds.
|
||||
*.sc -linguist-detectable
|
||||
@@ -19,7 +19,7 @@ body:
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
|
||||
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.sc --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
|
||||
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
|
||||
placeholder: |
|
||||
1. Sketch a circle on the XY plane
|
||||
|
||||
@@ -74,7 +74,7 @@ other is a tracked bug.
|
||||
- **Parameters and configurations** with unit-aware expressions.
|
||||
- **Assemblies** with joints, motion studies, contact sets, interference and mass properties.
|
||||
- **Sheet metal** with flanges, hems, fold/unfold and flat patterns, and plastic enclosure features.
|
||||
- **Files:** STEP, IGES, 3MF, STL, OBJ and glTF in and out where supported, URDF export, `.solidcraft` designs.
|
||||
- **Files:** STEP, IGES, 3MF, STL, OBJ and glTF in and out where supported, URDF export, `.sc` designs (`.solidcraft` files from earlier releases still open).
|
||||
- **Desktop app, browser app, headless CLI and MCP server**, all running the same commands.
|
||||
|
||||
The full list is in [docs/capabilities.md](docs/capabilities.md) and [docs/features.md](docs/features.md);
|
||||
|
||||
@@ -68,7 +68,7 @@ fn run() -> Result<(), String> {
|
||||
let generation_start = Instant::now();
|
||||
let (commands, command_timings) = session.run_script_timed(&recipe, false).map_err(|e| e.to_string())?;
|
||||
let generation_seconds = generation_start.elapsed().as_secs_f64();
|
||||
let design_path = output.with_extension("solidcraft");
|
||||
let design_path = output.with_extension(solidcraft_engine::io::DESIGN_EXT);
|
||||
session.execute("file.save_as", &json!({"path":design_path})).map_err(|e| e.to_string())?;
|
||||
let scene = solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?;
|
||||
let bodies: Vec<Value> = scene
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! no fixture.
|
||||
//!
|
||||
//! ```text
|
||||
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.solidcraft]
|
||||
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.sc]
|
||||
//! ```
|
||||
//! prints one JSON line per hundred commands (mean and max milliseconds, bodies so far), then the
|
||||
//! build total, then the phases `bench-open` prints for the saved design.
|
||||
@@ -205,7 +205,10 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
);
|
||||
let path = match flag("--save") {
|
||||
Some(p) => p.clone(),
|
||||
None => std::env::temp_dir().join(format!("bench-agent-{}.solidcraft", std::process::id())).to_string_lossy().into_owned(),
|
||||
None => std::env::temp_dir()
|
||||
.join(format!("bench-agent-{}.{}", std::process::id(), solidcraft_engine::io::DESIGN_EXT))
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
};
|
||||
s.execute("file.save", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
let r = crate::cmd_bench_open(std::slice::from_ref(&path));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! SolidCraft headless command line.
|
||||
//!
|
||||
//! ```text
|
||||
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
|
||||
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
|
||||
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
|
||||
//! solidcraft-cli run <script.json|design.sc|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
|
||||
//! solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
|
||||
//! solidcraft-cli script <design.sc> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
|
||||
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
|
||||
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
|
||||
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
|
||||
@@ -56,19 +56,19 @@ struct Command {
|
||||
}
|
||||
|
||||
/// `run`'s lines in the usage.
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
|
||||
/// `eval`'s lines in the usage.
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.solidcraft|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
|
||||
/// `script`'s lines in the usage.
|
||||
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
|
||||
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.sc|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
|
||||
/// `bench-open`'s lines in the usage.
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
/// `bench-agent`'s lines in the usage.
|
||||
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]\n";
|
||||
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.sc] [--script-out script.json]\n";
|
||||
/// `bench-assistant`'s lines in the usage.
|
||||
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
|
||||
/// `snapshot`'s lines in the usage.
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.sc|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.sc|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
|
||||
/// `render`'s lines in the usage.
|
||||
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
|
||||
/// `exec`'s lines in the usage.
|
||||
@@ -83,7 +83,7 @@ const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
|
||||
const USAGE_FUZZ: &str =
|
||||
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
|
||||
/// `mcp`'s lines in the usage.
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.sc|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
|
||||
/// `crashes`'s lines in the usage.
|
||||
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
|
||||
/// `licences`'s lines in the usage.
|
||||
@@ -817,7 +817,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
|
||||
|
||||
/// Separate file reading, JSON decoding/repair, timeline evaluation and display meshing.
|
||||
pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]")?;
|
||||
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.sc | part.step> [--parse-only]")?;
|
||||
let t = std::time::Instant::now();
|
||||
let step = solidcraft_engine::io::is_step_path(path);
|
||||
let limit = if step { solidcraft_engine::io::MAX_STEP_BYTES } else { solidcraft_engine::io::MAX_DESIGN_BYTES };
|
||||
@@ -863,13 +863,13 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "place_ms": place_ms, "bodies": st.bodies.len(), "triangles": triangles})
|
||||
);
|
||||
let flag = |name: &str| args.iter().position(|a| a == name);
|
||||
// `--save PATH`: save the opened design without its model cache (to PATH.plain.solidcraft) and
|
||||
// `--save PATH`: save the opened design without its model cache (to PATH.plain.sc) and
|
||||
// as the app does, with it, then read the cache back.
|
||||
let s = match flag("--save").and_then(|i| args.get(i + 1)) {
|
||||
Some(out) => {
|
||||
let mut s = s;
|
||||
// Without the cache first, for comparison.
|
||||
let plain = format!("{out}.plain.solidcraft");
|
||||
let plain = format!("{out}.plain.{}", solidcraft_engine::io::DESIGN_EXT);
|
||||
let t = std::time::Instant::now();
|
||||
s.execute("file.save_as", &json!({"path": plain, "cache": false})).map_err(|e| e.to_string())?;
|
||||
let plain_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
@@ -92,7 +92,7 @@ fn designs() -> Vec<(String, PathBuf)> {
|
||||
add("regressions", root.join("examples/regressions"), "json");
|
||||
add("demo", root.join("examples/demo"), "json");
|
||||
add("typical", root.join("tests/action_matrix/designs"), "json");
|
||||
add("demo", root.join("tests/action_matrix/designs"), "solidcraft");
|
||||
add("demo", root.join("tests/action_matrix/designs"), solidcraft_engine::io::DESIGN_EXT);
|
||||
let mut cases: Vec<PathBuf> =
|
||||
std::fs::read_dir(root.join("tests/oracle")).unwrap().flatten().map(|e| e.path()).filter(|p| p.join("recipe.json").is_file()).collect();
|
||||
cases.sort();
|
||||
@@ -105,7 +105,7 @@ fn designs() -> Vec<(String, PathBuf)> {
|
||||
/// Build a design from its file.
|
||||
fn load(path: &Path) -> Result<Session, String> {
|
||||
let mut s = Session::default();
|
||||
if path.extension().is_some_and(|x| x == "solidcraft") {
|
||||
if solidcraft_engine::io::is_design_container_path(path) {
|
||||
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
}
|
||||
@@ -1558,7 +1558,7 @@ fn sketches_on_loft_and_sweep_faces() {
|
||||
#[test]
|
||||
fn press_pull_on_the_pi_case_floor() {
|
||||
reproduces(
|
||||
Some("tests/action_matrix/designs/pi-case.solidcraft"),
|
||||
Some("tests/action_matrix/designs/pi-case.sc"),
|
||||
json!([{"command": "solid.press_pull", "params": {"face": [30, 20, 0], "distance": 1}}]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,12 +75,15 @@ fn designs() -> Vec<(String, Session)> {
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "solidcraft" || x == "json") && !p.to_string_lossy().ends_with(".expected.json"))
|
||||
.filter(|p| {
|
||||
(solidcraft_engine::io::is_design_container_path(p) || p.extension().is_some_and(|x| x == "json"))
|
||||
&& !p.to_string_lossy().ends_with(".expected.json")
|
||||
})
|
||||
.collect();
|
||||
files.sort();
|
||||
for p in files {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let s = if p.extension().is_some_and(|x| x == "solidcraft") {
|
||||
let s = if solidcraft_engine::io::is_design_container_path(&p) {
|
||||
open(&p)
|
||||
} else {
|
||||
let mut s = Session::default();
|
||||
@@ -150,7 +153,7 @@ fn pi_case_faces_per_step_at_both_heights() {
|
||||
const FACES: [(usize, usize); 13] =
|
||||
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
|
||||
let dir = scratch_dir("pi");
|
||||
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
|
||||
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.sc")).unwrap()).unwrap();
|
||||
let at = |height: &str, file: &str| {
|
||||
let mut d = design.clone();
|
||||
for p in d["params"].as_array_mut().unwrap() {
|
||||
|
||||
@@ -145,7 +145,7 @@ fn eval_and_inspect_reject_invalid_options_before_script_execution_or_reporting(
|
||||
#[test]
|
||||
fn cli_loads_both_design_encodings_by_contents() {
|
||||
let dir = Scratch::new();
|
||||
for extension in ["solidcraft", "solvecraft", "json"] {
|
||||
for extension in ["sc", "solidcraft", "solvecraft", "json"] {
|
||||
let saved = dir.0.join(format!("box.{extension}"));
|
||||
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&saved).output().unwrap());
|
||||
let bytes = std::fs::read(&saved).unwrap();
|
||||
@@ -412,6 +412,11 @@ fn legacy_extension_and_format_tag_still_open() {
|
||||
assert_eq!(body_count(&resaved), 2);
|
||||
let reopened = solidcraft_engine::io::read_design(&std::fs::read(&resaved).unwrap()).unwrap();
|
||||
assert!(reopened.to_json().contains("\"solidcraft/"));
|
||||
// `.solidcraft` (before `.sc`) opens as `--in` and as the design to evaluate; `.sc` too.
|
||||
let current = dir.0.join("current.sc");
|
||||
json_output(cli().args(["run", "--in"]).arg(&resaved).arg(dir.script()).arg("--save").arg(¤t).output().unwrap());
|
||||
assert_eq!(body_count(¤t), 3);
|
||||
assert!(solidcraft_engine::io::is_design_container(&std::fs::read(¤t).unwrap()), "a .sc file is a design container");
|
||||
}
|
||||
|
||||
/// `snapshot` starts from `--in` as `run` does (its script runs on that design), and refuses an
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Straight edges stay one edge after many cuts (#1049). The owner's design
|
||||
//! (`tests/action_matrix/designs/crazy-geom-cuts.solidcraft`, saved by 0.10.0): five extrudes,
|
||||
//! (`tests/action_matrix/designs/crazy-geom-cuts.sc`, saved by 0.10.0): five extrudes,
|
||||
//! joins and cuts, over four sketches, three of them on faces of the body with the faces'
|
||||
//! edges projected. Cuts used to leave the straight edges of the walls they sliced split at
|
||||
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
|
||||
@@ -14,7 +14,7 @@ use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
fn design() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.solidcraft")
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.sc")
|
||||
}
|
||||
|
||||
fn open(path: &Path) -> Session {
|
||||
|
||||
@@ -169,10 +169,15 @@ pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
|
||||
(!extensions.is_empty() && extensions.iter().all(image)).then_some(solidcraft_engine::doc::canvas::MAX_CANVAS_BYTES)
|
||||
}
|
||||
|
||||
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension).
|
||||
pub fn save_type(name: &str) -> (&'static str, &'static str, &'static str) {
|
||||
let json = std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("json"));
|
||||
if json { ("Plain JSON design", "application/json", ".json") } else { ("SolidCraft design", "application/octet-stream", ".solidcraft") }
|
||||
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension). A
|
||||
/// design is `.sc` unless the name already has an older design extension, which it keeps.
|
||||
pub fn save_type(name: &str) -> (&'static str, &'static str, String) {
|
||||
let ext = std::path::Path::new(name).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
|
||||
match ext.as_deref() {
|
||||
Some("json") => ("Plain JSON design", "application/json", ".json".into()),
|
||||
Some(e) if solidcraft_engine::io::is_design_ext(e) => ("SolidCraft design", "application/octet-stream", format!(".{e}")),
|
||||
_ => ("SolidCraft design", "application/octet-stream", format!(".{}", solidcraft_engine::io::DESIGN_EXT)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this a cancelled dialog (the browser's `AbortError`)? Not an error to show.
|
||||
@@ -264,7 +269,7 @@ mod web {
|
||||
let options = Object::new();
|
||||
let _ = Reflect::set(&options, &"suggestedName".into(), &name.into());
|
||||
let (description, mime, extension) = save_type(name);
|
||||
let _ = Reflect::set(&options, &"types".into(), &types(description, mime, extension));
|
||||
let _ = Reflect::set(&options, &"types".into(), &types(description, mime, &extension));
|
||||
options
|
||||
}
|
||||
|
||||
@@ -349,7 +354,9 @@ mod web {
|
||||
let options = Object::new();
|
||||
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
|
||||
let extensions = Array::new();
|
||||
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"] {
|
||||
let designs = solidcraft_engine::io::DESIGN_EXTS.iter().map(|e| format!(".{e}"));
|
||||
let others = [".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"].map(String::from);
|
||||
for e in designs.chain(others) {
|
||||
extensions.push(&e.into());
|
||||
}
|
||||
let accept = Object::new();
|
||||
@@ -667,6 +674,7 @@ mod tests {
|
||||
#[test]
|
||||
fn only_design_opens_use_the_disk_dialog() {
|
||||
assert!(opens_design(&[]));
|
||||
assert!(opens_design(&["sc"]));
|
||||
assert!(opens_design(&["solidcraft"]));
|
||||
assert!(opens_design(&["solvecraft"]));
|
||||
assert!(!opens_design(&["step", "stp"]));
|
||||
@@ -710,9 +718,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_save_dialog_offers_the_type_of_the_name() {
|
||||
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
|
||||
assert_eq!(save_type("Plate.sc").2, ".sc");
|
||||
assert_eq!(save_type("Plate.JSON").2, ".json");
|
||||
assert_eq!(save_type("Plate").2, ".solidcraft");
|
||||
assert_eq!(save_type("Plate").2, ".sc");
|
||||
// A design opened under an older extension keeps it.
|
||||
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,11 +24,13 @@ pub const OPFS: &str = disk::OPFS_PREFIX;
|
||||
pub const DOWNLOADS: &str = disk::DOWNLOADS_PREFIX;
|
||||
/// Files brought in from the computer.
|
||||
pub const UPLOAD: &str = "/upload/";
|
||||
/// The autosave of unsaved work (in OPFS: `recovery.solidcraft` and `recovery.json`).
|
||||
const RECOVERY: &str = "recovery.solidcraft";
|
||||
/// The autosave earlier releases kept (compatibility shim, docs/compatibility.md): recovered
|
||||
/// when there is no current one.
|
||||
const LEGACY_RECOVERY: &str = "recovery.solvecraft";
|
||||
/// The autosave of unsaved work (in OPFS: `recovery.sc` and `recovery.json`), named with
|
||||
/// `solidcraft_engine::io::DESIGN_EXT`.
|
||||
const RECOVERY: &str = "recovery.sc";
|
||||
/// The autosaves earlier releases kept, newest first, one per entry of
|
||||
/// `solidcraft_engine::io::LEGACY_DESIGN_EXTS` (compatibility shim, docs/compatibility.md): the
|
||||
/// newest is recovered when there is no current one.
|
||||
const LEGACY_RECOVERY: [&str; 2] = ["recovery.solidcraft", "recovery.solvecraft"];
|
||||
const RECOVERY_META: &str = "recovery.json";
|
||||
|
||||
/// What to do with a file brought in from the computer.
|
||||
@@ -282,9 +284,10 @@ pub fn start(files: &Rc<Files>) {
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
let has_recovery = all.iter().any(|(n, _, _)| n == RECOVERY);
|
||||
let legacy = (!has_recovery).then(|| LEGACY_RECOVERY.into_iter().find(|l| all.iter().any(|(n, _, _)| n == l))).flatten();
|
||||
for (name, bytes, modified) in all {
|
||||
if name == LEGACY_RECOVERY {
|
||||
if !has_recovery {
|
||||
if LEGACY_RECOVERY.contains(&name.as_str()) {
|
||||
if legacy == Some(name.as_str()) {
|
||||
vfs::insert(&format!("{OPFS}{RECOVERY}"), bytes);
|
||||
}
|
||||
continue;
|
||||
@@ -424,7 +427,9 @@ pub fn autosave(files: &Rc<Files>, doc: Option<(&solidcraft_engine::doc::Documen
|
||||
}
|
||||
None => spawn_local(async move {
|
||||
let _ = remove_file(RECOVERY).await;
|
||||
let _ = remove_file(LEGACY_RECOVERY).await;
|
||||
for legacy in LEGACY_RECOVERY {
|
||||
let _ = remove_file(legacy).await;
|
||||
}
|
||||
let _ = remove_file(RECOVERY_META).await;
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -72,10 +72,10 @@ impl Download {
|
||||
}
|
||||
let Some((name, bytes)) = self.pending.take() else { return };
|
||||
// The extension selects the design loader even when the URL has no extension.
|
||||
let path = "/linked/design.solidcraft";
|
||||
let path = &format!("/linked/design.{}", solidcraft_engine::io::DESIGN_EXT);
|
||||
vfs::insert(path, bytes);
|
||||
app.open_path(path);
|
||||
if app.session.path.as_deref() == Some(path) {
|
||||
if app.session.path.as_deref() == Some(path.as_str()) {
|
||||
app.session.path = None;
|
||||
app.session.doc_mut().name = name;
|
||||
app.session.mark_unsaved();
|
||||
@@ -90,9 +90,14 @@ async fn fetch(value: &str) -> Reply {
|
||||
let window = web_sys::window().ok_or("No browser window")?;
|
||||
let page_origin = window.location().origin().map_err(|_| "Cannot read page origin")?;
|
||||
let url = design_url(value, &page_origin)?;
|
||||
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
|
||||
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design");
|
||||
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
|
||||
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
|
||||
// `arm.sc` (or a legacy `arm.solidcraft`) is named `arm`.
|
||||
let stem = match decoded.rsplit_once('.') {
|
||||
Some((stem, ext)) if !stem.is_empty() && solidcraft_engine::io::is_design_ext(ext) => stem,
|
||||
_ => decoded.as_str(),
|
||||
};
|
||||
let name: String = stem.chars().take(255).collect();
|
||||
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
|
||||
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
|
||||
design_url(&response.url(), &page_origin).map_err(|_| "Design redirected to a URL that does not use HTTPS or the page's own origin")?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! SolidCraft desktop app.
|
||||
//!
|
||||
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]`
|
||||
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.sc | part.step]`
|
||||
//!
|
||||
//! `--safe-mode` (or `SOLIDCRAFT_SAFE_MODE=1`) starts with default settings, no recovery offer and
|
||||
//! the software renderer; see `startup` and docs/book/src/guide/troubleshooting.md.
|
||||
@@ -778,7 +778,7 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"usage: solidcraft [--control PORT] [--sample] [--theme-file THEME.json] [--crash-log] [--safe-mode] [design.solidcraft | part.step]"
|
||||
"usage: solidcraft [--control PORT] [--sample] [--theme-file THEME.json] [--crash-log] [--safe-mode] [design.sc | part.step]"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -903,7 +903,7 @@
|
||||
"home.new_mm": "Nový návrh",
|
||||
"home.new_mm_hint": "Milimetry · otevře se v pracovním prostoru Návrh",
|
||||
"home.open": "Otevřít…",
|
||||
"home.open_hint": "Návrh ve formátu .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Návrh ve formátu .sc ({shortcut})",
|
||||
"home.open_now": "Otevřít nyní",
|
||||
"home.opened_days_ago": "Před {count} dny",
|
||||
"home.opened_hours_ago": "Před {count} h",
|
||||
|
||||
@@ -1106,7 +1106,7 @@
|
||||
"home.new_mm": "Nyt design",
|
||||
"home.new_mm_hint": "Millimeter · åbner i Design",
|
||||
"home.open": "Åbn…",
|
||||
"home.open_hint": "Et .solidcraft-design ({shortcut})",
|
||||
"home.open_hint": "Et .sc-design ({shortcut})",
|
||||
"home.open_now": "Åbn nu",
|
||||
"home.opened_days_ago": "For {count} dage siden",
|
||||
"home.opened_hours_ago": "For {count} t siden",
|
||||
|
||||
@@ -1229,7 +1229,7 @@
|
||||
"home.new_mm": "Neuer Entwurf",
|
||||
"home.new_mm_hint": "Millimeter · öffnet sich im Arbeitsbereich Entwurf",
|
||||
"home.open": "Öffnen…",
|
||||
"home.open_hint": "Ein .solidcraft-Entwurf ({shortcut})",
|
||||
"home.open_hint": "Ein .sc-Entwurf ({shortcut})",
|
||||
"home.open_now": "Jetzt öffnen",
|
||||
"home.opened_days_ago": "Vor {count} Tagen",
|
||||
"home.opened_hours_ago": "Vor {count} Std.",
|
||||
|
||||
@@ -1291,7 +1291,7 @@
|
||||
"home.new_mm": "New design",
|
||||
"home.new_mm_hint": "Millimetres · opens in Design",
|
||||
"home.open": "Open…",
|
||||
"home.open_hint": "A .solidcraft design ({shortcut})",
|
||||
"home.open_hint": "A .sc design ({shortcut})",
|
||||
"home.open_now": "Open now",
|
||||
"home.opened_days_ago": "{count} days ago",
|
||||
"home.opened_hours_ago": "{count} h ago",
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
"home.new_mm": "Nuevo diseño",
|
||||
"home.new_mm_hint": "Milímetros · se abre en Diseño",
|
||||
"home.open": "Abrir…",
|
||||
"home.open_hint": "Un diseño .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Un diseño .sc ({shortcut})",
|
||||
"home.open_now": "Abrir ahora",
|
||||
"home.opened_days_ago": "Hace {count} días",
|
||||
"home.opened_hours_ago": "Hace {count} h",
|
||||
|
||||
@@ -1227,7 +1227,7 @@
|
||||
"home.new_mm": "Nouvelle conception",
|
||||
"home.new_mm_hint": "Millimètres · ouvre l’espace Conception",
|
||||
"home.open": "Ouvrir…",
|
||||
"home.open_hint": "Une conception .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Une conception .sc ({shortcut})",
|
||||
"home.open_now": "Ouvrir maintenant",
|
||||
"home.opened_days_ago": "Il y a {count} j",
|
||||
"home.opened_hours_ago": "Il y a {count} h",
|
||||
|
||||
@@ -900,7 +900,7 @@
|
||||
"home.new_mm": "Novo deseño",
|
||||
"home.new_mm_hint": "Milímetros · ábrese en Deseño",
|
||||
"home.open": "Abrir…",
|
||||
"home.open_hint": "Un deseño .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Un deseño .sc ({shortcut})",
|
||||
"home.open_now": "Abrir agora",
|
||||
"home.opened_days_ago": "Hai {count} días",
|
||||
"home.opened_hours_ago": "Hai {count} h",
|
||||
|
||||
@@ -899,7 +899,7 @@
|
||||
"home.new_mm": "Új terv",
|
||||
"home.new_mm_hint": "Milliméter · a Tervezés munkaterületen nyílik meg",
|
||||
"home.open": "Megnyitás…",
|
||||
"home.open_hint": "Egy .solidcraft terv ({shortcut})",
|
||||
"home.open_hint": "Egy .sc terv ({shortcut})",
|
||||
"home.open_now": "Jelenleg megnyitva",
|
||||
"home.opened_days_ago": "{count} napja",
|
||||
"home.opened_hours_ago": "{count} órája",
|
||||
|
||||
@@ -899,7 +899,7 @@
|
||||
"home.new_mm": "Desain baru",
|
||||
"home.new_mm_hint": "Milimeter · terbuka di Desain",
|
||||
"home.open": "Buka…",
|
||||
"home.open_hint": "Desain .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Desain .sc ({shortcut})",
|
||||
"home.open_now": "Sedang terbuka",
|
||||
"home.opened_days_ago": "{count} hari lalu",
|
||||
"home.opened_hours_ago": "{count} jam lalu",
|
||||
|
||||
@@ -925,7 +925,7 @@
|
||||
"home.new_mm": "Nuovo progetto",
|
||||
"home.new_mm_hint": "Millimetri · si apre in Progettazione",
|
||||
"home.open": "Apri…",
|
||||
"home.open_hint": "Un progetto .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Un progetto .sc ({shortcut})",
|
||||
"home.open_now": "Apri ora",
|
||||
"home.opened_days_ago": "{count} giorni fa",
|
||||
"home.opened_hours_ago": "{count} h fa",
|
||||
|
||||
@@ -1291,7 +1291,7 @@
|
||||
"home.new_mm": "新しいデザイン",
|
||||
"home.new_mm_hint": "ミリメートル · デザインで開きます",
|
||||
"home.open": "開く…",
|
||||
"home.open_hint": ".solidcraft 形式のデザイン ({shortcut})",
|
||||
"home.open_hint": ".sc 形式のデザイン ({shortcut})",
|
||||
"home.open_now": "今すぐ開く",
|
||||
"home.opened_days_ago": "{count} 日前",
|
||||
"home.opened_hours_ago": "{count} 時間前",
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"home.new_mm": "새 설계",
|
||||
"home.new_mm_hint": "밀리미터 · 설계 작업 공간에서 열기",
|
||||
"home.open": "열기…",
|
||||
"home.open_hint": ".solidcraft 설계 ({shortcut})",
|
||||
"home.open_hint": ".sc 설계 ({shortcut})",
|
||||
"home.open_now": "지금 열기",
|
||||
"home.opened_days_ago": "{count}일 전",
|
||||
"home.opened_hours_ago": "{count}시간 전",
|
||||
|
||||
@@ -900,7 +900,7 @@
|
||||
"home.new_mm": "Reka bentuk baharu",
|
||||
"home.new_mm_hint": "Milimeter · dibuka dalam Reka Bentuk",
|
||||
"home.open": "Buka…",
|
||||
"home.open_hint": "Reka bentuk .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Reka bentuk .sc ({shortcut})",
|
||||
"home.open_now": "Buka sekarang",
|
||||
"home.opened_days_ago": "{count} hari lalu",
|
||||
"home.opened_hours_ago": "{count} jam lalu",
|
||||
|
||||
@@ -1053,7 +1053,7 @@
|
||||
"home.new_mm": "Nytt design",
|
||||
"home.new_mm_hint": "Millimeter · åpnes i Design",
|
||||
"home.open": "Åpne…",
|
||||
"home.open_hint": "Et .solidcraft-design ({shortcut})",
|
||||
"home.open_hint": "Et .sc-design ({shortcut})",
|
||||
"home.open_now": "Åpne nå",
|
||||
"home.opened_days_ago": "for {count} dager siden",
|
||||
"home.opened_hours_ago": "for {count} t siden",
|
||||
|
||||
@@ -899,7 +899,7 @@
|
||||
"home.new_mm": "Nieuw ontwerp",
|
||||
"home.new_mm_hint": "Millimeters · opent in Ontwerp",
|
||||
"home.open": "Openen…",
|
||||
"home.open_hint": "Een .solidcraft-ontwerp ({shortcut})",
|
||||
"home.open_hint": "Een .sc-ontwerp ({shortcut})",
|
||||
"home.open_now": "Momenteel geopend",
|
||||
"home.opened_days_ago": "{count} dagen geleden",
|
||||
"home.opened_hours_ago": "{count} u geleden",
|
||||
|
||||
@@ -961,7 +961,7 @@
|
||||
"home.new_mm": "Nowy projekt",
|
||||
"home.new_mm_hint": "Milimetry · otwiera w obszarze Projektowanie",
|
||||
"home.open": "Otwórz…",
|
||||
"home.open_hint": "Projekt .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Projekt .sc ({shortcut})",
|
||||
"home.open_now": "Otwórz teraz",
|
||||
"home.opened_days_ago": "{count} dni temu",
|
||||
"home.opened_hours_ago": "{count} godz. temu",
|
||||
|
||||
@@ -1074,7 +1074,7 @@
|
||||
"home.new_mm": "Novo projeto",
|
||||
"home.new_mm_hint": "Milímetros · abre em Projeto",
|
||||
"home.open": "Abrir…",
|
||||
"home.open_hint": "Um projeto .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Um projeto .sc ({shortcut})",
|
||||
"home.open_now": "Abrir agora",
|
||||
"home.opened_days_ago": "Há {count} dias",
|
||||
"home.opened_hours_ago": "Há {count} h",
|
||||
|
||||
@@ -899,7 +899,7 @@
|
||||
"home.new_mm": "Proiect nou",
|
||||
"home.new_mm_hint": "Milimetri · se deschide în Proiectare",
|
||||
"home.open": "Deschide…",
|
||||
"home.open_hint": "Un proiect .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Un proiect .sc ({shortcut})",
|
||||
"home.open_now": "Deschis acum",
|
||||
"home.opened_days_ago": "Acum {count} zile",
|
||||
"home.opened_hours_ago": "Acum {count} h",
|
||||
|
||||
@@ -899,7 +899,7 @@
|
||||
"home.new_mm": "Новый проект",
|
||||
"home.new_mm_hint": "Миллиметры · откроется в рабочей области «Проектирование»",
|
||||
"home.open": "Открыть…",
|
||||
"home.open_hint": "Проект .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Проект .sc ({shortcut})",
|
||||
"home.open_now": "Открыть сейчас",
|
||||
"home.opened_days_ago": "{count} дн. назад",
|
||||
"home.opened_hours_ago": "{count} ч назад",
|
||||
|
||||
@@ -899,7 +899,7 @@
|
||||
"home.new_mm": "Ny konstruktion",
|
||||
"home.new_mm_hint": "Millimeter · öppnas i Konstruktion",
|
||||
"home.open": "Öppna…",
|
||||
"home.open_hint": "En .solidcraft-konstruktion ({shortcut})",
|
||||
"home.open_hint": "En .sc-konstruktion ({shortcut})",
|
||||
"home.open_now": "Öppen nu",
|
||||
"home.opened_days_ago": "För {count} dagar sedan",
|
||||
"home.opened_hours_ago": "För {count} h sedan",
|
||||
|
||||
@@ -1252,7 +1252,7 @@
|
||||
"home.new_mm": "Yeni tasarım",
|
||||
"home.new_mm_hint": "Milimetre · Tasarım alanında açılır",
|
||||
"home.open": "Aç…",
|
||||
"home.open_hint": "Bir .solidcraft tasarımı ({shortcut})",
|
||||
"home.open_hint": "Bir .sc tasarımı ({shortcut})",
|
||||
"home.open_now": "Şimdi aç",
|
||||
"home.opened_days_ago": "{count} gün önce",
|
||||
"home.opened_hours_ago": "{count} sa. önce",
|
||||
|
||||
@@ -1252,7 +1252,7 @@
|
||||
"home.new_mm": "Новий проєкт",
|
||||
"home.new_mm_hint": "Міліметри · відкриється в робочому просторі «Проєктування»",
|
||||
"home.open": "Відкрити…",
|
||||
"home.open_hint": "Проєкт .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Проєкт .sc ({shortcut})",
|
||||
"home.open_now": "Відкрити зараз",
|
||||
"home.opened_days_ago": "{count} дн. тому",
|
||||
"home.opened_hours_ago": "{count} год тому",
|
||||
|
||||
@@ -1291,7 +1291,7 @@
|
||||
"home.new_mm": "Thiết kế mới",
|
||||
"home.new_mm_hint": "Milimét · mở trong Thiết kế",
|
||||
"home.open": "Mở…",
|
||||
"home.open_hint": "Một thiết kế .solidcraft ({shortcut})",
|
||||
"home.open_hint": "Một thiết kế .sc ({shortcut})",
|
||||
"home.open_now": "Mở ngay",
|
||||
"home.opened_days_ago": "{count} ngày trước",
|
||||
"home.opened_hours_ago": "{count} giờ trước",
|
||||
|
||||
@@ -900,7 +900,7 @@
|
||||
"home.new_mm": "新增設計",
|
||||
"home.new_mm_hint": "公釐 · 在設計工作區中開啟",
|
||||
"home.open": "開啟…",
|
||||
"home.open_hint": ".solidcraft 設計檔案 ({shortcut})",
|
||||
"home.open_hint": ".sc 設計檔案 ({shortcut})",
|
||||
"home.open_now": "目前開啟中",
|
||||
"home.opened_days_ago": "{count} 天前",
|
||||
"home.opened_hours_ago": "{count} 小時前",
|
||||
|
||||
@@ -1291,7 +1291,7 @@
|
||||
"home.new_mm": "新建设计",
|
||||
"home.new_mm_hint": "毫米 · 在设计工作区中打开",
|
||||
"home.open": "打开…",
|
||||
"home.open_hint": ".solidcraft 设计文件 ({shortcut})",
|
||||
"home.open_hint": ".sc 设计文件 ({shortcut})",
|
||||
"home.open_now": "立即打开",
|
||||
"home.opened_days_ago": "{count} 天前",
|
||||
"home.opened_hours_ago": "{count} 小时前",
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
<comment>SolidCraft design</comment>
|
||||
<icon name="io.tangencylabs.solidcraft"/>
|
||||
<sub-class-of type="application/json"/>
|
||||
<!-- shared-mime-info also gives *.sc to Scala scripts (text/x-scala, weight 50); with
|
||||
SolidCraft installed its designs win. -->
|
||||
<glob pattern="*.sc" weight="60"/>
|
||||
<!-- Compatibility shim (docs/compatibility.md): designs saved before .sc, and before the rename. -->
|
||||
<glob pattern="*.solidcraft"/>
|
||||
<!-- Compatibility shim (docs/compatibility.md): designs saved before the rename. -->
|
||||
<glob pattern="*.solvecraft"/>
|
||||
<alias type="application/x-solvecraft"/>
|
||||
</mime-type>
|
||||
|
||||
@@ -45,8 +45,11 @@
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>SolidCraft design</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<!-- solvecraft: designs saved before the rename (compatibility shim, docs/compatibility.md). -->
|
||||
<array><string>solidcraft</string><string>solvecraft</string></array>
|
||||
<!-- solidcraft: designs saved before .sc; solvecraft: before the rename (compatibility
|
||||
shims, docs/compatibility.md). -->
|
||||
<array><string>sc</string><string>solidcraft</string><string>solvecraft</string></array>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array><string>io.tangencylabs.solidcraft.design</string></array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
@@ -63,5 +66,24 @@
|
||||
<string>Alternate</string>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- The design type: .sc, and the older .solidcraft and .solvecraft (docs/compatibility.md). -->
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.tangencylabs.solidcraft.design</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>SolidCraft design</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array><string>public.data</string><string>public.content</string></array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array><string>sc</string><string>solidcraft</string><string>solvecraft</string></array>
|
||||
<key>public.mime-type</key>
|
||||
<array><string>application/x-solidcraft</string></array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -11,6 +11,6 @@ Some earlier releases were installed as SolveCraft.app. Move that one to the Tra
|
||||
once SolidCraft.app is in Applications, and replace any Dock shortcut with the
|
||||
old name (open SolidCraft.app, then choose Options > Keep in Dock).
|
||||
|
||||
Designs saved as .solvecraft still open, and SolidCraft copies your settings
|
||||
and recovery files from the old SolveCraft folder on its first launch.
|
||||
New designs are saved as .solidcraft.
|
||||
Designs saved as .solidcraft or .solvecraft still open, and SolidCraft copies
|
||||
your settings and recovery files from the old SolveCraft folder on its first
|
||||
launch. New designs are saved as .sc.
|
||||
@@ -9,7 +9,7 @@
|
||||
Per-machine install with a setup wizard: Welcome, install folder (Program Files\SolidCraft by
|
||||
default, remembered for upgrades), Ready, progress, and a Finish page with a ticked "Launch
|
||||
SolidCraft" box. Installs a Start Menu shortcut, solidcraft-cli (and adds its folder to the system PATH), the licence files (including
|
||||
THIRD-PARTY-LICENSES.txt), makes SolidCraft the owner of `.solidcraft` (and of the pre-rename `.solvecraft`), and offers it under
|
||||
THIRD-PARTY-LICENSES.txt), makes SolidCraft the owner of `.sc` (and of the older `.solidcraft` and pre-rename `.solvecraft`), and offers it under
|
||||
"Open with" for STEP, IGES, 3MF and STL. `msiexec /qn` installs silently.
|
||||
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
|
||||
-->
|
||||
@@ -84,15 +84,17 @@
|
||||
Icon="SolidcraftIcon.exe" IconIndex="0" />
|
||||
</File>
|
||||
|
||||
<!-- .solidcraft: SolidCraft's own format; SolidCraft is its default app. -->
|
||||
<!-- .sc: SolidCraft's own format; SolidCraft is its default app. -->
|
||||
<ProgId Id="Solidcraft.Design" Description="SolidCraft Design" Icon="SolidcraftExe" IconIndex="0">
|
||||
<Extension Id="solidcraft" ContentType="application/x-solidcraft">
|
||||
<Extension Id="sc" ContentType="application/x-solidcraft">
|
||||
<Verb Id="open" Command="Open" TargetFile="SolidcraftExe" Argument=""%1"" />
|
||||
</Extension>
|
||||
</ProgId>
|
||||
<!-- Compatibility shim (docs/compatibility.md): designs saved before the rename open as
|
||||
the same ProgId. Registered by hand: a second Extension under one ProgId would write
|
||||
its open verb twice (WIX0091). -->
|
||||
<!-- Compatibility shim (docs/compatibility.md): designs saved as .solidcraft (before .sc)
|
||||
and as .solvecraft (before the rename) open as the same ProgId. Registered by hand: a
|
||||
second Extension under one ProgId would write its open verb twice (WIX0091). -->
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\.solidcraft" Value="Solidcraft.Design" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\.solidcraft" Name="Content Type" Value="application/x-solidcraft" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\.solvecraft" Value="Solidcraft.Design" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\.solvecraft" Name="Content Type" Value="application/x-solvecraft" Type="string" />
|
||||
<!-- STEP, IGES and meshes: offered under "Open with", never forced as the default. -->
|
||||
@@ -108,6 +110,7 @@
|
||||
<!-- "Open with" app list and the Run dialog (Win+R "solidcraft"). -->
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe" Name="FriendlyAppName" Value="SolidCraft" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\shell\open\command" Value=""[#SolidcraftExe]" "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".sc" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".solidcraft" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".solvecraft" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\solidcraft.exe\SupportedTypes" Name=".step" Value="" Type="string" />
|
||||
|
||||
@@ -555,8 +555,8 @@ pub(super) fn extract(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let sc = scope(s, comp, CMD)?;
|
||||
let given = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(CMD, "`path` is required"))?;
|
||||
let file = absolute(Path::new(given));
|
||||
if !file.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("solidcraft")) {
|
||||
return Err(bad(CMD, "the new design must be a .solidcraft file"));
|
||||
if !solidcraft_io::is_design_container_path(&file) {
|
||||
return Err(bad(CMD, "the new design must be a .sc file"));
|
||||
}
|
||||
if s.path.as_deref().is_some_and(|own| absolute(Path::new(own)) == file) {
|
||||
return Err(bad(CMD, "choose another file than this design's own"));
|
||||
@@ -853,7 +853,7 @@ mod tests {
|
||||
};
|
||||
refuse(&mut s, json!({"component": 0, "path": file}), "not the design");
|
||||
refuse(&mut s, json!({"component": "Plate"}), "`path` is required");
|
||||
refuse(&mut s, json!({"component": "Plate", "path": dir.join("plate.step")}), ".solidcraft");
|
||||
refuse(&mut s, json!({"component": "Plate", "path": dir.join("plate.step")}), ".sc file");
|
||||
|
||||
// A joint from Plate's subcomponent to the rest of the design
|
||||
let pin_occ = s.doc.occurrences.iter().find(|o| o.name.starts_with("Pin")).map(|o| o.id).unwrap();
|
||||
|
||||
@@ -30,7 +30,7 @@ mod relink_tests;
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("component.insert_design", "Insert Design", insert_design).at("SOLID", "ASSEMBLE").icon("component").params(
|
||||
"path: a .solidcraft design → a new component linked to it, holding its solid bodies as placed in that design and a copy of its design properties; \
|
||||
"path: a .sc design → a new component linked to it, holding its solid bodies as placed in that design and a copy of its design properties; \
|
||||
name? (default: the design's name); parent?: component id or name (default: the active one); translate?: [x,y,z]; \
|
||||
reuse?: bool (default false): insert a free occurrence, reusing an existing link to the same file when available",
|
||||
),
|
||||
@@ -39,13 +39,13 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
→ updated, unchanged, warnings (also joints whose faces are gone or that no longer solve)",
|
||||
),
|
||||
CommandSpec::new("component.relink", "Relink Source", relink).icon("update").params(
|
||||
"component: id or name of a linked component; path: a saved .solidcraft design — replaces the source and linked bodies for all its occurrences, preserving identities and placements; returns path, previous_path, warnings, joint_errors and joint_conflicts",
|
||||
"component: id or name of a linked component; path: a saved .sc design — replaces the source and linked bodies for all its occurrences, preserving identities and placements; returns path, previous_path, warnings, joint_errors and joint_conflicts",
|
||||
),
|
||||
CommandSpec::new("component.break_link", "Break Link", break_link)
|
||||
.icon("break")
|
||||
.params("component: id or name of a linked component — keeps its bodies as they are and forgets the design file"),
|
||||
CommandSpec::new("component.extract", "Extract to File", extract::extract).icon("export").params(
|
||||
"component: id or name of a component that is not linked; path: the .solidcraft design to write → its history, subcomponents and their joints \
|
||||
"component: id or name of a component that is not linked; path: the .sc design to write → its history, subcomponents and their joints \
|
||||
move to that design, and the component becomes linked to it (as component.insert_design makes it); returns path, file, feature, bodies, \
|
||||
notices (what stays behind), warnings and copies (components that build the same bodies: see component.replace_with_link)",
|
||||
),
|
||||
@@ -53,7 +53,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
"component: id or name of a linked component → copies: [{component, name}], the components not linked whose own history builds the same bodies",
|
||||
),
|
||||
CommandSpec::new("component.replace_with_link", "Replace with Link", extract::replace_with_link).icon("chain").params(
|
||||
"component or components: [ids or names] of components that are not linked; with: a linked component, or path: a .solidcraft design \
|
||||
"component or components: [ids or names] of components that are not linked; with: a linked component, or path: a .sc design \
|
||||
(its link here, else a new linked component) → each component's history goes and its placements become placements of the linked \
|
||||
component; refused when its bodies are not the same as the link's; returns component, name, replaced and warnings",
|
||||
),
|
||||
@@ -367,8 +367,8 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let given = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "`path` is required"))?;
|
||||
let file = absolute(Path::new(given));
|
||||
if !file.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("solidcraft")) {
|
||||
return Err(bad(cmd, "choose a saved .solidcraft design"));
|
||||
if !solidcraft_io::is_design_container_path(&file) {
|
||||
return Err(bad(cmd, "choose a saved .sc design"));
|
||||
}
|
||||
// A network or device path is judged from its text before the disk is asked anything.
|
||||
link_refused(s.path.as_deref(), given).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
|
||||
@@ -27,7 +27,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("open")
|
||||
.key("Ctrl+O")
|
||||
.noundo()
|
||||
.params("path: .solidcraft design, .dxf/.svg drawing (opens as a new, unsaved design with an XY sketch, using sketch.insert_dxf/sketch.insert_svg units and defaults), or a .step/.stp/.igs/.fcstd/.3dm/.3mf/.stl/.obj/.glb/.gltf file (STEP, IGES, FreeCAD, Rhino, 3MF, STL, OBJ or glTF; opens as a new design holding the file as a component named after it, as file.insert_step and file.insert_mesh make); up?: z|y for a STEP, IGES, FreeCAD, Rhino or glTF file (see file.insert_step and file.insert_mesh); names?: auto|solid|product|instance for a STEP, IGES, FreeCAD or Rhino file (see file.insert_step); read_only?: bool (a design: saving to its own file is refused, file.save_as keeps the changes elsewhere) → open_elsewhere: {host, user, app, opened} when another SolidCraft has the design open (its lock file is fresh)"),
|
||||
.params("path: .sc design (or a .solidcraft design from an earlier release), .dxf/.svg drawing (opens as a new, unsaved design with an XY sketch, using sketch.insert_dxf/sketch.insert_svg units and defaults), or a .step/.stp/.igs/.fcstd/.3dm/.3mf/.stl/.obj/.glb/.gltf file (STEP, IGES, FreeCAD, Rhino, 3MF, STL, OBJ or glTF; opens as a new design holding the file as a component named after it, as file.insert_step and file.insert_mesh make); up?: z|y for a STEP, IGES, FreeCAD, Rhino or glTF file (see file.insert_step and file.insert_mesh); names?: auto|solid|product|instance for a STEP, IGES, FreeCAD or Rhino file (see file.insert_step); read_only?: bool (a design: saving to its own file is refused, file.save_as keeps the changes elsewhere) → open_elsewhere: {host, user, app, opened} when another SolidCraft has the design open (its lock file is fresh)"),
|
||||
CommandSpec::new("file.insert_step", "Insert STEP", insert_step)
|
||||
.at("SOLID", "INSERT")
|
||||
.icon("import")
|
||||
@@ -340,7 +340,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// The name a design never given one by its user is shown under.
|
||||
const DEFAULT_NAME: &str = "Untitled";
|
||||
|
||||
/// A design still under the default name takes its file's name (`bracket.solidcraft` →
|
||||
/// A design still under the default name takes its file's name (`bracket.sc` →
|
||||
/// "bracket"); a name the user chose is kept. `None` when nothing changes.
|
||||
fn default_name_for(name: &str, path: &str) -> Option<String> {
|
||||
let name = name.trim();
|
||||
@@ -953,6 +953,39 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A `.solidcraft` design from an earlier release opens, takes its file's name, and `file.save`
|
||||
/// writes it back to that same file (container format), never to a new `.sc` beside it.
|
||||
#[test]
|
||||
fn saving_an_opened_legacy_design_keeps_its_name() {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-legacy-ext-save-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let old = dir.join("Bracket.solidcraft");
|
||||
let mut first = Session::default();
|
||||
first.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})).unwrap();
|
||||
first.execute("file.save_as", &json!({"path": old.to_string_lossy()})).unwrap();
|
||||
let mut s = Session::default();
|
||||
s.execute("doc.open", &json!({"path": old.to_string_lossy()})).unwrap();
|
||||
assert_eq!(s.doc.name, "Bracket");
|
||||
s.execute("solid.box", &json!({"length": 5, "width": 5, "height": 5})).unwrap();
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
assert_eq!(s.path.as_deref(), old.to_str());
|
||||
let names: Vec<String> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| !n.ends_with(".bak") && !n.ends_with(".lock"))
|
||||
.collect();
|
||||
assert_eq!(names, ["Bracket.solidcraft"]);
|
||||
let bytes = std::fs::read(&old).unwrap();
|
||||
assert!(solidcraft_io::is_design_container(&bytes));
|
||||
assert_eq!(solidcraft_io::read_design(&bytes).unwrap().features.len(), 2);
|
||||
// Save As to the new extension leaves the old file as it was.
|
||||
s.execute("file.save_as", &json!({"path": dir.join("Bracket.sc").to_string_lossy()})).unwrap();
|
||||
assert!(old.is_file() && dir.join("Bracket.sc").is_file());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #5467: a script, the CLI or the control channel never gets a prompt. A Save over a file
|
||||
/// another program or computer changed since it was opened is refused with an error naming
|
||||
/// `overwrite`, which replaces it; a design opened read-only only saves to another file.
|
||||
|
||||
@@ -679,7 +679,7 @@ fn every_flat_face_of_one_sketch_extruded_up_and_down_takes_an_attached_sketch()
|
||||
assert_eq!(sketch_and_extrude_on_every_flat_face(&reopened, &dir, "synthetic-reopened"), 12);
|
||||
// The logo design as it was reported (format 17, with a removed shelled body before it).
|
||||
let mut logo = Session::default();
|
||||
run(&mut logo, "doc.open", json!({"path": root.join("logo-up-down-join.solidcraft").to_string_lossy()}));
|
||||
run(&mut logo, "doc.open", json!({"path": root.join("logo-up-down-join.sc").to_string_lossy()}));
|
||||
assert_eq!(sketch_and_extrude_on_every_flat_face(&logo, &dir, "logo"), 12);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//!
|
||||
//! - **Panics** are caught by a hook. One inside a command becomes a report with the design as
|
||||
//! it was before the command and the command itself, so `solidcraft-cli run replay.json --in
|
||||
//! design.solidcraft` reproduces it. Panics the third-party kernel raises (and
|
||||
//! design.sc` reproduces it. Panics the third-party kernel raises (and
|
||||
//! `solidcraft_kernel::guard` turns into errors) are reported the same way, marked recovered.
|
||||
//! - **Crashes no hook sees** (an abort, a stack overflow, a segfault, the process being killed)
|
||||
//! are found at the next start: every running app keeps a small, locked command journal; a
|
||||
@@ -26,7 +26,7 @@
|
||||
//! into the unclean exit's report.
|
||||
//!
|
||||
//! Reports are folders `crashes/<time>-<signature>/` holding `report.json`, and when known
|
||||
//! `design.solidcraft`, `replay.json` and `minidump.dmp`. Nothing is ever sent anywhere.
|
||||
//! `design.sc` (`design.solidcraft` in reports from earlier releases), `replay.json` and `minidump.dmp`. Nothing is ever sent anywhere.
|
||||
|
||||
use std::backtrace::Backtrace;
|
||||
use std::cell::{Cell, RefCell};
|
||||
@@ -88,7 +88,7 @@ pub struct Summary {
|
||||
pub version: String,
|
||||
/// The report's folder.
|
||||
pub path: String,
|
||||
/// `replay.json` and `design.solidcraft` are there: the crash can be replayed.
|
||||
/// `replay.json` and the design (`design.sc`) are there: the crash can be replayed.
|
||||
pub replayable: bool,
|
||||
}
|
||||
|
||||
@@ -1013,8 +1013,9 @@ fn write_report(state: &State, kind: &str, p: Option<&Panic>, extra: Extra) -> O
|
||||
let mut notes = extra.notes;
|
||||
if let Some(d) = &extra.design {
|
||||
if d.len() <= MAX_DESIGN_BYTES {
|
||||
if solidcraft_io::write_atomic(&folder.join("design.solidcraft"), d, false).is_ok() {
|
||||
files.insert("design".into(), json!("design.solidcraft"));
|
||||
let name = format!("design.{}", solidcraft_io::DESIGN_EXT);
|
||||
if solidcraft_io::write_atomic(&folder.join(&name), d, false).is_ok() {
|
||||
files.insert("design".into(), json!(name));
|
||||
}
|
||||
} else {
|
||||
notes.push(format!("the design is larger than {} MB and was not copied", MAX_DESIGN_BYTES >> 20));
|
||||
@@ -1189,7 +1190,11 @@ pub fn read(dir: &Path, id: &str) -> std::result::Result<Value, String> {
|
||||
let folder = dir.join(id);
|
||||
let mut v = read_report_file(&folder.join("report.json")).ok_or_else(|| format!("no crash report `{id}`"))?;
|
||||
v["path"] = json!(folder.to_string_lossy());
|
||||
for (k, f) in [("design_path", "design.solidcraft"), ("replay_path", "replay.json"), ("minidump_path", "minidump.dmp")] {
|
||||
// Reports from earlier releases hold `design.solidcraft`.
|
||||
if let Some(design) = solidcraft_io::DESIGN_EXTS.iter().map(|ext| folder.join(format!("design.{ext}"))).find(|p| p.is_file()) {
|
||||
v["design_path"] = json!(design.to_string_lossy());
|
||||
}
|
||||
for (k, f) in [("replay_path", "replay.json"), ("minidump_path", "minidump.dmp")] {
|
||||
if folder.join(f).is_file() {
|
||||
v[k] = json!(folder.join(f).to_string_lossy());
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ fn a_command_panic_is_reported_once_with_the_design_and_the_command() {
|
||||
assert!(!bt.iter().any(|f| f.as_str().unwrap().contains("crash::hook")), "the hook's own frames are dropped: {bt:?}");
|
||||
assert_eq!(v["recent_commands"][0]["command"], "solid.box");
|
||||
// The design from before the command, and the command: together they reproduce the crash.
|
||||
assert!(v["design_path"].as_str().unwrap().ends_with("design.sc"), "{v}");
|
||||
let design = solidcraft_io::read_design(&fs::read(v["design_path"].as_str().unwrap()).unwrap()).unwrap();
|
||||
assert_eq!(design.features.len(), 1);
|
||||
let replay: Value = serde_json::from_slice(&fs::read(v["replay_path"].as_str().unwrap()).unwrap()).unwrap();
|
||||
@@ -504,6 +505,17 @@ fn fake_report(dir: &Path, secs: u64, sig: u64, kind: &str) -> String {
|
||||
id
|
||||
}
|
||||
|
||||
/// A report folder an earlier release wrote holds `design.solidcraft`: still found.
|
||||
#[test]
|
||||
fn a_report_from_an_earlier_release_still_finds_its_design() {
|
||||
let d = fresh_dir("legacy-design");
|
||||
let id = fake_report(&d, 1_700_000_000, 7, "command_panic");
|
||||
fs::write(d.join(&id).join("design.solidcraft"), solidcraft_io::write_design(&solidcraft_doc::Document::default())).unwrap();
|
||||
let v = read(&d, &id).unwrap();
|
||||
assert!(v["design_path"].as_str().unwrap().ends_with("design.solidcraft"), "{v}");
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_stay_within_their_limits() {
|
||||
let d = fresh_dir("limits");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Autosave and crash recovery.
|
||||
//!
|
||||
//! Each open design keeps its own recovery entry in the recovery folder: `<id>.solidcraft` (the design
|
||||
//! Each open design keeps its own recovery entry in the recovery folder: `<id>.sc` (the design
|
||||
//! as of the last autosave), `<id>.json` (its file path, name and time) and `<id>.lock`, which
|
||||
//! the app holds locked while it runs. Autosaves are written atomically, only when the design
|
||||
//! has unsaved changes it hasn't written yet; a design with nothing unsaved has no entry. An
|
||||
@@ -114,12 +114,10 @@ fn read_entry(path: &Path) -> Option<Entry> {
|
||||
serde_json::from_slice(&bytes).ok()
|
||||
}
|
||||
|
||||
/// An entry's design: `<id>.solidcraft`, or `<id>.solvecraft` for an autosave copied from a
|
||||
/// pre-rename recovery folder (compatibility shim, docs/compatibility.md).
|
||||
/// An entry's design: `<id>.sc`, or `<id>.solidcraft` / `<id>.solvecraft` for an autosave an
|
||||
/// earlier release wrote (compatibility shim, docs/compatibility.md).
|
||||
fn design_file(dir: &Path, id: &str) -> PathBuf {
|
||||
let current = file(dir, id, solidcraft_io::DESIGN_EXT);
|
||||
let legacy = file(dir, id, solidcraft_io::LEGACY_DESIGN_EXT);
|
||||
if !current.is_file() && legacy.is_file() { legacy } else { current }
|
||||
solidcraft_io::DESIGN_EXTS.iter().map(|ext| file(dir, id, ext)).find(|p| p.is_file()).unwrap_or_else(|| file(dir, id, solidcraft_io::DESIGN_EXT))
|
||||
}
|
||||
|
||||
fn file(dir: &Path, id: &str, ext: &str) -> PathBuf {
|
||||
@@ -170,13 +168,13 @@ pub fn load(dir: &Path, id: &str) -> Result<(Document, Entry)> {
|
||||
}
|
||||
|
||||
/// Delete an entry. Only a real one: `<id>.json` must describe entry `id` and its design must
|
||||
/// be there, so a folder and id that merely name someone's `part.solidcraft` delete nothing.
|
||||
/// be there, so a folder and id that merely name someone's `part.sc` delete nothing.
|
||||
pub fn remove(dir: &Path, id: &str) -> Result<()> {
|
||||
let is_entry = valid_id(id) && read_entry(&file(dir, id, "json")).is_some_and(|e| e.id == id) && design_file(dir, id).is_file();
|
||||
if !is_entry {
|
||||
return Err(EngineError::Other(format!("no recovered design `{id}`")));
|
||||
}
|
||||
for ext in ["json", solidcraft_io::DESIGN_EXT, solidcraft_io::LEGACY_DESIGN_EXT, "lock"] {
|
||||
for ext in ["json", "lock"].into_iter().chain(solidcraft_io::DESIGN_EXTS) {
|
||||
let _ = fs::remove_file(file(dir, id, ext));
|
||||
}
|
||||
Ok(())
|
||||
@@ -331,7 +329,7 @@ impl Autosaver {
|
||||
self.last_at = Instant::now();
|
||||
if !s.is_dirty() {
|
||||
if self.has_entry {
|
||||
for ext in ["json", "solidcraft"] {
|
||||
for ext in ["json"].into_iter().chain(solidcraft_io::DESIGN_EXTS) {
|
||||
let _ = fs::remove_file(file(&self.dir, &self.id, ext));
|
||||
}
|
||||
self.has_entry = false;
|
||||
@@ -387,7 +385,7 @@ impl Autosaver {
|
||||
if let Some(w) = self.writing.take() {
|
||||
let _ = w.done.recv();
|
||||
}
|
||||
for ext in ["json", solidcraft_io::DESIGN_EXT] {
|
||||
for ext in ["json"].into_iter().chain(solidcraft_io::DESIGN_EXTS) {
|
||||
let _ = fs::remove_file(file(&self.dir, &self.id, ext));
|
||||
}
|
||||
self.lock = None;
|
||||
@@ -409,19 +407,37 @@ impl Autosaver {
|
||||
mod tests {
|
||||
#[test]
|
||||
fn autosave_from_a_legacy_folder_is_listed_and_loads() {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-recovery-legacy-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
fs::create_dir_all(&d).unwrap();
|
||||
let id = "1-2-3";
|
||||
let doc = Document::default();
|
||||
fs::write(d.join(format!("{id}.solvecraft")), solidcraft_io::write_design(&doc)).unwrap();
|
||||
let entry = Entry { id: id.into(), path: Some("/d/part.solvecraft".into()), name: "part".into(), saved_at: 1, features: 0 };
|
||||
fs::write(d.join(format!("{id}.json")), serde_json::to_vec(&entry).unwrap()).unwrap();
|
||||
assert_eq!(orphans(&d).iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), [id]);
|
||||
let (_, loaded) = load(&d, id).unwrap();
|
||||
assert_eq!(loaded, entry);
|
||||
remove(&d, id).unwrap();
|
||||
assert!(orphans(&d).is_empty() && !d.join(format!("{id}.solvecraft")).exists());
|
||||
// Autosaves written by earlier releases: `.solvecraft` (before the rename) and
|
||||
// `.solidcraft` (before designs were saved as `.sc`).
|
||||
for ext in solidcraft_io::LEGACY_DESIGN_EXTS {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-recovery-legacy-{ext}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
fs::create_dir_all(&d).unwrap();
|
||||
let id = "1-2-3";
|
||||
let doc = Document::default();
|
||||
fs::write(d.join(format!("{id}.{ext}")), solidcraft_io::write_design(&doc)).unwrap();
|
||||
let entry = Entry { id: id.into(), path: Some(format!("/d/part.{ext}")), name: "part".into(), saved_at: 1, features: 0 };
|
||||
fs::write(d.join(format!("{id}.json")), serde_json::to_vec(&entry).unwrap()).unwrap();
|
||||
assert_eq!(orphans(&d).iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), [id], ".{ext}");
|
||||
let (_, loaded) = load(&d, id).unwrap();
|
||||
assert_eq!(loaded, entry);
|
||||
remove(&d, id).unwrap();
|
||||
assert!(orphans(&d).is_empty() && !d.join(format!("{id}.{ext}")).exists(), ".{ext}");
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
}
|
||||
}
|
||||
|
||||
/// New autosaves are written as `<id>.sc`.
|
||||
#[test]
|
||||
fn autosaves_are_written_as_sc() {
|
||||
let d = dir("sc-ext");
|
||||
let mut s = Session::default();
|
||||
let mut a = Autosaver::new(&d, Duration::ZERO).unwrap();
|
||||
s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})).unwrap();
|
||||
assert!(a.save(&s).unwrap());
|
||||
assert!(d.join(format!("{}.sc", a.id)).is_file());
|
||||
assert!(!d.join(format!("{}.solidcraft", a.id)).exists());
|
||||
a.discard();
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
@@ -551,7 +567,7 @@ mod tests {
|
||||
let mut t = Session::default();
|
||||
let r = t.execute("doc.recover", &json!({"dir": d.to_string_lossy(), "id": id, "discard": true})).unwrap();
|
||||
assert_eq!(r["features"], 1, "{r}");
|
||||
assert!(orphans(&d).is_empty() && !file(&d, &id, "json").exists() && !file(&d, &id, "solidcraft").exists());
|
||||
assert!(orphans(&d).is_empty() && !file(&d, &id, "json").exists() && !file(&d, &id, solidcraft_io::DESIGN_EXT).exists());
|
||||
let part = d.join("part.solidcraft");
|
||||
fs::write(&part, solidcraft_io::write_design(&Document::default())).unwrap();
|
||||
assert!(t.execute("doc.recover", &json!({"dir": d.to_string_lossy(), "id": "part", "discard": true})).is_err());
|
||||
@@ -568,7 +584,7 @@ mod tests {
|
||||
assert!(a.tick(&s, false).unwrap());
|
||||
s.mark_saved();
|
||||
a.tick(&s, false).unwrap();
|
||||
assert!(!file(&d, &a.id, "solidcraft").exists());
|
||||
assert!(!file(&d, &a.id, solidcraft_io::DESIGN_EXT).exists());
|
||||
a.close(&s);
|
||||
assert_eq!(fs::read_dir(&d).unwrap().count(), 0);
|
||||
// Closing with unsaved changes keeps them for the next launch.
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::{run, volume};
|
||||
use crate::Session;
|
||||
|
||||
fn open_fixture() -> Session {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/cubeish-chamfer-too-large.solidcraft");
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/cubeish-chamfer-too-large.sc");
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "doc.open", json!({"path": path.to_string_lossy()}));
|
||||
s
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
//! #1490: the owner's shelled battery case (`battery-tpu-case-v2.solidcraft`, format 21, saved
|
||||
//! #1490: the owner's shelled battery case (`battery-tpu-case-v2.sc`, format 21, saved
|
||||
//! at `wall = 2.5 mm`) with a pleat loft joined into a window in one end wall. The loft stands on
|
||||
//! the case's open rim and its end strips lie in the wall's inner face. At walls of 2.4–2.6 mm
|
||||
//! the join failed with "no result": every exact route was refused, and the last resort's
|
||||
@@ -12,7 +12,7 @@ use super::run;
|
||||
use crate::Session;
|
||||
|
||||
fn open() -> Session {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/battery-tpu-case-v2.solidcraft");
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/battery-tpu-case-v2.sc");
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "doc.open", json!({"path": path.to_string_lossy()}));
|
||||
s
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! cut) was never meshed: its boundary was projected onto its surface starting from a corner
|
||||
//! that a search without a hint misses, so the face was dropped. Every boolean with the body
|
||||
//! then failed (truck's meshes both operands to classify faces), and the exact fallback refused
|
||||
//! it for the same face. The owner's design, `battery-tpu-case.solidcraft` (format 18).
|
||||
//! it for the same face. The owner's design, `battery-tpu-case.sc` (format 18).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@@ -13,7 +13,7 @@ use super::run;
|
||||
use crate::Session;
|
||||
|
||||
fn open_at(feature: &str) -> Session {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/battery-tpu-case.solidcraft");
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/battery-tpu-case.sc");
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "doc.open", json!({"path": path.to_string_lossy()}));
|
||||
run(&mut s, "timeline.roll_to", json!({"feature": feature}));
|
||||
|
||||
@@ -178,7 +178,7 @@ pub static FORMATS: &[FormatSpec] = &[
|
||||
FormatSpec {
|
||||
id: "design",
|
||||
label: "SolidCraft design",
|
||||
extensions: &[crate::DESIGN_EXT, crate::LEGACY_DESIGN_EXT, "json"],
|
||||
extensions: &[crate::DESIGN_EXTS[0], crate::DESIGN_EXTS[1], crate::DESIGN_EXTS[2], "json"],
|
||||
role: Role::Design,
|
||||
import: Some(ImportSpec { target: ImportTarget::Design, options: &[], native_only: false }),
|
||||
export: None,
|
||||
@@ -470,7 +470,7 @@ mod tests {
|
||||
fn import_filter_groups_hold_the_expected_extensions() {
|
||||
let native = import_filters(true);
|
||||
let find = |label: &str| native.iter().find(|(l, _)| *l == label).map(|(_, e)| e.clone()).unwrap_or_default();
|
||||
assert_eq!(find("SolidCraft design (container or JSON)"), vec![crate::DESIGN_EXT, crate::LEGACY_DESIGN_EXT, "json"]);
|
||||
assert_eq!(find("SolidCraft design (container or JSON)"), vec!["sc", "solidcraft", "solvecraft", "json"]);
|
||||
assert_eq!(find("STEP"), vec!["step", "stp"]);
|
||||
assert_eq!(find("IGES"), vec!["igs", "iges"]);
|
||||
assert_eq!(find("FreeCAD and Rhino"), vec!["fcstd", "3dm"]);
|
||||
|
||||
+30
-18
@@ -1,4 +1,4 @@
|
||||
//! File formats. Designs use JSON or versioned containers (`.solidcraft`); the authoritative
|
||||
//! File formats. Designs use JSON or versioned containers (`.sc`; `.solidcraft` and `.solvecraft` still open); the authoritative
|
||||
//! document rebuilds on load and optional display caches remain provisional. Bodies export
|
||||
//! as STL (binary or ASCII), OBJ, STEP and 3MF, and assemblies as URDF ([`urdf()`]); STEP, IGES,
|
||||
//! FreeCAD and Rhino files import as a base feature (see [`step_import_feature`]), 3MF and
|
||||
@@ -46,27 +46,35 @@ pub use urdf::{UrdfOptions, UrdfPackage, urdf};
|
||||
use solidcraft_doc::{Document, Metadata, ModelState};
|
||||
use solidcraft_geom::{Mesh, Vec3};
|
||||
|
||||
/// Design file extension.
|
||||
pub const DESIGN_EXT: &str = "solidcraft";
|
||||
/// The design extension of earlier releases. Compatibility shim (docs/compatibility.md): such
|
||||
/// files still open everywhere a design opens; new designs are saved as [`DESIGN_EXT`].
|
||||
pub const LEGACY_DESIGN_EXT: &str = "solvecraft";
|
||||
/// Design file extension: new designs, autosaves and recovery files are written as `.sc`.
|
||||
pub const DESIGN_EXT: &str = "sc";
|
||||
/// The design extensions of earlier releases, newest first. Compatibility shim
|
||||
/// (docs/compatibility.md): such files still open everywhere a design opens, and Save keeps
|
||||
/// writing an opened file under its own name; new designs are saved as [`DESIGN_EXT`].
|
||||
pub const LEGACY_DESIGN_EXTS: [&str; 2] = ["solidcraft", "solvecraft"];
|
||||
/// Every extension a design file may have, current first.
|
||||
pub const DESIGN_EXTS: [&str; 2] = [DESIGN_EXT, LEGACY_DESIGN_EXT];
|
||||
pub const DESIGN_EXTS: [&str; 3] = [DESIGN_EXT, LEGACY_DESIGN_EXTS[0], LEGACY_DESIGN_EXTS[1]];
|
||||
|
||||
/// Is this a design file name (`.solidcraft`, or the legacy extension), any case?
|
||||
/// Is this extension (without the dot) one a design file may have (`sc` or a legacy one), any case?
|
||||
pub fn is_design_ext(ext: &str) -> bool {
|
||||
DESIGN_EXTS.iter().any(|x| ext.eq_ignore_ascii_case(x))
|
||||
}
|
||||
|
||||
/// Is this a design file name (`.sc`, a legacy extension, or plain `.json`), any case?
|
||||
pub fn is_design_path(path: &str) -> bool {
|
||||
std::path::Path::new(path)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|e| DESIGN_EXTS.iter().chain(["json"].iter()).any(|x| e.eq_ignore_ascii_case(x)))
|
||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| is_design_ext(e) || e.eq_ignore_ascii_case("json"))
|
||||
}
|
||||
|
||||
/// Is this a design container file (`.sc` or a legacy extension, not plain JSON), any case?
|
||||
pub fn is_design_container_path(path: &std::path::Path) -> bool {
|
||||
path.extension().and_then(|e| e.to_str()).is_some_and(is_design_ext)
|
||||
}
|
||||
/// Largest design file we read.
|
||||
pub const MAX_DESIGN_BYTES: usize = 256 << 20;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IoError {
|
||||
#[error("unsupported format `{0}` (use stl, obj, step, 3mf, urdf or solidcraft)")]
|
||||
#[error("unsupported format `{0}` (use stl, obj, step, 3mf, urdf or sc)")]
|
||||
Format(String),
|
||||
#[error("nothing to export: the design has no bodies")]
|
||||
Empty,
|
||||
@@ -346,7 +354,7 @@ impl Format {
|
||||
"3mf" => Format::ThreeMf,
|
||||
"3mf-tagged" => Format::ThreeMfTagged,
|
||||
"urdf" => Format::Urdf,
|
||||
"solidcraft" | "solvecraft" | "json" => Format::Design,
|
||||
e if e == "json" || is_design_ext(e) => Format::Design,
|
||||
_ => return Err(IoError::Format(ext)),
|
||||
})
|
||||
}
|
||||
@@ -577,7 +585,7 @@ pub fn export_with_quality(
|
||||
let objs: Vec<MeshObject> = sel.iter().map(|b| mesh_object(b, format == Format::ThreeMfTagged, quality)).collect::<Result<_>>()?;
|
||||
write_3mf_with(&objs, &threemf_metadata(meta))?
|
||||
}
|
||||
Format::Design => return Err(IoError::Format("solidcraft (save the document instead)".into())),
|
||||
Format::Design => return Err(IoError::Format("sc (save the document instead)".into())),
|
||||
Format::Urdf => return Err(IoError::Format("urdf (an assembly package; use file.export)".into())),
|
||||
})
|
||||
}
|
||||
@@ -944,10 +952,14 @@ mod tests {
|
||||
assert!(step_import_feature(b"ISO-10303-21;", "x.step").is_err());
|
||||
assert!(step_import_feature(&[0xff, 0xfe, 0x00], "x.step").is_err());
|
||||
assert!(Format::from_name("a.dwg").is_err());
|
||||
// Compatibility shim: the legacy design extension is still a design.
|
||||
// Compatibility shim: the legacy design extensions are still designs.
|
||||
assert_eq!(Format::from_name("old.solvecraft").unwrap(), Format::Design);
|
||||
assert_eq!(Format::from_name("new.solidcraft").unwrap(), Format::Design);
|
||||
assert!(is_design_path("/d/old.SOLVECRAFT") && is_design_path("new.solidcraft") && !is_design_path("a.step"));
|
||||
assert_eq!(Format::from_name("older.solidcraft").unwrap(), Format::Design);
|
||||
assert_eq!(Format::from_name("new.sc").unwrap(), Format::Design);
|
||||
assert_eq!(Format::from_name("NEW.SC").unwrap(), Format::Design);
|
||||
assert!(is_design_path("/d/old.SOLVECRAFT") && is_design_path("older.solidcraft") && is_design_path("new.sc") && !is_design_path("a.step"));
|
||||
assert_eq!(DESIGN_EXT, "sc");
|
||||
assert_eq!(DESIGN_EXTS, ["sc", "solidcraft", "solvecraft"], "new extension first, then every older one");
|
||||
assert!(read_stl(&[0u8; 10]).is_err());
|
||||
let mut fake = vec![0u8; 84];
|
||||
fake[80..84].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
|
||||
@@ -826,6 +826,28 @@ fn legacy_design_files_and_resource_uris() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let d = call(&mut s, 1, "resources/read", json!({"uri": "solvecraft://design"}));
|
||||
assert!(d["result"]["contents"][0]["text"].as_str().unwrap().contains("timeline"));
|
||||
|
||||
// `.sc` is what new designs are saved as; a `.solidcraft` file opens and `save {}` writes it
|
||||
// back under its own name.
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-mcp-ext-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let (new, old) = (dir.join("part.sc"), dir.join("part.solidcraft"));
|
||||
let mut s = server();
|
||||
tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10}}));
|
||||
tool(&mut s, "save", json!({"path": new.to_string_lossy()}));
|
||||
assert!(new.is_file());
|
||||
std::fs::rename(&new, &old).unwrap();
|
||||
tool(&mut s, "open", json!({"path": old.to_string_lossy()}));
|
||||
tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 5, "width": 5, "height": 5}}));
|
||||
tool(&mut s, "save", json!({}));
|
||||
assert!(old.is_file() && !new.exists(), "save keeps the opened file's name");
|
||||
assert_eq!(solidcraft_engine::io::read_design(&std::fs::read(&old).unwrap()).unwrap().features.len(), 2);
|
||||
tool(&mut s, "open", json!({"path": old.to_string_lossy()}));
|
||||
tool(&mut s, "save", json!({"path": new.to_string_lossy()}));
|
||||
tool(&mut s, "open", json!({"path": new.to_string_lossy()}));
|
||||
assert!(old.is_file() && new.is_file(), "save as leaves the old file");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// The default folders (#205): the recovery folder works out of the box, `screenshot` without a
|
||||
|
||||
@@ -296,7 +296,7 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
tool(
|
||||
"open",
|
||||
"Open a design",
|
||||
"Open a .solidcraft design file, a STEP file (.step/.stp: a new design whose bodies come from the file, as an \
|
||||
"Open a .sc design file (or a .solidcraft one from an earlier release), a STEP file (.step/.stp: a new design whose bodies come from the file, as an \
|
||||
Import base feature in a component named after the file; an assembly's parts become nested components) or a \
|
||||
mesh (.3mf/.stl/.obj/.glb/.gltf: mesh bodies in such a component; a glTF file's nodes become nested components and its \
|
||||
materials appearances); the result reports the component (id, component_name). Replaces the current design; insert into it \
|
||||
@@ -305,7 +305,7 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
so it is turned by default (`up: \"z\"` keeps its axes). `names` picks what the bodies are named after when the solids' own names say nothing useful.",
|
||||
obj(
|
||||
json!({
|
||||
"path": string("Path to a .solidcraft, .step/.stp, .3mf, .stl, .obj, .glb or .gltf file"),
|
||||
"path": string("Path to a .sc (or .solidcraft), .step/.stp, .3mf, .stl, .obj, .glb or .gltf file"),
|
||||
"up": {"type": "string", "enum": ["z", "y"], "description": "STEP and glTF only. z keeps the file's axes; y: the file has Y up, so its bodies are turned to stand Z up. Default: z for STEP, y for glTF"},
|
||||
"names": {"type": "string", "enum": ["auto", "solid", "product", "instance"], "description": "STEP, IGES, FreeCAD and Rhino. auto (default): keep meaningful solid names, replace placeholders with the nearest named product, else instance; solid: the solid's own name; product: the part's name; instance: the occurrence's name, else the product's"},
|
||||
}),
|
||||
@@ -316,9 +316,9 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
tool(
|
||||
"save",
|
||||
"Save the design",
|
||||
"Save the design to `path` or its current file. The default is a design container; a .json path selects plain JSON. Set format to container or json to override the path.",
|
||||
"Save the design to `path` or its current file (a design opened as .solidcraft keeps that name). New designs are .sc files. The default is a design container; a .json path selects plain JSON. Set format to container or json to override the path.",
|
||||
obj(
|
||||
json!({"path": string("Path to save to (default: the current file)"), "format": {"type": "string", "enum": ["container", "json"], "description": "Design encoding (default: JSON for .json, container otherwise)"}}),
|
||||
json!({"path": string("Path to save to, e.g. part.sc (default: the current file)"), "format": {"type": "string", "enum": ["container", "json"], "description": "Design encoding (default: JSON for .json, container otherwise)"}}),
|
||||
&[],
|
||||
),
|
||||
Effect::Files,
|
||||
|
||||
@@ -157,7 +157,7 @@ fn main() {
|
||||
};
|
||||
// "save": only the save (the design is there already).
|
||||
if text.contains("save") {
|
||||
let path = std::env::var_os("HOME").map(std::path::PathBuf::from).unwrap_or_default().join("Documents/block.solidcraft");
|
||||
let path = std::env::var_os("HOME").map(std::path::PathBuf::from).unwrap_or_default().join("Documents/block.sc");
|
||||
let title = format!("save {}", path.display());
|
||||
say(session, "I'll save the design to your Documents folder.");
|
||||
tool(session, "t4", &title, "edit", "in_progress", None);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! A tool call in words, for its card (#1457): "Extrude Sketch1 by 10 mm", "Box 40 × 30 × 20 mm,
|
||||
//! then Fillet 4 edges (r 4 mm)", "Saved block.solidcraft".
|
||||
//! then Fillet 4 edges (r 4 mm)", "Saved block.sc".
|
||||
//!
|
||||
//! A first pass built from the command's translated label, its main parameters (lengths in the
|
||||
//! design's units) and the bodies the result names. Anything it doesn't know (another agent's own
|
||||
|
||||
@@ -1546,7 +1546,7 @@ mod tests {
|
||||
h.frames(3);
|
||||
h.settle();
|
||||
assert!(!h.app.home.open);
|
||||
assert_eq!(h.app.session.path.as_deref(), h.dir.join("Copy.solidcraft").to_str());
|
||||
assert_eq!(h.app.session.path.as_deref(), h.dir.join("Copy.sc").to_str());
|
||||
assert_eq!(std::fs::read(source).unwrap(), bytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -547,7 +547,7 @@ pub fn crash_draft(report: &serde_json::Value) -> Draft {
|
||||
if recent.is_empty() { "(none recorded)".to_string() } else { recent.join(", ") },
|
||||
s("id"),
|
||||
match (report.get("design_path").is_some(), report.get("replay_path").is_some()) {
|
||||
(true, true) => "the design as it was before the crash (design.solidcraft) and the commands that crash it (replay.json)",
|
||||
(true, true) => "the design as it was before the crash (design.sc) and the commands that crash it (replay.json)",
|
||||
(false, true) => "the commands that crash it on a new design (replay.json)",
|
||||
_ => "no replay (see its notes)",
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) fn start(app: &mut SolveApp, component: u64) {
|
||||
return;
|
||||
}
|
||||
let document = app.session.doc.clone();
|
||||
app.pick_open_for(&["solidcraft"], move |app, path| {
|
||||
app.pick_open_for(&solidcraft_engine::io::DESIGN_EXTS, move |app, path| {
|
||||
let Some(path) = path else { return };
|
||||
// A host or programmatic caller can replace/edit a design while a native picker is
|
||||
// pending. Never apply that old component id to a different design or newer revision.
|
||||
@@ -68,14 +68,15 @@ pub(crate) fn extract(app: &mut SolveApp, component: u64) {
|
||||
}
|
||||
let Some(name) = app.session.doc.components.iter().find(|c| c.id == component).map(|c| c.name.clone()) else { return };
|
||||
let document = app.session.doc.clone();
|
||||
app.pick_save(&format!("{name}.solidcraft"), &["solidcraft"], move |app, path| {
|
||||
let ext = solidcraft_engine::io::DESIGN_EXT;
|
||||
app.pick_save(&format!("{name}.{ext}"), &[ext], move |app, path| {
|
||||
let Some(mut path) = path else { return };
|
||||
if !Arc::ptr_eq(&document, &app.session.doc) {
|
||||
app.set_status(app.tr_label("ui.extract_design_changed").to_string(), true);
|
||||
return;
|
||||
}
|
||||
if std::path::Path::new(&path).extension().is_none() {
|
||||
path.push_str(".solidcraft");
|
||||
path = format!("{path}.{ext}");
|
||||
}
|
||||
if let Ok(result) = app.run("component.extract", json!({"component": component, "path": path})) {
|
||||
report(app, &result, "ui.extract_done");
|
||||
@@ -177,7 +178,7 @@ mod tests {
|
||||
|
||||
fn answer(app: &mut SolveApp, path: Option<String>) {
|
||||
app.services.pick_open = Some(Box::new(move |extensions| {
|
||||
assert_eq!(extensions, &["solidcraft"]);
|
||||
assert_eq!(extensions, &["sc", "solidcraft", "solvecraft"], "a design of any release");
|
||||
let result = path.clone();
|
||||
Box::pin(async move { result })
|
||||
}));
|
||||
@@ -271,15 +272,15 @@ mod tests {
|
||||
let item = find(&app, component).unwrap();
|
||||
let chosen = folder.join("bracket").to_string_lossy().into_owned();
|
||||
app.services.pick_save = Some(Box::new(move |name, extensions| {
|
||||
assert_eq!((name, extensions), ("Bracket.solidcraft", &["solidcraft"][..]));
|
||||
assert_eq!((name, extensions), ("Bracket.sc", &["sc"][..]));
|
||||
let result = Some(chosen.clone());
|
||||
Box::pin(async move { result })
|
||||
}));
|
||||
context_menu::run_item(&mut app, &item, egui::Pos2::ZERO);
|
||||
assert!(app.file_picker_busy());
|
||||
crate::file_picker::tick(&mut app, &egui::Context::default());
|
||||
assert!(folder.join("bracket.solidcraft").is_file());
|
||||
assert!(app.session.doc.links[&component].path.ends_with("bracket.solidcraft"));
|
||||
assert!(folder.join("bracket.sc").is_file());
|
||||
assert!(app.session.doc.links[&component].path.ends_with("bracket.sc"));
|
||||
assert!(app.status.as_ref().is_some_and(|(s, _, error)| s.contains("its own design") && !error));
|
||||
assert!(find(&app, component).is_none(), "a linked component is not extracted again");
|
||||
assert!(menu(&app, Target::Component { id: component }).is_some());
|
||||
|
||||
@@ -402,7 +402,7 @@ mod tests {
|
||||
edit(&mut app);
|
||||
tick_and_wait(&mut app);
|
||||
let id = entries(&dir)[0].id.clone();
|
||||
let blocked = dir.join(format!("{id}.solidcraft"));
|
||||
let blocked = dir.join(format!("{id}.sc"));
|
||||
fs::remove_file(&blocked).unwrap();
|
||||
fs::create_dir(&blocked).unwrap();
|
||||
edit(&mut app);
|
||||
|
||||
@@ -790,7 +790,7 @@ fn save_as_named(app: &mut SolveApp, name: &str, extension: &str) {
|
||||
});
|
||||
}
|
||||
|
||||
/// File > Download design (browser build): the open design as a `.solidcraft` file download.
|
||||
/// File > Download design (browser build): the open design as a `.sc` file download.
|
||||
fn download_design(app: &mut SolveApp) {
|
||||
let name = format!("{}.{}", app.session.doc.name, solidcraft_engine::io::DESIGN_EXT);
|
||||
let result = app
|
||||
@@ -2423,6 +2423,49 @@ mod header_report_tests {
|
||||
assert!(FileAction::Export("step").enabled(&h.app));
|
||||
}
|
||||
|
||||
/// New designs are saved as `.sc`. A design opened from a `.solidcraft` file is saved back to
|
||||
/// that file by Save (no rename behind the user's back); Save As offers `.sc` and leaves it.
|
||||
#[test]
|
||||
fn save_offers_sc_and_keeps_an_opened_solidcraft_file() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
let offered = std::rc::Rc::new(std::cell::RefCell::new(Vec::<(String, Vec<String>)>::new()));
|
||||
let (seen, dir) = (offered.clone(), h.dir.clone());
|
||||
h.app.services.pick_save = Some(Box::new(move |name, extensions| {
|
||||
seen.borrow_mut().push((name.to_string(), extensions.iter().map(|e| e.to_string()).collect()));
|
||||
let path = dir.join(name).to_string_lossy().into_owned();
|
||||
Box::pin(std::future::ready(Some(path)))
|
||||
}));
|
||||
h.app.run("solid.box", json!({"length":2,"width":3,"height":4})).unwrap();
|
||||
h.app.session.doc_mut().name = "Bracket".into();
|
||||
FileAction::Save.run(&mut h.app);
|
||||
h.frames(3);
|
||||
h.settle();
|
||||
assert_eq!(offered.borrow().last().map(|(n, e)| (n.as_str(), e.clone())), Some(("Bracket.sc", vec!["sc".to_string(), "json".to_string()])));
|
||||
assert!(h.dir.join("Bracket.sc").is_file(), "a new design is saved as .sc");
|
||||
|
||||
// A design from an earlier release.
|
||||
let old = h.dir.join("Old.solidcraft");
|
||||
std::fs::rename(h.dir.join("Bracket.sc"), &old).unwrap();
|
||||
h.app.run("doc.open", json!({"path": old.to_string_lossy()})).unwrap();
|
||||
h.app.run("solid.box", json!({"length":1,"width":1,"height":1})).unwrap();
|
||||
let asked = offered.borrow().len();
|
||||
FileAction::Save.run(&mut h.app);
|
||||
h.frames(3);
|
||||
h.settle();
|
||||
assert_eq!(offered.borrow().len(), asked, "Save writes the opened file without asking");
|
||||
assert_eq!(h.app.session.path.as_deref(), Some(old.to_string_lossy().as_ref()));
|
||||
assert!(!h.app.session.is_dirty());
|
||||
assert_eq!(solidcraft_engine::io::read_design(&std::fs::read(&old).unwrap()).unwrap().features.len(), 2);
|
||||
assert!(!h.dir.join("Old.sc").exists() && !h.dir.join("Bracket.sc").exists(), "nothing renamed");
|
||||
|
||||
FileAction::SaveAs.run(&mut h.app);
|
||||
h.frames(3);
|
||||
h.settle();
|
||||
let name = offered.borrow().last().map(|(n, _)| n.clone()).unwrap();
|
||||
assert!(name.ends_with(".sc"), "Save As offers .sc: {name}");
|
||||
assert!(h.dir.join(&name).is_file() && old.is_file(), "Save As leaves the .solidcraft file as it was");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_menu_actions_are_disabled_behind_an_open_issue_report() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
@@ -2558,7 +2601,7 @@ mod header_report_tests {
|
||||
h.step(&json!({"widget":"Download design"})).unwrap();
|
||||
let got = got.borrow();
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].0, "Plate.solidcraft");
|
||||
assert_eq!(got[0].0, "Plate.sc");
|
||||
assert!(solidcraft_engine::io::is_design_container(&got[0].1));
|
||||
assert!(h.app.session.path.is_none(), "a download is no save: the design keeps no file");
|
||||
}
|
||||
@@ -2577,11 +2620,7 @@ mod header_report_tests {
|
||||
h.step(&json!({"key":"S","cmd":true,"shift":true})).unwrap();
|
||||
h.frames(3);
|
||||
h.settle();
|
||||
assert_eq!(
|
||||
h.app.session.path.as_deref(),
|
||||
h.dir.join("Copy.solidcraft").to_str(),
|
||||
"Ctrl+Shift+S must open Save As, not save over the current file"
|
||||
);
|
||||
assert_eq!(h.app.session.path.as_deref(), h.dir.join("Copy.sc").to_str(), "Ctrl+Shift+S must open Save As, not save over the current file");
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes, "Save As keeps the file the design came from");
|
||||
// The File menu shows the shortcut of the command it runs.
|
||||
let spec = solidcraft_engine::find_command("file.save_as").expect("file.save_as is a command");
|
||||
|
||||
@@ -308,7 +308,7 @@ const ELEMENTS: &[Element] = &[
|
||||
super::file_view::RecoveryMessage {
|
||||
title: "1 design was recovered",
|
||||
before_filename: " after SolidCraft closed unexpectedly: ",
|
||||
filename: "enclosure-lid.solidcraft",
|
||||
filename: "enclosure-lid.sc",
|
||||
after_filename: ", 2 minutes ago.",
|
||||
},
|
||||
"Recover",
|
||||
@@ -326,7 +326,7 @@ const ELEMENTS: &[Element] = &[
|
||||
id,
|
||||
RecentRow {
|
||||
title: "Sample Plate",
|
||||
path: "Designs/sample-plate.solidcraft",
|
||||
path: "Designs/sample-plate.sc",
|
||||
when: "Open now",
|
||||
workspace: "Design",
|
||||
thumbnail: None,
|
||||
@@ -342,7 +342,7 @@ const ELEMENTS: &[Element] = &[
|
||||
"gallery:file:recent",
|
||||
RecentRow {
|
||||
title: "Sample Plate",
|
||||
path: "Designs/sample-plate.solidcraft",
|
||||
path: "Designs/sample-plate.sc",
|
||||
when: "Open now",
|
||||
workspace: "Design",
|
||||
thumbnail: None,
|
||||
@@ -1952,7 +1952,7 @@ fn file_view_application_evidence_in_both_themes_and_widths() {
|
||||
std::fs::create_dir_all(&output_dir).unwrap();
|
||||
let fixture_dir = std::env::temp_dir().join(format!("solidcraft-file-evidence-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&fixture_dir).unwrap();
|
||||
let path = fixture_dir.join("sample-plate.solidcraft");
|
||||
let path = fixture_dir.join("sample-plate.sc");
|
||||
let fixture = path.to_string_lossy().to_string();
|
||||
let mut sample = crate::test_support::sample_app();
|
||||
sample.session.execute("file.save_as", &serde_json::json!({"path":fixture})).unwrap();
|
||||
@@ -1971,7 +1971,7 @@ fn file_view_application_evidence_in_both_themes_and_widths() {
|
||||
app.ui.dark = dark;
|
||||
app.recoverable.push(solidcraft_engine::recovery::Entry {
|
||||
id: "public-evidence".into(),
|
||||
path: Some("enclosure-lid.solidcraft".into()),
|
||||
path: Some("enclosure-lid.sc".into()),
|
||||
name: "Enclosure lid".into(),
|
||||
saved_at: web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).unwrap().as_secs().saturating_sub(120),
|
||||
features: 14,
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"params": {
|
||||
"command": "file.save_as",
|
||||
"params": {
|
||||
"path": "$DIR/enclosure.solidcraft"
|
||||
"path": "$DIR/enclosure.sc"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -165,7 +165,7 @@
|
||||
"params": {
|
||||
"command": "doc.open",
|
||||
"params": {
|
||||
"path": "$DIR/enclosure.solidcraft"
|
||||
"path": "$DIR/enclosure.sc"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
{"click": {"handle": "params:close"}},
|
||||
{"expect": {"param": {"width": 100}, "errors": 0}},
|
||||
{"click": {"handle": "titlebar:save"}},
|
||||
{"expect": {"file": "Sample Plate.solidcraft"}},
|
||||
{"expect": {"file": "Sample Plate.sc"}},
|
||||
{"key": "N", "cmd": true},
|
||||
{"expect": {"bodies": 0}},
|
||||
{"choose_file": "Sample Plate.solidcraft"},
|
||||
{"choose_file": "Sample Plate.sc"},
|
||||
{"key": "O", "cmd": true},
|
||||
{"until": {"file_busy": false}},
|
||||
{"debug": 1},
|
||||
|
||||
@@ -44,7 +44,7 @@ records are reported. Unsupported destructive links refuse the entire copy. See
|
||||
|
||||
**Extract to File** (`component.extract`, in a component's right-click menu) moves a component
|
||||
into a design of its own: its sketches, features, bodies, subcomponents and the joints among them
|
||||
go to the new `.solidcraft` file at its origin, with the parameters they use, and the component
|
||||
go to the new `.sc` file at its origin, with the parameters they use, and the component
|
||||
becomes linked to that file, as **Insert Design** makes it. Its placements and the joints to the
|
||||
rest of the assembly stay. Components whose own history builds the same bodies are offered to
|
||||
become placements of the new link too (`component.copies`, `component.replace_with_link`).
|
||||
@@ -362,7 +362,7 @@ the part's bodies only: its timeline, parameters and sheet-metal data (flat patt
|
||||
table) stay in the part's own design.
|
||||
|
||||
If a source file moves or is renamed, right-click its linked component or occurrence in the
|
||||
Browser and choose **Relink Source (All Instances)…**. Choose the saved `.solidcraft` design
|
||||
Browser and choose **Relink Source (All Instances)…**. Choose the saved `.sc` design
|
||||
to use. Relinking updates that component's stored path and solid bodies for **all its
|
||||
instances**, keeping their identities and placements. Cancelling or choosing a missing,
|
||||
invalid, empty or self-referencing design leaves the assembly unchanged. The operation is
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Format | Read | Write |
|
||||
|---|---|---|
|
||||
| `.solidcraft` (versioned container or JSON design) | yes | yes |
|
||||
| `.sc` (versioned container or JSON design; `.solidcraft` files from earlier releases still open) | yes | yes |
|
||||
| `.json` (self-contained plain JSON design) | yes | yes |
|
||||
| STEP (AP203, AP214, AP242) | solids, assemblies, units, names, colours | AP242 with names, colours and the component tree |
|
||||
| IGES 5.3 | manifold solids, or trimmed surfaces sewn into solids | manifold solids with names and colours |
|
||||
@@ -117,7 +117,7 @@ from a newer version are refused with a message.
|
||||
desktop; **Save As JSON** is also available in the File menu on desktop and web. Choose
|
||||
the `.json` extension for plain JSON. **Export Design as JSON** writes an independent,
|
||||
self-contained copy without changing the current file. Scripts can explicitly pass
|
||||
`"format": "json"` to `file.save` or `file.save_as` even with a `.solidcraft` name.
|
||||
`"format": "json"` to `file.save` or `file.save_as` even with a `.sc` name.
|
||||
Opening detects the format from the contents, so existing JSON designs keep working.
|
||||
Containers favor fast reading and saving by default. Scripts can request
|
||||
`"compression": "compact"` when file size matters more than CPU time.
|
||||
@@ -232,7 +232,7 @@ over a file that changed on disk fails with an error until it is given `overwrit
|
||||
## Saving in the web app
|
||||
|
||||
In Chrome, Edge and Opera, **Save As** opens your browser's save dialog and writes the
|
||||
`.solidcraft` file to the folder you choose, and **Open** opens the browser's open dialog.
|
||||
`.sc` file to the folder you choose, and **Open** opens the browser's open dialog.
|
||||
**Save** then writes back to that same file. The browser remembers the file, so after you
|
||||
reload the page the first **Save** asks once for permission to write to it, and carries on.
|
||||
A design you open from your computer saves back to the file you opened.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Getting started
|
||||
|
||||
The desktop executable is `solidcraft`, the CLI is `solidcraft-cli`, and designs are saved as
|
||||
`.solidcraft`. Designs, settings and setups from builds released under the previous product name keep
|
||||
`.sc` (`.solidcraft` designs from earlier releases still open, and Save keeps their name). Designs, settings and setups from builds released under the previous product name keep
|
||||
working (see the [compatibility notes](https://github.com/bherbruck/solidcraft/blob/main/docs/compatibility.md)).
|
||||
|
||||
## Desktop
|
||||
@@ -17,7 +17,7 @@ cargo run --release -p solidcraft -- --sample # opens with a sample part
|
||||
```
|
||||
|
||||
`solidcraft part.step` opens a STEP, IGES, 3MF, STL or OBJ file as a new design, and
|
||||
`solidcraft design.solidcraft` opens a saved design. Windows builds cross-compile from Linux with
|
||||
`solidcraft design.sc` opens a saved design. Windows builds cross-compile from Linux with
|
||||
`cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft`.
|
||||
|
||||
### macOS
|
||||
@@ -134,19 +134,19 @@ no longer matches the build the browser would keep. Debug builds (`trunk serve`)
|
||||
|
||||
### Design links and embeds
|
||||
|
||||
Use `?open=<encoded HTTPS URL>` to open a hosted `.solidcraft` design as an unsaved
|
||||
Use `?open=<encoded HTTPS URL>` to open a hosted `.sc` design as an unsaved
|
||||
copy, including its history, constraints, and joints. Encode the URL with
|
||||
`encodeURIComponent` when it contains query parameters. The host must allow CORS
|
||||
requests from the app's origin. Downloads, including redirected URLs, must use HTTPS
|
||||
or stay on the app's own origin, and fit within the File > Open design size limit.
|
||||
Failed downloads and invalid designs show an error and leave the empty design.
|
||||
|
||||
For self-hosting, `?open=/dutchman/jobs/abc/part.solidcraft` resolves against the
|
||||
For self-hosting, `?open=/dutchman/jobs/abc/part.sc` resolves against the
|
||||
page's origin, using its scheme and port, including plain HTTP on a LAN. A full URL
|
||||
on that same origin is also accepted; other absolute URLs still require HTTPS.
|
||||
For example, expose a reverse-proxy service in Compose, routing `/` to
|
||||
`solidcraft:8080` and `/dutchman/` to a converter service. The converter can link to
|
||||
`/?open=/dutchman/jobs/abc/part.solidcraft` without a separate origin or CORS setup.
|
||||
`/?open=/dutchman/jobs/abc/part.sc` without a separate origin or CORS setup.
|
||||
Paths must start with one `/`; protocol-relative paths (`//`), backslashes, control
|
||||
characters, and decoded `..` segments are refused. Redirects must end at HTTPS or
|
||||
the page's own origin. Embed mode and **Open in SolidCraft** preserve the path.
|
||||
@@ -155,7 +155,7 @@ Add `embed` to show the viewport, timeline, browser tree, and joint controls wit
|
||||
the ribbon, file menus, or start page:
|
||||
|
||||
```html
|
||||
<iframe src="https://docs.solidcraft.io/app/?embed&open=https://example.org/arm.solidcraft"
|
||||
<iframe src="https://docs.solidcraft.io/app/?embed&open=https://example.org/arm.sc"
|
||||
width="960" height="540" title="Arm design"></iframe>
|
||||
```
|
||||
|
||||
@@ -249,7 +249,7 @@ cargo run --release -p solidcraft-cli -- run examples/bracket.json --out bracket
|
||||
cargo run --release -p solidcraft-cli -- eval examples/bracket.json # volume, area, faces…
|
||||
cargo run --release -p solidcraft-cli -- snapshot examples/bracket.json --out bracket.png
|
||||
cargo run --release -p solidcraft-cli -- exec solid.box '{"length": 20, "width": 10, "height": 5}'
|
||||
cargo run --release -p solidcraft-cli -- run mechanism.json --in arm.solidcraft # with a motion.export_animation step: a GIF
|
||||
cargo run --release -p solidcraft-cli -- run mechanism.json --in arm.sc # with a motion.export_animation step: a GIF
|
||||
```
|
||||
|
||||
A script is a list of commands:
|
||||
@@ -269,8 +269,8 @@ A script is a list of commands:
|
||||
`snapshot` looks at the model from the iso view. `--view NAME` takes another standard view (`front`, `back`, `top`, `bottom`, `left`, `right`) or the name of a view saved in the design with `view.save`. `--camera JSON` takes any camera, as the MCP `screenshot` tool does (its conventions are in [the MCP page](mcp.md#camera-and-close-up-recipe)). A script that applies a view itself with `view.apply` sets the camera too. The ground grid lies on Z 0, so it cuts through a part that straddles it: `--no-grid` leaves it out of the picture, and `--no-axes` the origin's axes. The picture is drawn on the GPU with the viewport's own pipelines, so it shows what the window would (a surface analysis such as `inspect.zebra` included); a machine without a GPU uses a software one (Mesa's lavapipe on Linux, WARP on Windows), and with neither (a Mac without usable Metal, a Linux box without Mesa's Vulkan) the CPU rasterizer draws it, without surface analysis colours, and the reply's `renderer` says `cpu` with a warning. `SOLIDCRAFT_GPU=auto|hardware|software|cpu` chooses (`hardware` and `software` fail rather than fall to the CPU), and `solidcraft-cli gpu` lists the adapters and the tier a snapshot would use.
|
||||
|
||||
```sh
|
||||
solidcraft-cli snapshot part.solidcraft --out photo.png --view Photo
|
||||
solidcraft-cli snapshot part.solidcraft --out close-up.png --no-grid --no-axes --camera '{"target": [30, -20, 0], "yaw": 0.6, "pitch": 0.25, "distance": 60}'
|
||||
solidcraft-cli snapshot part.sc --out photo.png --view Photo
|
||||
solidcraft-cli snapshot part.sc --out close-up.png --no-grid --no-axes --camera '{"target": [30, -20, 0], "yaw": 0.6, "pitch": 0.25, "distance": 60}'
|
||||
```
|
||||
|
||||
### Seeing what a cut or a join is about to do
|
||||
@@ -291,9 +291,9 @@ the other side, `XY`, `XZ` or `YZ` for a plane through the origin, or the comman
|
||||
|
||||
```sh
|
||||
# The pin the cut would remove, inside the plate it stops 6 mm short of.
|
||||
solidcraft-cli snapshot plate.solidcraft --out preview.png --tool Pin
|
||||
solidcraft-cli snapshot plate.solidcraft --out cut.png --tool Pin --section z=12
|
||||
solidcraft-cli snapshot plate.solidcraft --out glass.png --body "Plate:opacity=0.25" --body "Pin:color=#1a7f4b"
|
||||
solidcraft-cli snapshot plate.sc --out preview.png --tool Pin
|
||||
solidcraft-cli snapshot plate.sc --out cut.png --tool Pin --section z=12
|
||||
solidcraft-cli snapshot plate.sc --out glass.png --body "Plate:opacity=0.25" --body "Pin:color=#1a7f4b"
|
||||
```
|
||||
|
||||
A body drawn see-through is drawn see-through everywhere a picture is drawn, not only here:
|
||||
@@ -335,7 +335,7 @@ A design's history turns into a script that rebuilds it, which makes a bug that
|
||||
on one design a one-file reproduction:
|
||||
|
||||
```sh
|
||||
cargo run --release -p solidcraft-cli -- script part.solidcraft --out repro.json
|
||||
cargo run --release -p solidcraft-cli -- script part.sc --out repro.json
|
||||
cargo run --release -p solidcraft-cli -- eval repro.json # the same part
|
||||
```
|
||||
|
||||
@@ -354,7 +354,7 @@ and Z is up.
|
||||
## Name and compatibility
|
||||
|
||||
SolidCraft was renamed from an earlier working name. Since 0.5.0 everything uses the new
|
||||
name: the `solidcraft` and `solidcraft-cli` programs, `.solidcraft` designs, `SOLIDCRAFT_*`
|
||||
name: the `solidcraft` and `solidcraft-cli` programs, `.solidcraft` designs (`.sc` since then), `SOLIDCRAFT_*`
|
||||
environment variables, settings folders and the `solidcraft` MCP server. Upgrading keeps
|
||||
working: designs, settings, recovery files, environment variables and CLI/MCP setups from
|
||||
earlier builds carry over. Every compatibility shim is listed in the
|
||||
|
||||
@@ -24,15 +24,15 @@ uses only the software renderer and keeps your settings.
|
||||
## A design won't open or hangs while opening
|
||||
|
||||
- **Open the previous save.** Every save keeps the version it replaced next to it as
|
||||
`<file>.bak` (see [Files](files.md)). Copy it to a new name ending in `.solidcraft`, for
|
||||
example `bracket-previous.solidcraft`, and open that. Work saved after the previous save is
|
||||
`<file>.bak` (see [Files](files.md)). Copy it to a new name ending in `.sc`, for
|
||||
example `bracket-previous.sc`, and open that. Work saved after the previous save is
|
||||
not in it.
|
||||
- **Check the crash reports.** If SolidCraft crashed while the design was open, the newest report
|
||||
in the `crashes` folder (see the table below) usually holds a `design.solidcraft` with the
|
||||
in the `crashes` folder (see the table below) usually holds a `design.sc` with the
|
||||
design as it was. `solidcraft-cli crashes` lists the reports.
|
||||
- **Unsaved designs** are in the `recovery` folder, not next to the file.
|
||||
- **Report it.** GitHub doesn't accept `.solidcraft` attachments, so zip the file
|
||||
(`design.solidcraft.zip`) and attach it to an issue, with the crash report if there is one.
|
||||
- **Report it.** GitHub doesn't accept `.sc` (or `.solidcraft`) attachments, so zip the file
|
||||
(`design.sc.zip`) and attach it to an issue, with the crash report if there is one.
|
||||
|
||||
## Reset the settings
|
||||
|
||||
@@ -54,7 +54,7 @@ folder. Releases before the rename used `SolveCraft` folders next to these; Soli
|
||||
reads them, so you can leave them alone.
|
||||
|
||||
- The **`recovery`** folder holds autosaves of designs that were never saved. Rename the data
|
||||
folder rather than deleting it, so you can still open those autosaves (`.solidcraft` files)
|
||||
folder rather than deleting it, so you can still open those autosaves (`.sc` files, or `.solidcraft` from earlier releases)
|
||||
with File > Open.
|
||||
- The **`crashes`** folder holds crash reports. Please attach the newest one to a bug report.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ and a plastic-enclosure subset work; surfaces, drawings, CAM and simulation are
|
||||
the [feature list](dev/features.md) lists every command by area, and the
|
||||
[reference tests](dev/oracle.md) check modelling results against analytic and FreeCAD values.
|
||||
|
||||
Existing `.solidcraft` designs and `solidcraft-cli` commands keep their names and behavior.
|
||||
New designs are saved as `.sc`. Existing `.solidcraft` designs still open, and Save keeps their name; `solidcraft-cli` commands keep their names and behavior.
|
||||
|
||||
This book has two halves: the **user guide** for people modelling with SolidCraft, and the
|
||||
**developer guide** for people working on it. The [command reference](reference/commands.md) is
|
||||
|
||||
@@ -149,7 +149,7 @@ A design is saved as a *design file*.
|
||||
|
||||
### design file
|
||||
|
||||
A file in which SolidCraft saves one design, with the `.solidcraft` extension.
|
||||
A file in which SolidCraft saves one design, with the `.sc` extension (`.solidcraft` in earlier releases; those files still open).
|
||||
|
||||
- **In SolidCraft:** `file.save`, `file.save_as`, `doc.open`; the `solidcraft/N` format tag.
|
||||
- **Don't use:** document, project file.
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ cargo run --release -p solidcraft -- --sample # desktop app with a samp
|
||||
cargo run --release -p solidcraft-cli -- commands # the command registry
|
||||
cargo run --release -p solidcraft-cli -- run examples/bracket.json --out bracket.step
|
||||
cargo run --release -p solidcraft-cli -- snapshot examples/bracket.json --out bracket.png
|
||||
cargo run --release -p solidcraft-cli -- snapshot part.solidcraft --out preview.png --tool Pin --section z=12
|
||||
cargo run --release -p solidcraft-cli -- snapshot part.sc --out preview.png --tool Pin --section z=12
|
||||
cargo run --release -p solidcraft-cli -- render examples/render-studio.json --out product.png
|
||||
cargo run --release -p solidcraft -- part.step # open a STEP file as a new design
|
||||
cargo run --release -p solidcraft-cli -- eval part.step # measure an imported STEP file
|
||||
|
||||
@@ -54,7 +54,7 @@ and help is welcome: see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
retained shallow prismatic pockets, exact line/arc/circle DXF outlines and bend tables;
|
||||
plastic enclosure features and rules. Pocket depth and side survive unfold/refold and appear
|
||||
separately from through-hole outlines in the flat preview and DXF.
|
||||
- **Files**: design containers (`.solidcraft`) with lossless binary mesh payloads;
|
||||
- **Files**: design containers (`.sc`; `.solidcraft` still opens) with lossless binary mesh payloads;
|
||||
plain JSON remains supported for opening, Save As and export. Desktop Open can show a saved
|
||||
provisional picture from warm geometry caches while rebuilding before editing; caches never
|
||||
replace the parametric design. Export to STEP (AP242 with body names, colours and
|
||||
|
||||
@@ -15,10 +15,10 @@ From the repository root, build the CLI and generate the editable design:
|
||||
|
||||
```sh
|
||||
cargo build --release -p solidcraft-cli
|
||||
target/release/solidcraft-cli run examples/chebyshev-motion-trace.json --out target/chebyshev.solidcraft
|
||||
target/release/solidcraft-cli run examples/chebyshev-motion-trace.json --out target/chebyshev.sc
|
||||
```
|
||||
|
||||
On Windows the executable has the `.exe` extension. Open `target/chebyshev.solidcraft`
|
||||
On Windows the executable has the `.exe` extension. Open `target/chebyshev.sc`
|
||||
in SolidCraft. Open **Motion Study**, choose **Chebyshev cycle** and play it. The
|
||||
red tip follows the retained **Chebyshev trajectory** sketch while the blue crank
|
||||
turns through 360°. Playback reuses the trace rather than sampling it again.
|
||||
@@ -44,7 +44,7 @@ For a repeatable CLI comparison, save this as `target/change-chebyshev.json`:
|
||||
```
|
||||
|
||||
```sh
|
||||
target/release/solidcraft-cli run target/change-chebyshev.json --in target/chebyshev.solidcraft --out target/chebyshev-length55.solidcraft --timings
|
||||
target/release/solidcraft-cli run target/change-chebyshev.json --in target/chebyshev.sc --out target/chebyshev-length55.sc --timings
|
||||
```
|
||||
|
||||
Use `trace_sweep` and `180 deg` in that command to demonstrate the driving-range
|
||||
@@ -84,14 +84,14 @@ design. This uses SolidCraft's renderer and motion driver to export a looping GI
|
||||
```
|
||||
|
||||
```sh
|
||||
target/release/solidcraft-cli run target/animate-chebyshev.json --in target/chebyshev.solidcraft
|
||||
target/release/solidcraft-cli run target/animate-chebyshev.json --in target/chebyshev.sc
|
||||
```
|
||||
|
||||
For comparable still images, use the same camera before and after the edit:
|
||||
|
||||
```sh
|
||||
target/release/solidcraft-cli snapshot --in target/chebyshev.solidcraft --out target/chebyshev-before.png --width 900 --height 650 --camera '{"target":[25,40,5],"yaw":0,"pitch":1.25,"distance":215,"fov":0}' --no-grid --no-axes
|
||||
target/release/solidcraft-cli snapshot --in target/chebyshev-length55.solidcraft --out target/chebyshev-after.png --width 900 --height 650 --camera '{"target":[25,40,5],"yaw":0,"pitch":1.25,"distance":215,"fov":0}' --no-grid --no-axes
|
||||
target/release/solidcraft-cli snapshot --in target/chebyshev.sc --out target/chebyshev-before.png --width 900 --height 650 --camera '{"target":[25,40,5],"yaw":0,"pitch":1.25,"distance":215,"fov":0}' --no-grid --no-axes
|
||||
target/release/solidcraft-cli snapshot --in target/chebyshev-length55.sc --out target/chebyshev-after.png --width 900 --height 650 --camera '{"target":[25,40,5],"yaw":0,"pitch":1.25,"distance":215,"fov":0}' --no-grid --no-axes
|
||||
```
|
||||
|
||||
`--timings` reports command execution separately from process startup and file
|
||||
|
||||
+32
-16
@@ -3,33 +3,49 @@
|
||||
SolidCraft was first called SolveCraft, and builds up to 0.4 kept that name in their
|
||||
identifiers after the displayed name changed. 0.5.0 renamed the binaries, release assets and
|
||||
packages; the release after it renames everything else: crates `solidcraft-*`, binaries `solidcraft` and `solidcraft-cli`, `.solidcraft`
|
||||
designs, the `"format": "solidcraft/N"` tag, `SOLIDCRAFT_*` environment variables, settings
|
||||
designs (saved as `.sc` since; see section 1), the `"format": "solidcraft/N"` tag, `SOLIDCRAFT_*` environment variables, settings
|
||||
and recovery folders, the MCP server name, docs, CI and packaging.
|
||||
|
||||
The old name survives **only** in the shims below (plus their tests, this file, and changelog /
|
||||
release notes). `git grep -i solvecraft` must list nothing else. When you add or remove a
|
||||
shim, update this file in the same PR.
|
||||
|
||||
## 1. `.solvecraft` design files
|
||||
## 1. `.solvecraft` and `.solidcraft` design files
|
||||
|
||||
New designs are saved as `.solidcraft`. Files with the old extension still open everywhere:
|
||||
New designs are saved as `.sc`. Files with either older extension still open everywhere: `.solvecraft`
|
||||
(saved before the rename) and `.solidcraft` (saved before designs moved to `.sc`). One constant
|
||||
names the extension new designs get, `DESIGN_EXT` (`sc`), and one list names every extension a
|
||||
design may have, `DESIGN_EXTS` (`sc`, then `LEGACY_DESIGN_EXTS`: `solidcraft`, `solvecraft`);
|
||||
code uses those, never its own string.
|
||||
|
||||
| Path in | Where |
|
||||
|---|---|
|
||||
| Extension → design format | `crates/io/src/lib.rs` (`LEGACY_DESIGN_EXT`, format detection) |
|
||||
| Open dialog filters (desktop) | `apps/solidcraft/src/main.rs` (`services`) |
|
||||
| Drag-drop, recent files, `doc.open`, MCP `open`, CLI `eval`/`run` | go through the same format detection in `solidcraft_io` |
|
||||
| Browser file picker | `apps/solidcraft-web/src/files.rs` (`accept`) |
|
||||
| Windows | `assets/platforms/windows/solidcraft.wxs`: the `.solvecraft` extension on the design ProgId and in `SupportedTypes` |
|
||||
| macOS | `assets/platforms/macos/Info.plist.in`: `solvecraft` in the design document type's extensions |
|
||||
| Linux / FreeBSD / Flatpak | `assets/platforms/linux/io.tangencylabs.solidcraft.mime.xml`: `*.solvecraft` glob and the `application/x-solvecraft` alias |
|
||||
| Extension → design format | `crates/io/src/lib.rs` (`DESIGN_EXTS`, `is_design_ext`, `is_design_path`, format detection) |
|
||||
| Open dialog filters (desktop and browser) | `crates/io/src/formats.rs` (the design row lists every entry of `DESIGN_EXTS`), shown by `apps/solidcraft/src/main.rs` (`services`) |
|
||||
| Drag-drop, recent files, `doc.open`, MCP `open`, CLI `eval`/`run`, Insert/Link/Relink design | go through the same format detection in `solidcraft_io` (`is_design_container_path` for links) |
|
||||
| Browser file picker | `apps/solidcraft-web/src/disk.rs` (`open_options`, `opens_design`) |
|
||||
| Windows | `assets/platforms/windows/solidcraft.wxs`: `.sc` is the design ProgId's extension; `.solidcraft` and `.solvecraft` point at the same ProgId and are in `SupportedTypes` |
|
||||
| macOS | `assets/platforms/macos/Info.plist.in`: `sc`, `solidcraft` and `solvecraft` in the design document type and its `io.tangencylabs.solidcraft.design` UTI |
|
||||
| Linux / FreeBSD / Flatpak / Snap | `assets/platforms/linux/io.tangencylabs.solidcraft.mime.xml`: `*.sc`, `*.solidcraft` and `*.solvecraft` globs under `application/x-solidcraft` (plus the `application/x-solvecraft` alias); the `.desktop` file names that type |
|
||||
|
||||
Recent-files entries that point at a `.solvecraft` file keep working: they are plain paths and
|
||||
open through the same detection. The browser app also lists designs it stored under the old
|
||||
extension, and keyboard-shortcut files exported as `solvecraft-shortcuts/1` still import
|
||||
(`crates/ui-egui/src/toolbar_custom.rs`). Tests: `crates/io` (`Format::from_name`,
|
||||
`is_design_path`), `legacy_extension_and_format_tag_still_open` (CLI, `run_results.rs`) and
|
||||
`legacy_design_files_and_resource_uris` (MCP `open`).
|
||||
Opening a `.solidcraft` file and pressing **Save** writes that same file: nothing is renamed behind
|
||||
the user's back. **Save As** (and every new design, autosave, crash report and component
|
||||
extracted to its own design) offers `.sc`, and leaves the old file as it was.
|
||||
|
||||
Recent-files entries that point at an older file keep working: they are plain paths and
|
||||
open through the same detection. Autosaves and crash reports that earlier releases wrote
|
||||
(`<id>.solidcraft`, `<id>.solvecraft`, `design.solidcraft`) are still listed and recovered
|
||||
(`solidcraft_engine::recovery`, `solidcraft_engine::crash::read`), and the browser app still
|
||||
recovers `recovery.solidcraft` / `recovery.solvecraft` from its storage. The browser app also
|
||||
lists designs it stored under the old extensions, and keyboard-shortcut files exported as
|
||||
`solvecraft-shortcuts/1` still import (`crates/ui-egui/src/toolbar_custom.rs`). The designs in
|
||||
`apps/solidcraft-cli/tests/corpus` keep their `.solidcraft` names on purpose: they are files saved
|
||||
by earlier releases. Tests: `crates/io` (`Format::from_name`, `is_design_path`, the import filter
|
||||
groups), `legacy_extension_and_format_tag_still_open` (CLI, `run_results.rs`),
|
||||
`legacy_design_files_and_resource_uris` (MCP `open` and `save`),
|
||||
`saving_an_opened_legacy_design_keeps_its_name` (engine),
|
||||
`autosave_from_a_legacy_folder_is_listed_and_loads`, `autosaves_are_written_as_sc` and
|
||||
`a_report_from_an_earlier_release_still_finds_its_design`.
|
||||
|
||||
## 2. The `solvecraft/N` format tag
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ All of these read the same folder:
|
||||
|
||||
**Reproduce:** `solidcraft-cli crashes replay <id>` opens the report's design, runs its
|
||||
commands, and reports whether the same internal error happens again (same place and message).
|
||||
Without the CLI, run `solidcraft-cli run <report>/replay.json --in <report>/design.solidcraft`.
|
||||
Without the CLI, run `solidcraft-cli run <report>/replay.json --in <report>/design.sc`.
|
||||
A fixed crash turns into a regression test with the same two files.
|
||||
|
||||
**Check that reporting works:** with `SOLIDCRAFT_CRASH_TEST=1` set for the app, the command
|
||||
@@ -87,7 +87,7 @@ A folder `crashes/<YYYYMMDD-HHMMSS>-<signature>/`:
|
||||
The process still ends with the signal, so macOS also writes its own crash report, with every
|
||||
thread's stack: `~/Library/Logs/DiagnosticReports/solidcraft-<date>.ips` (Console ▸ Crash
|
||||
Reports), whose `pid` is the one in `native`.
|
||||
- `design.solidcraft`: the design the replay starts from
|
||||
- `design.sc`: the design the replay starts from
|
||||
- `replay.json`: `{"commands": [{command, params}, …]}`
|
||||
- `minidump.dmp` (Windows, native crashes; on macOS, see the system's own report above): the threads' stacks and the loaded modules at the
|
||||
crash, for WinDbg or Visual Studio. It holds stack memory only, not the design, but it stays
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Design containers, version 1
|
||||
|
||||
New designs use `.solidcraft`; `.solvecraft` remains a supported legacy extension. Opening detects the ZIP
|
||||
New designs use `.sc`; `.solidcraft` and `.solvecraft` remain supported legacy extensions. Opening detects the ZIP
|
||||
local-header signature (`50 4b 03 04`) independently of the name; otherwise it uses
|
||||
the existing JSON design reader and migrations. Plain JSON remains a fully supported,
|
||||
self-contained input/output format. `io::write_design` keeps its JSON contract;
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ document remains the reference implementation today.
|
||||
solidcraft-cli mcp
|
||||
|
||||
# Headless, starting from a design file or a command script.
|
||||
solidcraft-cli mcp --in part.solidcraft
|
||||
solidcraft-cli mcp --in part.sc
|
||||
|
||||
# Bridged to the running app, so you can watch the agent model. The app's control channel is
|
||||
# on by default (see below), so no flag is needed on either side:
|
||||
@@ -274,7 +274,7 @@ The `skill_reference` tool returns these documents from the copies built into So
|
||||
| `skill_reference` | `skill`, `file?` (default `reference.md`) | Read a bundled skill document. Skills: `solidcraft-modeling`, `solidcraft-mechanisms`, `solidcraft-editing`, `solidcraft-checking`. |
|
||||
| `running` | - | What the app is running or has queued, for every client (`control.running`, see [control-protocol.md](control-protocol.md#limits)): each request's connection and id, method, command id or a batch's step count, `running_s` and `queued`, never its parameters. Answers at once while the app is busy: ask it when a call timed out before sending a change again. Headless: always `{"running": []}`. |
|
||||
| `new_design` | `name?` | Start an empty design. |
|
||||
| `open` | `path` | Open a `.solidcraft` design, a STEP file (`.step`/`.stp`) as a new design holding its bodies (an Import base feature), or a 3MF/STL file as mesh bodies. |
|
||||
| `open` | `path` | Open a `.sc` design (or a `.solidcraft` one from an earlier release), a STEP file (`.step`/`.stp`) as a new design holding its bodies (an Import base feature), or a 3MF/STL file as mesh bodies. |
|
||||
| `save` | `path?` | Save the design (to its current file without `path`). |
|
||||
|
||||
`measure` with `items` reports each item under `items` (`length_mm`, `area_mm2` or `position`).
|
||||
|
||||
+3
-3
@@ -494,8 +494,8 @@ straight-line projection and exact surface-refinement reuse changes:
|
||||
|
||||
These are geometry rebuild times, not complete desktop opening times. The public fixture
|
||||
can be generated with `solidcraft-cli run examples/acceptance/sensor_enclosure.json
|
||||
--save sensor.solidcraft --quiet`, then measured with `solidcraft-cli bench-open
|
||||
sensor.solidcraft`. Each benchmark starts a new process; this does not measure a warm
|
||||
--save sensor.sc --quiet`, then measured with `solidcraft-cli bench-open
|
||||
sensor.sc`. Each benchmark starts a new process; this does not measure a warm
|
||||
timeline-cache hit. The local assembly is private and is not part of the repository.
|
||||
|
||||
Straight segments now use guarded algebraic closest-point projection instead of sampling
|
||||
@@ -536,7 +536,7 @@ ready: 13.04 s geometry, 2.20 s placement, 0.53 s meshing and 0.07 s thumbnails.
|
||||
the optimized `ci` profile and actual Vulkan rendering, with isolated settings and caches.
|
||||
Linux and Windows timings are separate measurements and must not be compared as a speedup.
|
||||
|
||||
`solidcraft-cli bench-open design.solidcraft` reports JSON lines for file reading,
|
||||
`solidcraft-cli bench-open design.sc` reports JSON lines for file reading,
|
||||
JSON decoding and migrations (including input-name repair), timeline evaluation, individual
|
||||
features, and world placement/display meshing. `--parse-only` stops before geometry evaluation.
|
||||
The parse timer includes document validation and repair, not just the JSON parser.
|
||||
|
||||
@@ -12,7 +12,7 @@ At most 32 disjoint dielectric bodies can be selected. Closed-medium topology, o
|
||||
|
||||
## Reusable headless studio
|
||||
|
||||
`solidcraft-cli mcp --in model.solidcraft --render-gpu --allow-path OUTPUT_DIRECTORY` explicitly installs the native path-tracing host once. Subsequent `render.scene`, `render.preflight`, `render.start`, `render.status` and `render.export` commands reuse the same evaluated native design and device. Without `--render-gpu`, MCP does not discover a path-tracing GPU. The flag is rejected with `--connect`, because a connected application owns its device. `SOLIDCRAFT_GPU` is respected; hardware-only workflows use `hardware` and never substitute software.
|
||||
`solidcraft-cli mcp --in model.sc --render-gpu --allow-path OUTPUT_DIRECTORY` explicitly installs the native path-tracing host once. Subsequent `render.scene`, `render.preflight`, `render.start`, `render.status` and `render.export` commands reuse the same evaluated native design and device. Without `--render-gpu`, MCP does not discover a path-tracing GPU. The flag is rejected with `--connect`, because a connected application owns its device. `SOLIDCRAFT_GPU` is respected; hardware-only workflows use `hardware` and never substitute software.
|
||||
|
||||
`tools/render-library/selene_studio.py` is a development-only orchestration helper for the pinned functional master. Start it with `python -i`, an explicitly selected freshly built CLI, and a new output directory. Review `studio.scene`, submit an explicit request with `studio.render(label, params)`, and call `studio.close()`. The helper records the unmodified master hash, executable hash, exact request, preflight, job identity, exported PNG hash and completion state. Numerical completion is not visual approval.
|
||||
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ German and Korean; command IDs and scenario handles remain unchanged.
|
||||
|
||||
```sh
|
||||
solidcraft-cli render examples/render-studio.json --out product.png --width 800 --height 600
|
||||
solidcraft-cli render design.solidcraft --setup Studio --out product.png --transparent
|
||||
solidcraft-cli render design.sc --setup Studio --out product.png --transparent
|
||||
```
|
||||
|
||||
The CLI follows preflight/start/native wait/export, with a default 120-second wait
|
||||
|
||||
@@ -85,8 +85,8 @@ assigns generic physical materials and applies these finishes through the same
|
||||
commands used by the editor:
|
||||
|
||||
```sh
|
||||
solidcraft-cli run examples/material-finishes.json --save target/material-finishes.solidcraft
|
||||
solidcraft-cli render target/material-finishes.solidcraft --out target/material-finishes.png --width 1600 --height 1000
|
||||
solidcraft-cli run examples/material-finishes.json --save target/material-finishes.sc
|
||||
solidcraft-cli render target/material-finishes.sc --out target/material-finishes.png --width 1600 --height 1000
|
||||
```
|
||||
|
||||
## Actual consumers and color
|
||||
|
||||
@@ -44,7 +44,7 @@ FF=$!
|
||||
# Waiting lets ffmpeg write the MP4 index so the failed take remains inspectable.
|
||||
trap 'kill -INT $FF 2>/dev/null || true; wait $FF 2>/dev/null || true' EXIT
|
||||
sleep 1
|
||||
python3 "$HERE/enclosure_ui_mcp.py" --port "$PORT" --cli "$CLI" --story "$STORY" --speed "$PAUSE" --minimum-caption "$CAPTION" --log "$OUT/steps.log" --save "$OUT/enclosure-$STORY.solidcraft"
|
||||
python3 "$HERE/enclosure_ui_mcp.py" --port "$PORT" --cli "$CLI" --story "$STORY" --speed "$PAUSE" --minimum-caption "$CAPTION" --log "$OUT/steps.log" --save "$OUT/enclosure-$STORY.sc"
|
||||
sleep "$HOLD"
|
||||
kill -INT $FF; wait $FF || true
|
||||
START=$(grep -o "start: [0-9.]*" "$OUT/ffmpeg.log" | head -1 | cut -d" " -f2)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Replay a design's timeline in a running SolidCraft, for a screen recording (record_rover.sh).
|
||||
|
||||
solidcraft --control 7878 &
|
||||
python3 examples/demo/rover_replay.py --port 7878 Rover-recorded.solidcraft
|
||||
python3 examples/demo/rover_replay.py --port 7878 Rover-recorded.sc
|
||||
|
||||
Opens the design, frames the finished model, rolls the timeline back to its start and then
|
||||
forward in even steps (the History bar's roll-to, as `timeline.roll_to` over the control
|
||||
|
||||
@@ -93,8 +93,8 @@ opposite edges, one connected triangle shell and positive signed volume.
|
||||
Example replay (outputs belong outside the repository):
|
||||
|
||||
```text
|
||||
solidcraft-cli run examples/studio/coffee-exploded.json --save coffee.solidcraft
|
||||
solidcraft-cli eval coffee.solidcraft --json-out coffee-measurements.json
|
||||
solidcraft-cli run examples/studio/coffee-exploded.json --save coffee.sc
|
||||
solidcraft-cli eval coffee.sc --json-out coffee-measurements.json
|
||||
```
|
||||
|
||||
Use the evidence runner with the paired render-request JSON to capture runtime
|
||||
|
||||
@@ -30,7 +30,7 @@ const NOT_A_BUG = /^\s*\[(feat|feature|change|enhancement)|feature request|sugge
|
||||
const APP_TITLE = /^([A-Z][A-Z ]+|Modeling|Part|Sketch|Surface|Sheet Metal|Plastic|Mesh|Render):\s*\S/;
|
||||
// A report this long describes what it saw; only short ones are asked.
|
||||
const SHORT = 300;
|
||||
const ATTACHMENT = /user-attachments\/|\.solidcraft\b|repro\.json|\.(step|stp|3mf|stl)\b/i;
|
||||
const ATTACHMENT = /user-attachments\/|\.(sc|solidcraft)\b|repro\.json|\.(step|stp|3mf|stl)\b/i;
|
||||
const COMMAND = /["`][a-z_]+\.[a-z_][a-z_.]*["`]/;
|
||||
const VERSION = /\b\d+\.\d+\.\d+\b|\b[0-9a-f]{7,40}\b/i;
|
||||
|
||||
@@ -61,7 +61,7 @@ function askBody(asks) {
|
||||
const lines = ['Thanks for the report! To reproduce it we need a little more:'];
|
||||
lines.push(
|
||||
'- **Steps to reproduce**: what you clicked or typed, in order. For a problem on a particular design, ' +
|
||||
'`solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script; attach that, or the design.',
|
||||
'`solidcraft-cli script design.sc --out repro.json` turns its history into a script; attach that, or the design.',
|
||||
);
|
||||
if (asks.includes('version')) lines.push('- **SolidCraft version** (Help → About) and your OS.');
|
||||
lines.push(
|
||||
|
||||
@@ -54,6 +54,7 @@ test('missing: empty app reports are asked to paste the copied report', () => {
|
||||
|
||||
test('missing: reproducible, long, app-filed and non-bug issues are left alone', () => {
|
||||
assert.deepEqual(info.missing('Loft fails', '## Reproduction\n1. sketch two circles\n2. loft them'), []);
|
||||
assert.deepEqual(info.missing('Loft fails', 'see design.sc attached'), []);
|
||||
assert.deepEqual(info.missing('Loft fails', 'see design.solidcraft attached'), []);
|
||||
assert.deepEqual(info.missing('Loft fails', 'runs `solid.loft` and fails'), []);
|
||||
assert.deepEqual(info.missing('Loft fails', long), []);
|
||||
|
||||
@@ -12,7 +12,7 @@ description: "Use when running SolidCraft to see a change working, driving it th
|
||||
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}`, then `ui.click {x, y}`
|
||||
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`, then read the PNG
|
||||
- For UI work, look at the result. Without a window use `ui.render` or `solidcraft-cli snapshot`.
|
||||
- Headless: `solidcraft-cli run script.json --out part.step`, `solidcraft-cli eval design.solidcraft`, `solidcraft-cli snapshot script.json --out shot.png`, `solidcraft-cli commands`.
|
||||
- Headless: `solidcraft-cli run script.json --out part.step`, `solidcraft-cli eval design.sc`, `solidcraft-cli snapshot script.json --out shot.png`, `solidcraft-cli commands`.
|
||||
- Build with `CARGO_TARGET_DIR=<repo>/target`.
|
||||
|
||||
## MCP tools
|
||||
|
||||
@@ -20,7 +20,7 @@ People trust SolidCraft with their designs. This outranks feature work.
|
||||
|
||||
## Proving a crash fix
|
||||
- Every crash fix lands with a regression test. `hostile_params_never_panic` in `crates/engine/src/tests.rs` runs every command with junk parameters; a new command gets this for free.
|
||||
- A crash report's `design.solidcraft` and `replay.json` are the test's starting point. Put the test in the area's existing test file (see `skills/solidcraft-tests/SKILL.md`).
|
||||
- A crash report's `design.sc` and `replay.json` are the test's starting point. Put the test in the area's existing test file (see `skills/solidcraft-tests/SKILL.md`).
|
||||
|
||||
## When someone says the app crashed
|
||||
The desktop app writes every panic and every unclean exit to a local crash folder, with the design and the commands that reproduce it (`solidcraft_engine::crash`, `docs/crash-reports.md`). Look there first:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: solidcraft-editing
|
||||
description: "Use when changing, fixing or extending an existing SolidCraft design through its MCP server - a .solidcraft file or the design open in the app. Typical requests: resize it, move a cut, change a hole or standoff, add a fillet or chamfer, make a fixed value parametric, or a feature shows an error after a change or after the app crashed."
|
||||
description: "Use when changing, fixing or extending an existing SolidCraft design through its MCP server - a .sc file (or an older .solidcraft one) or the design open in the app. Typical requests: resize it, move a cut, change a hole or standoff, add a fillet or chamfer, make a fixed value parametric, or a feature shows an error after a change or after the app crashed."
|
||||
---
|
||||
|
||||
# SolidCraft editing
|
||||
@@ -59,7 +59,7 @@ Inspect one feature of each kind, not every copy.
|
||||
- Never retry the crashing command unchanged.
|
||||
|
||||
## Save
|
||||
`save {"path":"...-v2.solidcraft"}` writes a copy. `save {}` overwrites the opened file; do that only when the user asked for it.
|
||||
`save {"path":"...-v2.sc"}` writes a copy. `save {}` overwrites the opened file; do that only when the user asked for it.
|
||||
|
||||
## Red flags
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ The rules that cannot be broken in a PR. `AGENTS.md` lists them in one line each
|
||||
- Before any icon, icon assignment or media edit, read `assets/AGENTS.md`, `assets/icons/ui/AGENTS.md` and `assets/icons/ui/STYLE_GUIDE.md`. Use the canonical bases and named finished references there; never invent a parallel icon style. `cargo xtask icons` validates them.
|
||||
|
||||
## The name
|
||||
- SolidCraft everywhere: crates `solidcraft-*`, binaries `solidcraft` and `solidcraft-cli`, `.solidcraft` designs, the `solidcraft/N` format tag, `SOLIDCRAFT_*` environment variables, settings and recovery folders, the MCP server name, docs, CI and packaging.
|
||||
- SolidCraft everywhere: crates `solidcraft-*`, binaries `solidcraft` and `solidcraft-cli`, `.sc` designs (`.solidcraft` still opens), the `solidcraft/N` format tag, `SOLIDCRAFT_*` environment variables, settings and recovery folders, the MCP server name, docs, CI and packaging.
|
||||
- The old name survives only in the compatibility shims listed in `docs/compatibility.md`. Do not add new uses of it.
|
||||
- Public docs, web and Flatpak deployments use project Pages `/solidcraft/`. Do not add permanent old-name Pages aliases.
|
||||
- Labels, parameter docs, messages and docs use the terms in the glossary (`docs/book/src/reference/glossary.md`): one term for each idea. A change that adds a new idea adds its term there.
|
||||
|
||||
@@ -138,7 +138,7 @@ The same pattern moved the solar wings of the SELENE rover (`Rover-v7.solidcraft
|
||||
| `a joint needs two different occurrences` | Both origins found the same part. Give `occurrence` on both. If the parts are bodies of one component (an imported STEP file often brings them so), run `component.from_bodies {"bodies": [...]}` first. |
|
||||
| "unknown command" for `file.open` | Open a design with the `open` tool or `doc.open {path}`. |
|
||||
| Rigid hoses, cables or springs drift as the mechanism moves | Bodies don't bend. Hide them with `browser.visibility`, or model them per link. |
|
||||
| `solidcraft-cli snapshot SCRIPT --in design` runs on an empty design | `snapshot` ignores `--in`. Run `solidcraft-cli run SCRIPT --in design --save out.solidcraft` first. |
|
||||
| `solidcraft-cli snapshot SCRIPT --in design` runs on an empty design | `snapshot` ignores `--in`. Run `solidcraft-cli run SCRIPT --in design --save out.sc` first. |
|
||||
| A URDF export lacks a joint | `export` to `.urdf` leaves out each joint that closes a loop, with a warning, because URDF is a tree. |
|
||||
|
||||
## Reply cost
|
||||
|
||||
@@ -59,7 +59,7 @@ Work the part out as numbers first. Then build it in a few large `batch` calls,
|
||||
- When the server is bridged, `source` defaults to the whole app window and `view` also turns the user's camera, so pass `"source":"model"` and leave out `view` unless the user wants the camera moved.
|
||||
|
||||
## Finish
|
||||
`export {path}` writes `.step` for CAD, `.3mf`/`.stl` for printing, or `.urdf` (an assembly with joints, for robot simulators). `save {path}` writes `.solidcraft`.
|
||||
`export {path}` writes `.step` for CAD, `.3mf`/`.stl` for printing, or `.urdf` (an assembly with joints, for robot simulators). `save {path}` writes `.sc` (`.solidcraft` files from earlier releases still open).
|
||||
|
||||
## Red flags
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ Written against SolidCraft 0.18.2. Report what you observed, never a guessed cau
|
||||
- **Version and OS.** Help → About in the app, `solidcraft --version` or `solidcraft-cli --version`; the MCP server reports its version in `initialize` (`serverInfo.version`). Note whether the user built from source.
|
||||
- **What the user did**, in order, what they expected and what happened instead.
|
||||
- **The exact error text**, copied, not paraphrased: a failed `execute` or `batch` step's `failed.error`, the timeline feature's `error` in `inspect_design {"measure":false,"sketches":false}`, or the message in the app.
|
||||
- **If the app closed or froze:** `crash_reports {}` first. A matching report has the version, the command that crashed, the commands before it and a replay. `solidcraft-cli crashes replay <id>` checks that it still happens. Its `design.solidcraft` is the user's design: see Privacy.
|
||||
- **If the app closed or froze:** `crash_reports {}` first. A matching report has the version, the command that crashed, the commands before it and a replay. `solidcraft-cli crashes replay <id>` checks that it still happens. Its `design.sc` is the user's design: see Privacy.
|
||||
|
||||
## 2. Reproduce in a fresh design
|
||||
Start from nothing, so the report does not depend on the user's file.
|
||||
- **Headless:** write the steps as a command script `{"commands":[...]}` and run `solidcraft-cli run repro.json`. A failing feature fails the run and names the feature. This never touches the design open in the app.
|
||||
- **Over MCP:** `new_design {}`, then the steps in one `batch {"commands":[...],"rollback":false}`. With `rollback: false` the steps before the failure stay, so you can inspect the state it failed in. When the server is bridged to the app and the open design is `dirty`, ask before `new_design`.
|
||||
- Find commands and parameters with `list_commands {"filter":"fillet"}` and `describe_command {"id":"solid.fillet"}`, not the unfiltered catalog.
|
||||
- **Only in the user's design:** `solidcraft-cli script design.solidcraft --out repro.json` writes its history as a command script. `solidcraft-cli eval repro.json` replays it and prints the measurements; `solidcraft-cli run repro.json` stops at the first failing feature. If the script rebuilds without the problem, say so in the report: it then depends on something the history does not hold (an import, a file the design links to).
|
||||
- **Only in the user's design:** `solidcraft-cli script design.sc --out repro.json` writes its history as a command script. `solidcraft-cli eval repro.json` replays it and prints the measurements; `solidcraft-cli run repro.json` stops at the first failing feature. If the script rebuilds without the problem, say so in the report: it then depends on something the history does not hold (an import, a file the design links to).
|
||||
- **Interactive only** (a dialog, a pick, a drag, the viewport): write the clicks as numbered app steps. A script cannot show those.
|
||||
- Check that it fails the same way twice. A problem that comes and goes is still worth reporting; say how often it happened.
|
||||
|
||||
@@ -40,9 +40,9 @@ Shrink while the same failure, with the same message, still happens. Re-run afte
|
||||
- Re-run the final minimal script from scratch once more. Its output goes into the report as it is.
|
||||
|
||||
## 5. Privacy
|
||||
- Never attach, upload or quote the user's design, a crash report's `design.solidcraft`, or file contents without asking. The minimal synthetic script is the default attachment.
|
||||
- GitHub does not accept `.solidcraft` attachments. A design the user agrees to share goes up as a `.zip`: a `.solidcraft` file is a ZIP already, so copy or rename it to `name.solidcraft.zip`, or zip it. SolidCraft opens either kind of `.zip`.
|
||||
- Strip home folders, user names, part and project names and file paths from messages and logs (`/home/alice/clients/acme/bracket.solidcraft` → `bracket.solidcraft`).
|
||||
- Never attach, upload or quote the user's design, a crash report's `design.sc`, or file contents without asking. The minimal synthetic script is the default attachment.
|
||||
- GitHub does not accept `.sc` attachments. A design the user agrees to share goes up as a `.zip`: a `.sc` file is a ZIP already, so copy or rename it to `name.sc.zip`, or zip it. SolidCraft opens either kind of `.zip`.
|
||||
- Strip home folders, user names, part and project names and file paths from messages and logs (`/home/alice/clients/acme/bracket.sc` → `bracket.sc`).
|
||||
- Check screenshots for other windows, file names and anything personal.
|
||||
|
||||
## 6. Search, then write
|
||||
|
||||
@@ -12,7 +12,7 @@ The long form is `docs/book/src/dev/testing.md`.
|
||||
- Engine tests sit next to the code or in `*_tests.rs` files; they measure the result (volume, area, face/edge/vertex counts) against a value you can derive.
|
||||
- UI scenarios are JSON in `crates/ui-egui/tests/scenarios/*.json`; a new file is picked up automatically. Put `{"pending": "why"}` in a scenario that records a bug before it is fixed, and remove it in the fixing PR.
|
||||
- Reference tests are `tests/oracle/<case>/recipe.json` with an `expected.json` beside it, run by `cargo xtask oracle`. Expected values are worked out in closed form (`tools/oracle-analytic/`) or computed by FreeCAD run as a separate program (`tools/freecad-oracle/`). A case that fails on a known bug names its issue (`"known_failure": "#123"`). Never change an expected value to match SolidCraft's output, and never take one from a commercial CAD product.
|
||||
- Crash fixes: a regression test, started from the crash report's `design.solidcraft` and `replay.json` (`skills/solidcraft-crash-safety/SKILL.md`).
|
||||
- Crash fixes: a regression test, started from the crash report's `design.sc` and `replay.json` (`skills/solidcraft-crash-safety/SKILL.md`).
|
||||
|
||||
## A failing test needs its fix
|
||||
A failing test without its fix cannot merge: it turns `main` red for everyone. Include the fix in the same PR, or mark the test `#[ignore = "fails until #123 is fixed"]` and remove that line in the PR that fixes it.
|
||||
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user