Rename SolveCraft to SolidCraft everywhere, with compatibility shims (#389)

* WIP deep rename (not for push)

* WIP2

* WIP3

* WIP4 wxs fix

* rustfmt

* Rename the remaining identifiers in merged crash-report code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

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-12 17:17:01 -04:00
1 parent 27415e4dd7
commit 296683229d
464 files changed
+3408 -2956

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
use std::collections::HashMap;
use solvecraft_geom::Vec2;
use solidcraft_geom::Vec2;
use crate::sketch2d::{Geom2, MAX_DRAWING_BYTES, MAX_ENTITIES, bulge_arc};
use crate::{IoError, Result};
+94
View File
@@ -0,0 +1,94 @@
//! First-launch adoption of a pre-rename settings or data folder. Compatibility shim (see
//! docs/compatibility.md): when the current folder doesn't exist yet and the legacy one does,
//! the legacy folder is copied over. The legacy folder is never changed or deleted, so an older
//! release installed side by side keeps working.
use std::path::Path;
/// Most files and nesting copied from a legacy folder (it holds preferences, autosaves and
/// small caches; anything bigger is not ours).
const MAX_ENTRIES: usize = 10_000;
const MAX_DEPTH: usize = 8;
/// Copy `legacy` to `current` if `current` doesn't exist and `legacy` is a folder. Returns
/// whether a copy was made. Symbolic links are skipped. A copy that fails part way leaves what
/// was copied (the next launch sees `current` and doesn't try again) and reports the error.
pub fn adopt_legacy_dir(current: &Path, legacy: &Path) -> std::io::Result<bool> {
if current.exists() || !legacy.is_dir() || current == legacy {
return Ok(false);
}
let mut budget = MAX_ENTRIES;
copy_tree(legacy, current, 0, &mut budget)?;
Ok(true)
}
fn copy_tree(from: &Path, to: &Path, depth: usize, budget: &mut usize) -> std::io::Result<()> {
std::fs::create_dir_all(to)?;
if depth >= MAX_DEPTH {
return Ok(());
}
for entry in std::fs::read_dir(from)? {
let entry = entry?;
if *budget == 0 {
return Ok(());
}
*budget -= 1;
let kind = entry.file_type()?;
let target = to.join(entry.file_name());
if kind.is_dir() {
copy_tree(&entry.path(), &target, depth + 1, budget)?;
} else if kind.is_file() {
std::fs::copy(entry.path(), &target)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("solidcraft-legacy-dir-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn copies_legacy_folder_once_and_keeps_it() {
let d = scratch("copy");
let old = d.join("solvecraft");
std::fs::create_dir_all(old.join("recovery")).unwrap();
std::fs::write(old.join("graphics.json"), "{\"msaa\":8}").unwrap();
std::fs::write(old.join("recovery").join("a.solvecraft"), "x").unwrap();
let new = d.join("solidcraft");
assert!(adopt_legacy_dir(&new, &old).unwrap());
assert_eq!(std::fs::read_to_string(new.join("graphics.json")).unwrap(), "{\"msaa\":8}");
assert!(new.join("recovery").join("a.solvecraft").is_file());
// The legacy folder is untouched.
assert!(old.join("graphics.json").is_file() && old.join("recovery").join("a.solvecraft").is_file());
// Once the current folder exists, nothing is copied again.
std::fs::write(old.join("later.json"), "{}").unwrap();
assert!(!adopt_legacy_dir(&new, &old).unwrap());
assert!(!new.join("later.json").exists());
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn nothing_to_adopt() {
let d = scratch("none");
let new = d.join("solidcraft");
assert!(!adopt_legacy_dir(&new, &d.join("missing")).unwrap());
assert!(!new.exists());
// An existing current folder is never overwritten.
let old = d.join("solvecraft");
std::fs::create_dir_all(&old).unwrap();
std::fs::write(old.join("p.json"), "old").unwrap();
std::fs::create_dir_all(&new).unwrap();
std::fs::write(new.join("p.json"), "new").unwrap();
assert!(!adopt_legacy_dir(&new, &old).unwrap());
assert_eq!(std::fs::read_to_string(new.join("p.json")).unwrap(), "new");
let _ = std::fs::remove_dir_all(&d);
}
}
+51 -35
View File
@@ -1,4 +1,4 @@
//! File formats. Designs are saved as JSON (`.solvecraft`): the parametric document only, the
//! File formats. Designs are saved as JSON (`.solidcraft`): the parametric document only, the
//! model is recomputed on load. Bodies export as STL (binary or ASCII), OBJ, STEP and 3MF; STEP
//! files import as a base feature (see [`step_import_feature`]), 3MF and STL files as mesh bodies
//! (see [`mesh_import_feature`]).
@@ -6,6 +6,7 @@
#![forbid(unsafe_code)]
mod dxf;
mod legacy_dir;
pub mod safe_write;
mod sketch2d;
mod svg;
@@ -13,6 +14,7 @@ mod threemf;
pub mod vfs;
pub use dxf::{read_dxf, write_dxf};
pub use legacy_dir::adopt_legacy_dir;
pub use safe_write::{backup_path, write_atomic};
pub use sketch2d::{Geom2, MAX_DRAWING_BYTES};
pub use svg::read_svg;
@@ -20,26 +22,36 @@ mod obj;
pub use obj::read_obj;
pub use threemf::{MAX_3MF_BYTES, MeshObject, model_xml, read_3mf, weld, write_3mf};
use solvecraft_doc::{Document, ModelState};
use solvecraft_geom::{Mesh, Vec3};
use solidcraft_doc::{Document, ModelState};
use solidcraft_geom::{Mesh, Vec3};
/// Design file extension.
pub const DESIGN_EXT: &str = "solvecraft";
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";
/// Every extension a design file may have, current first.
pub const DESIGN_EXTS: [&str; 2] = [DESIGN_EXT, LEGACY_DESIGN_EXT];
/// Is this a design file name (`.solidcraft`, or the legacy extension), 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().any(|x| e.eq_ignore_ascii_case(x)))
}
/// 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 or solvecraft)")]
#[error("unsupported format `{0}` (use stl, obj, step, 3mf or solidcraft)")]
Format(String),
#[error("nothing to export: the design has no bodies")]
Empty,
#[error("{0}")]
Invalid(String),
#[error(transparent)]
Kernel(#[from] solvecraft_kernel::KernelError),
Kernel(#[from] solidcraft_kernel::KernelError),
#[error(transparent)]
Doc(#[from] solvecraft_doc::DocError),
Doc(#[from] solidcraft_doc::DocError),
}
pub type Result<T> = std::result::Result<T, IoError>;
@@ -65,7 +77,7 @@ pub fn is_iges_path(path: &str) -> bool {
pub struct StepFeature {
/// Timeline name: the root product's name, else the file name.
pub name: String,
pub kind: solvecraft_doc::FeatureKind,
pub kind: solidcraft_doc::FeatureKind,
/// Body names from the file (solid names, else product names).
pub body_names: Vec<String>,
pub warnings: Vec<String>,
@@ -81,8 +93,8 @@ pub fn step_import_feature(bytes: &[u8], file: &str) -> Result<StepFeature> {
// (the feature keeps that STEP text).
let text = String::from_utf8_lossy(bytes).into_owned();
let (text, iges_warnings) =
if is_iges_path(file) { solvecraft_kernel::iges_to_step(&text).map_err(IoError::Invalid)? } else { (text, Vec::new()) };
let imp = solvecraft_kernel::step_import_shared(&text)?;
if is_iges_path(file) { solidcraft_kernel::iges_to_step(&text).map_err(IoError::Invalid)? } else { (text, Vec::new()) };
let imp = solidcraft_kernel::step_import_shared(&text)?;
let stem = std::path::Path::new(file)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
@@ -99,7 +111,7 @@ pub fn step_import_feature(bytes: &[u8], file: &str) -> Result<StepFeature> {
Ok(StepFeature {
name,
body_names: imp.bodies.iter().map(|b| b.name.clone()).collect(),
kind: solvecraft_doc::FeatureKind::Import { file: file_name, step: text, components: imp.tree.clone() },
kind: solidcraft_doc::FeatureKind::Import { file: file_name, step: text, components: imp.tree.clone() },
warnings: iges_warnings.into_iter().chain(imp.warnings.iter().cloned()).collect(),
})
}
@@ -113,7 +125,7 @@ pub fn is_mesh_path(path: &str) -> bool {
pub struct MeshFeature {
/// Timeline name: the only mesh's name, else the file name.
pub name: String,
pub kind: solvecraft_doc::FeatureKind,
pub kind: solidcraft_doc::FeatureKind,
pub body_names: Vec<String>,
pub warnings: Vec<String>,
}
@@ -143,8 +155,8 @@ pub fn mesh_import_feature(bytes: &[u8], file: &str) -> Result<MeshFeature> {
// Check every mesh now, so the command fails rather than the timeline.
let mut meshes = Vec::with_capacity(objects.len());
for o in &objects {
solvecraft_kernel::mesh_body(&o.positions, &o.triangles).map_err(|e| IoError::Invalid(format!("mesh `{}`: {e}", o.name)))?;
meshes.push(solvecraft_doc::MeshData {
solidcraft_kernel::mesh_body(&o.positions, &o.triangles).map_err(|e| IoError::Invalid(format!("mesh `{}`: {e}", o.name)))?;
meshes.push(solidcraft_doc::MeshData {
name: o.name.clone(),
positions: o.positions.iter().flat_map(|p| [p.x, p.y, p.z]).collect(),
triangles: o.triangles.iter().flatten().copied().collect(),
@@ -159,7 +171,7 @@ pub fn mesh_import_feature(bytes: &[u8], file: &str) -> Result<MeshFeature> {
Ok(MeshFeature {
name,
body_names: objects.iter().map(|o| o.name.clone()).collect(),
kind: solvecraft_doc::FeatureKind::MeshImport { file: file_name, meshes },
kind: solidcraft_doc::FeatureKind::MeshImport { file: file_name, meshes },
warnings,
})
}
@@ -187,14 +199,14 @@ impl Format {
"step" | "stp" => Format::Step,
"igs" | "iges" => Format::Iges,
"3mf" => Format::ThreeMf,
"solvecraft" | "json" => Format::Design,
"solidcraft" | "solvecraft" | "json" => Format::Design,
_ => return Err(IoError::Format(ext)),
})
}
}
/// Fine export mesh for a body (chord tolerance 2e-4 of its size).
pub fn export_mesh(b: &solvecraft_kernel::Body) -> Result<Mesh> {
pub fn export_mesh(b: &solidcraft_kernel::Body) -> Result<Mesh> {
Ok(b.tessellate((b.size() * 2e-4).max(1e-3))?)
}
@@ -263,7 +275,7 @@ pub fn obj(meshes: &[(String, Mesh)]) -> String {
/// Export the given bodies of a model (all when `bodies` is empty) in a format.
pub fn export(state: &ModelState, bodies: &[String], format: Format, name: &str) -> Result<Vec<u8>> {
let sel: Vec<&solvecraft_doc::ModelBody> = state.bodies.iter().filter(|b| bodies.is_empty() || bodies.contains(&b.name)).collect();
let sel: Vec<&solidcraft_doc::ModelBody> = state.bodies.iter().filter(|b| bodies.is_empty() || bodies.contains(&b.name)).collect();
if sel.is_empty() {
return Err(IoError::Empty);
}
@@ -277,16 +289,16 @@ pub fn export(state: &ModelState, bodies: &[String], format: Format, name: &str)
}
}
Format::Step => {
let bs: Vec<solvecraft_kernel::ExportBody> =
sel.iter().map(|b| solvecraft_kernel::ExportBody { name: b.name.clone(), body: &b.body, color: b.body.color() }).collect();
let header = solvecraft_kernel::StepHeader { file_name: format!("{name}.step"), ..Default::default() };
solvecraft_kernel::step_export_bodies(&bs, &header)?.into_bytes()
let bs: Vec<solidcraft_kernel::ExportBody> =
sel.iter().map(|b| solidcraft_kernel::ExportBody { name: b.name.clone(), body: &b.body, color: b.body.color() }).collect();
let header = solidcraft_kernel::StepHeader { file_name: format!("{name}.step"), ..Default::default() };
solidcraft_kernel::step_export_bodies(&bs, &header)?.into_bytes()
}
Format::Iges => {
let bs: Vec<solvecraft_kernel::ExportBody> =
sel.iter().map(|b| solvecraft_kernel::ExportBody { name: b.name.clone(), body: &b.body, color: b.body.color() }).collect();
let header = solvecraft_kernel::StepHeader { file_name: format!("{name}.igs"), ..Default::default() };
solvecraft_kernel::iges_export_bodies(&bs, &header)?.into_bytes()
let bs: Vec<solidcraft_kernel::ExportBody> =
sel.iter().map(|b| solidcraft_kernel::ExportBody { name: b.name.clone(), body: &b.body, color: b.body.color() }).collect();
let header = solidcraft_kernel::StepHeader { file_name: format!("{name}.igs"), ..Default::default() };
solidcraft_kernel::iges_export_bodies(&bs, &header)?.into_bytes()
}
Format::ThreeMf => {
let objs: Vec<MeshObject> = sel
@@ -311,7 +323,7 @@ pub fn export(state: &ModelState, bodies: &[String], format: Format, name: &str)
.collect::<Result<_>>()?;
write_3mf(&objs)?
}
Format::Design => return Err(IoError::Format("solvecraft (save the document instead)".into())),
Format::Design => return Err(IoError::Format("solidcraft (save the document instead)".into())),
})
}
@@ -319,7 +331,7 @@ pub fn export(state: &ModelState, bodies: &[String], format: Format, name: &str)
/// the component's own frame (`local` is the model before placing occurrences), and an
/// assembly occurrence per component occurrence.
pub fn step_assembly(doc: &Document, local: &ModelState, name: &str) -> Result<Vec<u8>> {
use solvecraft_kernel::{ExportBody, ExportProduct};
use solidcraft_kernel::{ExportBody, ExportProduct};
let mut index: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
index.insert(0, 0);
let mut products = vec![ExportProduct { name: doc.name.clone(), bodies: Vec::new(), children: Vec::new() }];
@@ -338,8 +350,8 @@ pub fn step_assembly(doc: &Document, local: &ModelState, name: &str) -> Result<V
p.children.push((child, o.transform, o.name.clone()));
}
}
let header = solvecraft_kernel::StepHeader { file_name: format!("{name}.step"), ..Default::default() };
Ok(solvecraft_kernel::step_export_products(&products, 0, &header)?.into_bytes())
let header = solidcraft_kernel::StepHeader { file_name: format!("{name}.step"), ..Default::default() };
Ok(solidcraft_kernel::step_export_products(&products, 0, &header)?.into_bytes())
}
/// Read a design file's bytes.
@@ -425,8 +437,8 @@ pub const MAX_STL_TRIANGLES: usize = 20_000_000;
#[cfg(test)]
mod tests {
use super::*;
use solvecraft_doc::{FeatureKind, Model, Operation, point_expr};
use solvecraft_geom::Vec3;
use solidcraft_doc::{FeatureKind, Model, Operation, point_expr};
use solidcraft_geom::Vec3;
fn model() -> (Document, Model) {
let mut doc = Document::new("t");
@@ -472,11 +484,11 @@ mod tests {
fn mesh_volume(bytes: &[u8]) -> f64 {
let f = mesh_import_feature(bytes, "x.stl").unwrap();
let solvecraft_doc::FeatureKind::MeshImport { meshes, .. } = f.kind else { panic!() };
let solidcraft_doc::FeatureKind::MeshImport { meshes, .. } = f.kind else { panic!() };
let m = &meshes[0];
let pos: Vec<Vec3> = m.positions.chunks(3).map(|c| Vec3::new(c[0], c[1], c[2])).collect();
let tris: Vec<[u32; 3]> = m.triangles.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
solvecraft_kernel::measure(&solvecraft_kernel::mesh_body(&pos, &tris).unwrap()).unwrap().volume
solidcraft_kernel::measure(&solidcraft_kernel::mesh_body(&pos, &tris).unwrap()).unwrap().volume
}
/// STL quirks: a binary header starting with "solid", a zero triangle count, garbage
@@ -542,6 +554,10 @@ 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.
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!(read_stl(&[0u8; 10]).is_err());
let mut fake = vec![0u8; 84];
fake[80..84].copy_from_slice(&u32::MAX.to_le_bytes());
@@ -624,7 +640,7 @@ mod tests {
assert_eq!(a.name, b.name);
assert!(b.body.is_mesh() && b.body.is_closed_mesh(), "{} is not watertight", b.name);
let v_stl = stl_volume(&export(&st, std::slice::from_ref(&a.name), Format::StlBinary, "t").unwrap());
let v = solvecraft_kernel::measure(&b.body).unwrap().volume;
let v = solidcraft_kernel::measure(&b.body).unwrap().volume;
assert!((v - v_stl).abs() <= 1e-6 * v_stl.abs(), "{}: {v} vs {v_stl}", a.name);
}
// Mesh bodies export again (3MF, STL, OBJ) but not as STEP.
+1 -1
View File
@@ -3,7 +3,7 @@
//! are split into fans. Texture coordinates, normals and materials are ignored. The file is
//! untrusted: counts are capped and bad lines become warnings.
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::threemf::MeshObject;
use crate::{IoError, Result};
+4 -4
View File
@@ -114,7 +114,7 @@ mod tests {
use super::*;
fn dir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solvecraft-safe-write-{tag}-{}", std::process::id()));
let d = std::env::temp_dir().join(format!("solidcraft-safe-write-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
@@ -123,7 +123,7 @@ mod tests {
#[test]
fn replaces_and_keeps_a_backup() {
let d = dir("bak");
let p = d.join("a.solvecraft");
let p = d.join("a.solidcraft");
write_atomic(&p, b"one", true).unwrap();
assert!(!backup_path(&p).exists(), "nothing to back up the first time");
write_atomic(&p, b"two", true).unwrap();
@@ -139,7 +139,7 @@ mod tests {
#[test]
fn a_save_stopped_part_way_keeps_the_old_file() {
let d = dir("stop");
let p = d.join("a.solvecraft");
let p = d.join("a.solidcraft");
write_atomic(&p, b"old", true).unwrap();
for stop in [Step::TempWritten, Step::BackupWritten, Step::Replaced] {
write_steps(&p, b"new content", true, Some(stop)).unwrap();
@@ -160,7 +160,7 @@ mod tests {
#[test]
fn a_failed_write_leaves_the_target() {
let d = dir("fail");
let p = d.join("missing-dir").join("a.solvecraft");
let p = d.join("missing-dir").join("a.solidcraft");
assert!(write_atomic(&p, b"x", true).is_err());
assert!(!p.exists());
let _ = fs::remove_dir_all(&d);
+1 -1
View File
@@ -1,7 +1,7 @@
//! 2D drawing exchange for sketches: DXF (ASCII, read and write) and SVG (read). Readers produce
//! plain 2D geometry in millimetres; the engine turns it into sketch curves.
use solvecraft_geom::Vec2;
use solidcraft_geom::Vec2;
/// One imported 2D entity (millimetres, y up).
#[derive(Clone, Debug, PartialEq)]
+1 -1
View File
@@ -5,7 +5,7 @@
use quick_xml::Reader;
use quick_xml::events::{BytesStart, Event};
use solvecraft_geom::Vec2;
use solidcraft_geom::Vec2;
use crate::sketch2d::{Geom2, MAX_DRAWING_BYTES, MAX_ENTITIES};
use crate::{IoError, Result};
+1 -1
View File
@@ -12,7 +12,7 @@ use std::io::{Cursor, Read, Write};
use quick_xml::Reader;
use quick_xml::events::{BytesStart, Event};
use solvecraft_geom::Vec3;
use solidcraft_geom::Vec3;
use crate::{IoError, Result};
+2 -2
View File
@@ -113,7 +113,7 @@ pub fn read_to_string(path: &str, max: usize) -> Result<String> {
mod tests {
#[test]
fn files_round_trip() {
let dir = std::env::temp_dir().join(format!("solvecraft-vfs-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-vfs-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let p = dir.join("a.txt").to_string_lossy().to_string();
assert!(!super::exists(&p));
@@ -140,7 +140,7 @@ mod tests {
assert!(e.to_string().contains("not a regular file"), "{e}");
assert!(super::read_to_string("/dev/zero", 1024).is_err());
let dir = std::env::temp_dir().join(format!("solvecraft-vfs-fifo-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("solidcraft-vfs-fifo-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let fifo = dir.join("p");
let made = std::process::Command::new("mkfifo").arg(&fifo).status().map(|s| s.success()).unwrap_or(false);