Web files: designs live in the browser (OPFS), files come in by picker or drop and go out as downloads

- solvecraft_io::vfs: the engine's file access (disk natively; in the browser an in-memory set of
  files the host loads and stores). File commands and write_atomic go through it.
- Web host (apps/solvecraft-web/src/files.rs): stored designs are read from OPFS at start
  (storage.persist() asked for) and written back when a command writes one; Save/Save As keep
  designs in the browser, exports (STEP, STL, 3MF, OBJ, ...) download as files.
- File > Open shows "Files in this browser": open, insert, rename, download, delete, plus open or
  insert from the computer (showOpenFilePicker where there is one, an <input type=file>
  otherwise). Files dropped on the page are inserted or opened.
- Unsaved work is autosaved to OPFS and offered again after a reload or crash.
- Start page recents come from the stored designs, with thumbnails. In the browser the samples
  are built when opened (a picture blocks the page there, and a kernel panic can't be caught).
- Pages without OPFS (http on a network address, old browsers) keep designs in local storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 21:15:18 +00:00
1 parent d191ae571e
commit c9ed3d154d
13 files changed
+964 -42

No files matched your search

+4 -4
View File
@@ -34,11 +34,11 @@ fn source_text(p: &Value, cmd: &str) -> Result<String> {
return Ok(t.to_string());
}
let path = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "give `path` or `text`"))?;
let meta = std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
if meta.len() as usize > MAX_DRAWING_BYTES {
let meta = solvecraft_io::vfs::len(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
if meta as usize > MAX_DRAWING_BYTES {
return Err(EngineError::Other(format!("{path}: file too large")));
}
let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let bytes = solvecraft_io::vfs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
@@ -186,7 +186,7 @@ fn export_dxf(s: &mut Session, p: &Value) -> Result<Value> {
}
let text = solvecraft_io::write_dxf(&geom);
if let Some(path) = str_(p, "path").filter(|x| !x.trim().is_empty()) {
std::fs::write(path, &text).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
solvecraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
return Ok(json!({"path": path, "entities": geom.len()}));
}
Ok(json!({"dxf": text, "entities": geom.len()}))