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:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-27 22:41:40 +00:00
1 parent 613b468ef5
commit 03b8f4138b
105 files changed
+493 -274

No files matched your search

+3
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+5 -2
View File
@@ -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));
+14 -14
View File
@@ -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;
+3 -3
View File
@@ -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() {
+6 -1
View File
@@ -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(&current).output().unwrap());
assert_eq!(body_count(&current), 3);
assert!(solidcraft_engine::io::is_design_container(&std::fs::read(&current).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 {
+18 -8
View File
@@ -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]
+13 -8
View File
@@ -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;
}),
}
+9 -4
View File
@@ -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")?;
+2 -2
View File
@@ -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(());
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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.",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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} 時間前",
+1 -1
View File
@@ -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}시간 전",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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} ч назад",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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} год тому",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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} 小時前",
+1 -1
View File
@@ -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>
+24 -2
View File
@@ -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>
+3 -3
View File
@@ -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 -6
View File
@@ -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="&quot;%1&quot;" />
</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="&quot;[#SolidcraftExe]&quot; &quot;%1&quot;" 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" />
+3 -3
View File
@@ -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();
+6 -6
View File
@@ -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()))?;
+35 -2
View File
@@ -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.
+1 -1
View 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);
}
+11 -6
View File
@@ -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());
}
+12
View File
@@ -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");
+41 -25
View File
@@ -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.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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}));
+2 -2
View File
@@ -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
View File
@@ -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());
+22
View File
@@ -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
+4 -4
View File
@@ -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,
+1 -1
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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)",
},
+8 -7
View File
@@ -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());
+1 -1
View File
@@ -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);
+46 -7
View File
@@ -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");
+5 -5
View File
@@ -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},
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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.
+14 -14
View File
@@ -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
+6 -6
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View 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
+6 -6
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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(
+1
View File
@@ -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), []);
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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