M1: keep settings next to the executable in portable mode (#1320) (#1558)

The settings folder comes from craft-settings' rules: FIGCRAFT_CONFIG_DIR,
a `portable` file next to the executable (config/ beside it), then the
platform folder every earlier version used. The Windows portable zip ships
the marker, and a portable folder without preferences starts with a copy
of the platform folder's.
This commit is contained in:
BFlat
2026-10-30 03:35:12 +07:00
committed by GitHub
parent 28a0fe6d18
commit aa727500a4
11 changed files with 214 additions and 6 deletions
Generated
+52
View File
@@ -1041,6 +1041,17 @@ dependencies = [
"objc2-app-kit 0.3.2",
]
[[package]]
name = "craft-settings"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"craft-atomic",
"dirs",
"serde",
"toml_edit",
]
[[package]]
name = "craft-ui"
version = "0.1.0"
@@ -1266,6 +1277,27 @@ dependencies = [
"crypto-common 0.2.2",
]
[[package]]
name = "dirs"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -1675,6 +1707,7 @@ version = "0.12.1"
dependencies = [
"arboard",
"craft-menu",
"craft-settings",
"craft-update",
"craft-window",
"eframe",
@@ -3710,6 +3743,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "orbclient"
version = "0.3.55"
@@ -4291,6 +4330,16 @@ dependencies = [
"bitflags 2.13.2",
]
[[package]]
name = "redox_users"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
dependencies = [
"libredox",
"thiserror 2.0.21",
]
[[package]]
name = "regex"
version = "1.13.1"
@@ -5253,8 +5302,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
+2
View File
@@ -51,6 +51,8 @@ craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff13
# The macOS app menu whose Quit (⌘Q) asks the app before quitting, like the window's close button
# (craft-libs; the menu it replaces quits straight through `terminate:` and loses unsaved work).
craft-menu = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
# Where the settings live: the suite's folder rules, portable mode included (craft-libs).
craft-settings = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
# foundation
kurbo = { version = "0.13", features = ["serde"] }
+2 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -20,6 +20,8 @@ figcraft-text = { workspace = true }
# bespoke Wayland xdg-decoration probe. See `[workspace.dependencies]`: pinned to the unmerged
# craft-libs#217 branch and must be repointed to a merged `rev` before this PR is mergeable.
craft-window = { workspace = true }
# The suite's settings folder: `FIGCRAFT_CONFIG_DIR`, a portable marker, then the platform folder.
craft-settings = { workspace = true }
egui = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "clipboard", "wayland", "x11"] }
serde_json = { workspace = true }
+2
View File
@@ -43,6 +43,8 @@ mod recovery_tests;
#[cfg(test)]
mod services_tests;
#[cfg(test)]
mod settings_dir_tests;
#[cfg(test)]
mod startup_tests;
use std::sync::{Arc, Mutex};
+43 -1
View File
@@ -27,14 +27,56 @@ pub struct Desktop {
last_folder: Arc<Mutex<Option<PathBuf>>>,
}
/// The settings folder's name in the platform folder, as it always was (the suite keeps existing
/// folder names), and craft-settings' app id: `FIGCRAFT_CONFIG_DIR` names another folder.
const APP_ID: &str = "FigCraft";
/// Where the preferences, recovery copies and installed plugins live ([`settings_dir`]).
pub fn config_dir() -> Option<PathBuf> {
settings_dir(&craft_settings::Probe { platform_config_dir: platform_dir(), ..Default::default() })
}
/// The platform's config folder, read from the environment as before: `%APPDATA%` on Windows,
/// so a test's own `APPDATA` keeps the app away from the user's settings.
fn platform_dir() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
let base = std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library/Application Support"));
#[cfg(target_os = "windows")]
let base = std::env::var_os("APPDATA").map(PathBuf::from);
#[cfg(all(unix, not(target_os = "macos")))]
let base = std::env::var_os("XDG_CONFIG_HOME").map(PathBuf::from).or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")));
base.map(|b| b.join("FigCraft"))
base
}
/// The settings folder by the suite's rules (craft-settings), from what `probe` sees:
/// `FIGCRAFT_CONFIG_DIR` (set but empty: none, nothing read or written); a `portable` file next
/// to the executable (next to the `.app` on macOS), which puts them in `config/` beside it; else
/// `FigCraft` in the platform folder, where they always were. A portable folder without
/// preferences yet starts with a copy of the platform folder's, so an existing copy that becomes
/// portable keeps its settings; the original stays for an installed app. No platform folder: none.
pub(crate) fn settings_dir(probe: &craft_settings::Probe) -> Option<PathBuf> {
use craft_settings::{Location, Portable, Rule};
let resolved = craft_settings::paths::resolve(APP_ID, &Portable::DEFAULT, &Location::Auto, probe);
match (&resolved.rule, &resolved.dir, &probe.platform_config_dir) {
(Rule::Portable, Some(dir), Some(platform)) => seed(dir, &platform.join(APP_ID)),
// The current directory instead: settings would follow wherever the app was started.
(Rule::Fallback, ..) => return None,
_ => {}
}
resolved.dir
}
/// Copy `from`'s preferences into the folder `to` when it has none (owner-only, as written).
fn seed(to: &Path, from: &Path) {
let (target, source) = (to.join(STATE), from.join(STATE));
if target.exists() || !source.is_file() {
return;
}
let copied = std::fs::read(&source).and_then(|bytes| std::fs::create_dir_all(to).and_then(|()| write_private(&target, &bytes)));
match copied {
Ok(()) => log::info!("preferences: copied {} into the portable folder {}", source.display(), to.display()),
Err(e) => log::warn!("preferences: couldn't copy them into the portable folder {}: {e}", to.display()),
}
}
impl Desktop {
+101
View File
@@ -0,0 +1,101 @@
//! Where the settings live (#1320): a portable copy keeps them next to the executable, taking the
//! ones it had before; without a marker they stay in the platform folder they always used. Every
//! folder here is a temporary one: the probe sees no real environment, executable or profile.
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use craft_settings::Probe;
use figcraft_engine::Prefs;
use crate::services::{Desktop, settings_dir, write_prefs};
/// A fresh temporary folder with an `app` folder (the executable's) and an `appdata` folder (the
/// platform's config folder).
fn root(name: &str) -> PathBuf {
let r = std::env::temp_dir().join(format!("figcraft-settings-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&r);
std::fs::create_dir_all(r.join("app")).unwrap();
std::fs::create_dir_all(r.join("appdata")).unwrap();
r
}
/// What the rules see: the executable in `root/app`, the platform folder `platform`, and
/// `FIGCRAFT_CONFIG_DIR` set to `config_dir` when given.
fn probe(root: &Path, platform: Option<PathBuf>, config_dir: Option<&'static str>) -> Probe {
Probe {
env: Box::new(move |name| config_dir.filter(|_| name == "FIGCRAFT_CONFIG_DIR").map(OsString::from)),
exe_dir: Some(root.join("app")),
platform_config_dir: platform,
}
}
/// Preferences with a name to recognise them by.
fn prefs(name: &str) -> Prefs {
Prefs { user_name: name.into(), ..Prefs::default() }
}
/// The user name of the preferences the app starts with from `dir`.
fn loaded_name(dir: PathBuf) -> Option<String> {
Desktop::load_from(Some(dir), Arc::new(Mutex::new(None))).prefs.map(|p| p.user_name)
}
#[test]
fn without_a_marker_the_settings_stay_where_they_always_were() {
let root = root("installed");
let old = root.join("appdata").join("FigCraft");
write_prefs(&old, &prefs("Ada"));
let dir = settings_dir(&probe(&root, Some(root.join("appdata")), None));
assert_eq!(dir, Some(old.clone()), "the folder every earlier version used");
assert_eq!(loaded_name(old), Some("Ada".into()), "an existing user's preferences are read");
assert!(!root.join("app").join("config").exists(), "nothing is written next to the executable");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_portable_marker_keeps_the_settings_next_to_the_executable_and_takes_the_old_ones() {
let root = root("portable");
let old = root.join("appdata").join("FigCraft");
write_prefs(&old, &prefs("Ada"));
std::fs::write(root.join("app").join("portable"), "").unwrap();
let portable = root.join("app").join("config");
let dir = settings_dir(&probe(&root, Some(root.join("appdata")), None));
assert_eq!(dir, Some(portable.clone()));
assert_eq!(loaded_name(portable.clone()), Some("Ada".into()), "the copy starts with the settings it had");
assert_eq!(loaded_name(old.clone()), Some("Ada".into()), "the installed app's stay where they were");
// Later starts keep the portable copy's own settings.
write_prefs(&portable, &prefs("Grace"));
assert_eq!(settings_dir(&probe(&root, Some(root.join("appdata")), None)), Some(portable.clone()));
assert_eq!(loaded_name(portable), Some("Grace".into()), "never overwritten by the old ones again");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_new_portable_copy_starts_without_settings() {
let root = root("fresh");
std::fs::write(root.join("app").join("portable"), "").unwrap();
let portable = root.join("app").join("config");
assert_eq!(settings_dir(&probe(&root, Some(root.join("appdata")), None)), Some(portable.clone()));
assert_eq!(loaded_name(portable), None, "a first run");
// No platform folder at all: the marker still decides.
assert_eq!(settings_dir(&probe(&root, None, None)), Some(root.join("app").join("config")));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn figcraft_config_dir_names_the_folder_and_empty_means_none() {
let root = root("env");
std::fs::write(root.join("app").join("portable"), "").unwrap();
let named = settings_dir(&probe(&root, Some(root.join("appdata")), Some("elsewhere")));
assert_eq!(named, Some(PathBuf::from("elsewhere")), "ahead of the marker");
assert_eq!(settings_dir(&probe(&root, Some(root.join("appdata")), Some(""))), None, "isolated: nothing read or written");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn without_a_platform_folder_there_are_no_settings() {
let root = root("nowhere");
assert_eq!(settings_dir(&probe(&root, None, None)), None, "not the current directory");
let _ = std::fs::remove_dir_all(&root);
}
+3 -1
View File
@@ -1,6 +1,6 @@
# Hardware and platform parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); macOS: the Quit menu and ⌘Q ask about unsaved changes (#857); egui/eframe 0.37 dependency migration; hidden-window screenshot deadlines; zoom previews; hidden windows answer control requests; macOS file opening, the Windows start-up message, Linux packaging; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses; macOS: quitting ends the process once everything is kept, avoiding the Touch Bar teardown crash (#781); Linux and BSD clipboard: images under Wayland, reads time out; minimised windows under Wayland; window controls on Wayland without server decorations; canvas textures on device pixels; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); macOS: the Quit menu and ⌘Q ask about unsaved changes (#857); egui/eframe 0.37 dependency migration; hidden-window screenshot deadlines; zoom previews; hidden windows answer control requests; macOS file opening, the Windows start-up message, Linux packaging; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses; macOS: quitting ends the process once everything is kept, avoiding the Touch Bar teardown crash (#781); Linux and BSD clipboard: images under Wayland, reads time out; minimised windows under Wayland; window controls on Wayland without server decorations; canvas textures on device pixels; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses) · **Target:** Figma Design (desktop app 126.10.7)
How FigCraft uses hardware and which platforms it runs on, against Figma. Figma's desktop app is
Electron with a GPU helper process; its canvas renders on the GPU (WebGL, with WebGPU rolling out
@@ -40,6 +40,7 @@ Nothing has been timed against Figma on the same document (needs `.fig` import).
| Platform | Figma | FigCraft | Notes |
|---|---|---|---|
| macOS | desktop app (Intel and Apple silicon) | universal `.dmg`, signed and notarized (v0.2.0) | where nearly all QA happens. Quitting ends the process once the preferences, recovery copies and live sessions are taken care of: the window's teardown crashed on Macs with a Touch Bar (#781); the Quit menu and ⌘Q ask about unsaved changes like the main window's close button (#857), and like it close the main window only: with several windows open, each quit closes one; a quit that never asked — killed or crashed — still leaves recovery copies for the next start |
| Windows x64 / arm64 | desktop app | `.msi` and portable zip for x64, x86 and arm64 | built in CI; Windows: QA pass done 2026-10-11, findings in PR. The portable zip keeps its settings in `config\` beside `figcraft.exe` (a `portable` file there), taking an installed copy's preferences on its first start (#1320) |
| Windows x64 / arm64 | desktop app | `.msi` and portable zip for x64, x86 and arm64 | built in CI; Windows: QA pass done 2026-10-11, findings in PR. Draws with Direct3D 12 (FXC shaders) or OpenGL; when the graphics fail before the first frame it starts again on Vulkan, then OpenGL, then the power-saving GPU with lower limits (#965) |
| Linux | none (browser only) | AppImage, `.deb`, `.rpm`, Flatpak, tarball, x86_64 and aarch64; riscv64 tarball | CI tests run here. Clipboard (also the BSDs): reads run on a helper thread that gives up after 2 s, and images copied in other apps paste under Wayland through the data-control protocol (#693). Under Wayland the app can't tell its window is minimised, so control requests that need a drawn frame may still wait there (#720). None of this run on a Linux desktop here. On a Wayland compositor without server-side decorations (GNOME) the window gets winit's Adwaita controls (#592: checked by its author in a GNOME Wayland session; KDE not run) |
| FreeBSD | none | tarball | |
@@ -50,6 +51,7 @@ Nothing has been timed against Figma on the same document (needs `.fig` import).
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | M1, #1320: settings in portable mode. The settings folder now comes from craft-settings, the suite's rules: `FIGCRAFT_CONFIG_DIR` names a folder (set but empty: none, nothing read or written); a `portable` file next to the executable (next to the `.app` on macOS) puts the preferences, recovery copies and installed plugins in `config/` beside it; otherwise they stay in `FigCraft` in the platform folder, where every earlier version kept them, so nothing moves for an installed app. The Windows portable zip now ships the `portable` file. A portable folder without preferences yet starts with a copy of the platform folder's `state.json` (owner-only, the original left for an installed app), so a portable copy that kept its settings in `%APPDATA%` before keeps them after the update. Checked on Windows 11 with a portable copy beside an installed copy's settings: the first start took them (its name and start screen), a name changed there was kept in `config\` only, and without the marker the app read the installed settings again. Tests: `settings_dir_tests`. Numbers unchanged |
| 2026-10-30 | minor | M1, #965, #1438: "Parent device is lost" at start. Both reports lost the device while wgpu compiled its indirect-call validation shaders, which it builds with every device: on Windows Direct3D 12 took another program's `dxcompiler.dll` from the search path and failed without its `dxil.dll`; Intel Macs' Metal compiler aborts on them. Direct3D 12 now compiles with FXC (part of Windows; `WGPU_DX12_COMPILER` still chooses), and the validation shaders are left out (FigCraft draws nothing indirectly; `WGPU_VALIDATION_INDIRECT_CALL` turns them back on). When the graphics still fail before the window's first frame, the app starts itself again with the next attempt, from a fixed list: on Windows Direct3D 12/OpenGL, Vulkan, OpenGL, then the first again on the power-saving GPU with WebGL2-level device limits; on Linux and FreeBSD the defaults, OpenGL, then lower requirements; on macOS the defaults, then lower requirements. The last failure's message names what was tried; the log names the backend and GPU that drew. `WGPU_BACKEND` is still the one attempt. A restarted app waits up to 5 s for its `--control` port. Checked on Windows 11 with a local build that made the first one, two, three and then all four attempts fail: each fallback opened the window (Vulkan, OpenGL, Direct3D 12 on the integrated GPU) and four failures ended with the message. Not run on the reporters' machines or on macOS. Tests: `graphics_tests`, `startup_tests`. Numbers unchanged |
| 2026-10-29 | minor | Large images on the canvas (#1483): an image shown smaller than its pixels draws on the canvas from a copy scaled down to 4096 or 8192 px on its longer side, the smallest that holds what the screen shows (`figcraft_render::images::shown`), cached beside the image itself in the same 512 MB budget. Two 16 000 × 10 000 images took 640 MB each and evicted each other on every frame that drew both; as copies they take 80 MB together zoomed out and 336 MB at 8192. Moving one over the other went from about 2.1 s a frame to 11 ms zoomed to fit and 15 ms at 30% in the app (debug build). Closer than that, and for tiles, the canvas draws from the image itself, so 100% and closer stay sharp; a view of both at 100% still takes about 2 s a frame, and an image with shadows still decodes in full for them. Exports, thumbnails and every other render are unchanged. Tests: `large_image_tests`. Numbers unchanged |
| 2026-10-29 | minor | macOS quit confirmation (#857): winit's default Quit menu item (⌘Q) ended the process through AppKit's `terminate:` at once, so a dirty app died without asking anything. The app now installs the craft-libs app menu (craft-menu): its Quit sends the main window a close request — the same path as its close button — so the unsaved-changes question asks before anything is lost (`windows_tests::a_quit_with_unsaved_changes_waits_for_the_answer`; checked in the app on macOS). Like the close button it closes the main window only, so with several windows open each quit closes one. A second quit before the window draws again (two quits while it is minimised, when only the app's logic runs) counts once: before, it could quit the app over another window's unsaved changes (`windows_tests::quitting_twice_before_the_window_draws_closes_it_once`). Numbers unchanged |
+1 -1
View File
@@ -180,7 +180,7 @@ with the same limits and its manifest's permissions, as any other.
The desktop app keeps such plugins per user, beside its settings (`Plugins/` in FigCraft's settings
folder: `%APPDATA%\FigCraft` on Windows, `~/Library/Application Support/FigCraft` on macOS,
`~/.config/FigCraft` elsewhere), each in a folder of its own named by a hash of its id, with its
`~/.config/FigCraft` elsewhere; `config/` beside the executable in a portable copy), each in a folder of its own named by a hash of its id, with its
module and its assets. The CLI and the MCP server install for the session only.
**Updating.** When the directory has a newer version than the one installed, the plugin's row
+2 -1
View File
@@ -1,6 +1,6 @@
# FigCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
@@ -75,6 +75,7 @@ blockers.
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | M1, #1320: settings in portable mode: a `portable` file next to the executable keeps the settings, recovery copies and plugins in `config/` beside it (the Windows portable zip ships one), starting with the platform folder's preferences; the folder comes from craft-settings' rules, and without a marker it is the one every earlier version used. Tests: `settings_dir_tests` |
| 2026-10-30 | minor | M1, #965, #1438: "Parent device is lost" at start: Direct3D 12 compiles with FXC, no device compiles wgpu's indirect-validation shaders, and when the graphics still fail before the first frame the app starts again with the next of a fixed list of attempts (Windows: Vulkan, OpenGL; Linux and FreeBSD: OpenGL; then lower requirements on the power-saving GPU); the last failure names what was tried. Tests: `graphics_tests`, `startup_tests` |
| 2026-10-29 | minor | M4, #1219: Thai, Lao, Khmer and Burmese text, written without spaces between words, wraps between words instead of where a line overflows (`พัฒนาซอ|ฟต์แวร์` → `พัฒนา|ซอฟต์แวร์`). `figcraft_text::breaks::word_breaks` finds the word starts inside each run of these scripts with ICU4X's line segmenter (`icu_segmenter` 2.3): its dictionaries on desktop, its LSTM models in the browser (about 0.39 MB of wasm instead of 1.86 MB, which the 24 MiB gate can't take; they split a few compounds the dictionaries keep whole). Where a run meets spaces, Latin or CJK the UAX #14 rules decide as before, and a word wider than its box still takes #1400's emergency break. Text without these scripts does no extra work. Grade unchanged. Evidence: `breaks::tests::thai_lines_break_between_words`, `lao_khmer_and_burmese_lines_break_between_words`, `word_breaks_survive_junk`, `behaviour_tests::thai_lines_break_between_words` (fails on main). |
| 2026-10-30 | minor | M8, #1416: an attached MCP agent's presence (avatar in the people row, outline and name tag on the layers it last edited or selected), set by `Session::execute_as` and gone 8 seconds after its last command; display only, never synced. Tests: `agent_presence_tests` (engine), `live::agent` (ui-egui) |
+4 -1
View File
@@ -189,7 +189,10 @@ if ($env:CRAFT_FONTS_DIR) {
if (Test-Path $lic) { Copy-Item $lic (Join-Path $Portable "OFL-$($_.Name).txt") }
}
}
# files go to FigCraftData\ next to the exe instead of %APPDATA% (#228; see app_dirs.rs).
# A `portable` file next to figcraft.exe keeps its settings, recovery copies and plugins in
# config\ beside it instead of %APPDATA%\FigCraft (#1320). The first start copies the
# preferences an installed FigCraft kept there, if any.
New-Item -ItemType File -Force -Path (Join-Path $Portable 'portable') | Out-Null
$Zip = Join-Path $Dist "figcraft-$Version-windows-$Arch-portable.zip"
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
Compress-Archive -Path $Portable -DestinationPath $Zip