Save as New Workspace stored the layout and `window.workspace` could
switch to it, but the Workspace submenu was a fixed list of the
built-in workspaces, so a saved one never appeared and could not be
chosen from the menu.
The submenu now has a `savedWorkspaces` dynamic node after the
built-ins (DynCtx carries the saved names), in the in-window and native
menus, with a check mark on the current one as the built-ins have.
The in-window submenu was also too short to show it. egui lays out a
new submenu in a default 400 pt area, and the scroll area inside took
that height and kept it, so Window ▸ Workspace (514 pt) opened with its
last rows (Undocked Panels and below, where saved workspaces go)
scrolled out of view. Submenus now open at their full height and scroll
only when taller than the window.
Tests: saved_workspace_is_listed_in_the_workspace_menu (picks it from
the menu bar), saved_workspaces_are_in_the_native_workspace_menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apply_output checked `channels: rgba` against the module before it
applied `webmCodec`, and choosing AV1 then quietly dropped the channels
to RGB. So `render --format webm --webm-codec av1 --channels rgba` (one
renderQueue.add) succeeded and wrote an opaque AV1 stream.
The codec options now apply before Channels, so the existing "has no
alpha channel" error (how an unsupported channels request is already
reported) refuses the combination; for WebM it names the codec: "AV1
WebM has no alpha channel (VP9 WebM has)". Choosing AV1 alone on an
RGBA VP9 item still switches it to RGB, as changing the format does.
Test: av1_webm_refuses_requested_alpha.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apply_output switched the format whenever the Output To extension mapped
to a different format, and `.mp4` maps to H.264 only, so
renderQueue.setOutput with an .mp4 path (even the item's own default
name) turned an HEVC or AV1 module into H.264 and the render wrote an
H.264 stream.
The extension now changes the format only when the current format
doesn't write it (OutputFormat::extension): HEVC, AV1 and H.264 all write
.mp4, so they stay; `.mov` on H.264 still picks ProRes and `.mp4` on
ProRes still picks H.264.
Test: output_to_keeps_a_format_that_writes_the_extension.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Their params docs wrote the renderer as `renderer? classic3D|advanced3D`.
Without a `:`, the doc parser reads `|` as "or another key", so
`advanced3D` became an accepted key in the schemas and the unknown-key
check: `comp.new {"advanced3D": true}` succeeded and made a Classic 3D
comp.
The docs now use the value-list form the other commands use,
`renderer?: classic3D|advanced3D`. The key is removed rather than
implemented: it would only duplicate `renderer` (and could contradict
it). A scan of every command's schema found no other key that came from
a value list.
Test: renderer_values_are_not_parameter_keys (not in the schema, rejected
as unknown, `renderer` still works on both commands).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prop.setExpression keeps an expression with a syntax error but disables
it, and says why only in its own reply. MCP set_property answers with
prop.get instead, and prop.get reported `evaluated` / `expressionError`
only for enabled expressions, so set_property, get_property and prop.get
all showed the broken expression as if it were installed.
prop.get now reports a disabled expression too: `evaluated` is the
static value that renders, and `expressionError` is its syntax error.
Test: expression_syntax_errors_reach_the_reply (set_property and
get_property carry the SyntaxError; runtime errors and valid expressions
report as before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`effectcraft-cli script FILE` passes the file's text to script.run, which
ran a compiled .jsxbin script as JavaScript and failed with "SyntaxError:
unexpected '@'" at 1:1, while file.runScript already says .jsxbin isn't
supported.
script.run now refuses a script named `.jsxbin` or whose text starts with
the `@JSXBIN@` header, with file.runScript's message (one shared
constant). The check is in the command, so the CLI with or without
--bridge, MCP run_script and the Script Console all report it.
Tests: engine_entry_points (by header and by name), script_file_and_eval
(the CLI exits 1 with the message and no SyntaxError).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pan Behind drag passed its layer to the snapper's exclude list, which
dropped all of that layer's targets: its corners, edge midpoints and centre
as well as the anchor being dragged. The layer's box doesn't move in comp
space while the anchor pans (Position compensates), so those points are
valid targets.
The engine's per-layer snap targets are now one function, layer_targets,
which can leave out the anchor point. The gesture records its layer's
targets without the anchor as the drag begins and passes them to the
snapper (snap_with) as extra targets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Selecting a property group also selects its layer, and edit.clear only
looked for selected keys, mask vertices, puppet pins and effects before
deleting the selected layers, so Delete with a text animator, mask or
shape group selected deleted the whole layer.
edit.clear now removes the selected removable groups (text animators,
masks, shape items, trackers) of the active comp in one undo step before
falling back to layers. It shares the removal with prop.removeGroup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build::default_label picked a new layer's label from its type alone
(comps Sandstone, images Lavender, ...), ignoring the label of the Project
item it was made from.
A layer made from an item (footage, comp, solid, 3D model) now copies the
item's label when it is created; every path that adds an item to a comp
(layer.addItem from the menus, Project panel, Timeline and viewer drops,
models, precomps, imports) builds its layer there. Layers keep their own
label when the item's changes later. Layers without an item keep the type
defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
U (and P, S, R, T, A, E, M, F, L) kept one reveal filter for the whole
Timeline and replaced the set of open layers with the selected ones, so
revealing on one layer closed every other layer and dropped their
reveals.
Each layer now has its own filter (TimelineState::layer_reveal; an open
layer without one shows its whole tree). A shortcut applies to the
selected layers (all layers with none selected) through apply_reveal and
leaves the others as they are; `reveal` stays the last shortcut's filter
for toggling and Shift-adding. U's results replace only the target
layers' earlier picks, so U after UU shows the keyframed properties
again. anim.reveal tells the Timeline which layers it looked at, so a
layer with nothing keyframed closes. Twirling a layer by hand, Ctrl+`
and Collapse All clear the filters they cover.
Test: reveal_shortcuts_leave_unselected_layers_alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#154: renderQueue.add (and render.addOutputModule) applied `format`, then
dropped it before applying the output path, so `--format hevc --out
x.mp4` saw the .mp4 extension and switched the module back to H.264
(and ProRes / VP9 for .mov / .webm). An explicit format now wins and a
mismatched extension follows it (x.webm → x.mp4 for AV1), as choosing a
format in the Output Module renames Output To; an extension that agrees
with the format keeps the name as given. The format error lists hevc and
av1 everywhere (one FORMATS string).
#155: a `comp` that names no composition (name or id) failed with "no
active composition". comp_id now reports it as `no composition "Nope"`
(EngineError::NoSuchComp), and comp_for_queue passes that on instead of
collapsing it.
Tests: an_explicit_format_wins_over_the_output_extension and
an_unknown_comp_is_named_in_the_error (engine). Checked with the CLI:
the files are hevc/hvc1 and av1/av01 and decode cleanly with ffmpeg.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Composition viewer ignored drags from other panels. Now, as in After
Effects:
- A Project item dropped on the viewer becomes a layer centred where it
lands (above the selected layer, like any new layer); while dragging, the
viewer is outlined and the layer's frame follows the pointer.
- Media Browser files do the same; files dropped from the OS file manager
do too when the platform reports where the pointer is during the drag
(otherwise they import as before).
- An effect from Effects & Presets goes on the layer under the pointer,
which is outlined while dragging.
layer.addItem and layer.newModel take `position` ([x, y] comp px).
file.import takes `addToComp` with `time`, `index` and `position`;
mediaBrowser.import shares it (file::add_to_comp): layered files go in as
their comps, the comp they were dropped in stays open, and items that
can't be layers are reported in `errors`. viewer::layer_at is the hit
test pick() and the drop share.
Tests: ui_drag_drop.rs (project item and effect dropped on the viewer)
and imported_files_go_into_the_comp_where_they_were_dropped (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Project item (or Media Browser file) dropped in the Timeline always
went above the selected layer, starting where new layers start.
Now, as in After Effects, a line shows where it goes in the stack (above
the first layer row whose middle is below the pointer, else below the
last), and over the time graph a marker with a timecode tag shows its In
point, which follows the pointer (frame-snapped) or, with Shift, the
current time. The layer reorder drag shares the insertion logic.
layer.addItem and layer.newModel take `index` (1-based stack position);
mediaBrowser.import passes `time` and `index` on, one layer under the
other.
Tests: project_items_land_where_they_are_dropped_in_the_timeline
(ui_drag_drop.rs, real pointer drags: outline, time graph, Shift, below
the last layer) and add_item_at_a_stack_index_and_time (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Toggling a layer's Audio, Lock or Shy switch (or Hide Shy Layers) threw
away every cached frame: the RAM preview identity hashes each comp's
address, which any edit replaces, and the layer and disk cache keys
hashed every switch. None of these change pixels.
- Switches::pixels, Layer::same_pixels and Comp::same_pixels (exhaustive
field lists, so a new field must be considered) and Comp::pixel_form.
- Layer cache keys hash Switches::pixels; the disk cache's comp content
key hashes Comp::pixel_form.
- Frames::identity: when a new revision's comps draw the same pixels as
those of the comp's last new identity, that identity (and the project
holding its comps' addresses) is kept, so the frames stay, also when the
switch is toggled back.
With audio on, playback skipped frames that weren't rendered yet. Now the
sound waits: until the frames ahead (to the end of the preview, or what
fits in the RAM preview) are cached, every frame shows as it renders,
silently, like a preview without audio; then the sound starts in sync.
Reaching an uncached frame stops the sound instead of skipping frames.
Tests: audio_lock_and_shy_switches_keep_the_frames (frames.rs, nested
comps and toggling back), audio_lock_and_shy_keep_cached_layers (engine),
the disk key in tests_disk_cache, and
preview_with_audio_shows_every_frame_and_sounds_once_cached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use checked hex slices so prefs.set follows the existing invalid-colour fallback when a component splits a UTF-8 character. Cover invalid Unicode placements and valid mixed-case colours through the command.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
A Windows user reported that the Properties panel's font menu offered only the bundled Inter,
JetBrains Mono and Noto Serif. The system font folders were scanned only when a project asked for
a family the bundled fonts lack, so until then every font menu (Properties, Character, Essential
Graphics) and `file.findMissing {what: fonts}` saw the bundled families alone, on every
platform. Listing the families now scans the system fonts first, and the desktop app starts that
scan on a background thread at launch so the menus are complete when first opened.
The style menus in the Properties and Character panels list the chosen family's own styles
instead of a fixed Regular / Medium / SemiBold / Bold / Italic. Both menus build their lists only
while open.
New query for agents: `text.fonts {query?, rescan?}` (MCP tool `list_fonts`) lists every family
with its styles, origin (bundled / system / user) and own-language name; `rescan` picks up fonts
installed since launch.
Tests: a fresh-process test that the family list includes every family in the platform's font
folders without a prior scan (failed before), and `text.fonts` filtering, rescan and per-family
styles, and the `list_fonts` MCP tool.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EjTYdt76hTG4Kh2CJFnCr
As in After Effects, the Delete key and the Project panel's trash button ask before deleting
items that layers in other compositions use, saying how many layers in how many compositions go
with them (Delete / Cancel; Escape cancels). Unused items are deleted at once; agents running
project.delete directly are not asked. project.usage {items?} reports the count, sharing the
deletion's item and usage helpers. Follow-up to #72.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sysctl hw.physmem / hw.pagesize / vm free+inactive page counts, hw.model,
and freebsd-version, with a unit test for the sysctl parser. The FreeBSD
CI workflow itself landed with #69.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- effectcraft-segment: a FaceModel interface (find a face in a region, follow it; a Topology
maps the model's points to the tracker's landmarks and outline), a TensorFlow Lite reader and
interpreter (float ops, float16 weights folded; matches ai-edge-litert to ~1e-6), and
MediaPipe Face Landmarker (Google, Apache-2.0: BlazeFace + Face Mesh V2, 3.8 MB, pinned
SHA-256). The registry gains a task and authors per model; installed weights get a NOTICE.
- effectcraft-track: FaceTracker::new_with uses the model, falls back to the classical fitter
when it finds no face; chin and jaw come from the outline either way so measurements agree.
- Engine: roto_models becomes models (both tasks); face.models / face.model.* commands,
Settings ▸ Face Tracking, `wait` on select/install/download for scripts; track.mask reports
faceModel.
- Credits: ATTRIBUTION.md lists the optional models and the Apache-2.0 specs the code follows.
- Docs: README, ROADMAP (G7), gaps, parity (TRK-3 score unchanged pending G1), agents,
preferences, architecture, segment README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New L1 crate effectcraft-segment: the MaskModel interface (prompts → foreground probability),
a registry of open-licensed models (licence must be MIT/Apache/BSD/…; URL, size, SHA-256),
a PyTorch checkpoint reader (zip + pickle subset), dependency-free SHA-256, and MobileSAM
(TinyViT-5M + SAM prompt encoder / two-way transformer / mask decoder) on rayon and ndarray's
safe GEMM. Loads the official mobile_sam.pt as published; checked against candle
(embedding 0.3% RMS, 99.9% equal mask pixels); ~0.7 s/frame encoder at 32 threads, cached
per frame, ~0.1 s decoder.
- track::roto: segment_with / propagate_with prompt a model with the strokes or the flow-warped
matte (box, core points, mask); its probability is a strong, unblurred prior for the same
graph cut (strokes stay hard constraints); a failing model or one disagreeing with the flow
(IoU < 0.5) is ignored. Prior gets a weight.
- effects::roto: Version 2.0 / 3.0 use the model chosen in Settings (set_model / model_for);
the model id is part of the chain seed.
- engine: roto_models (roto.models, roto.model.select / install / download / remove), weights in
the models folder (host::config_dir()/models, shared by the app, CLI and MCP), background
loading, downloads through the system curl, verified; Settings ▸ Roto Brush page (prefs
roto.model). host::config_dir moved from the desktop app.
- Tests: segment (kernels vs direct sums, checkpoint reader, SHA-256 vectors, registry), track
(fake model: prior shapes the cut, failing / disagreeing models ignored; real weights with
EFFECTCRAFT_MOBILESAM: base IoU 0.989, propagated ≥ 0.980 vs classic 0.973), engine (registry,
verification, selection; real install + load), MCP.
- Docs: README, ROADMAP, parity (MSK-4 done: 90/2/0), preferences, agents, architecture,
crates/segment/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- audio::AudioScrub: an open output that plays a faded snippet (one frame, 30–100 ms) of the
comp's mix at each new frame, replacing what is still queued; holding still repeats nothing.
The mixing is shared with the preview feeder (mix_block); AudioFeed::clear.
- The Timeline ruler: Ctrl/Cmd-drag scrubs audio (EffectcraftApp::scrub_audio opens the output
through the audio device hook on first use). The Audio panel meters follow the snippets; an
idle second closes the output.
- playback.scrubAudio {time?} for agents.
- Tests: ui_audio_scrub (snippet length, fades, same-frame no-op, replace not pile up, silent
comps, Cmd-drag in the ruler, idle close) with a fake device. Docs: parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- commands::markers::nested_markers: a precomp layer's comp markers at their times in the
containing comp (start and stretch in closed form; with time remapping or a stretch over
protected regions, the first frame whose source time reaches them), within In–Out.
- markers.nested {layer}: the same list for agents.
- Timeline: drawn on the precomp bar as outlined read-only markers (positions cached per
revision), hover names them, double-click opens the nested comp at the marker; automation id
timeline.layer.<id>.nestedMarker.<i>.
- Tests: engine (start, stretch, trim, time remap, non-precomp error), UI (draw, label, open).
Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PanelKind::Viewer(n): more Composition viewers (panels::viewers). One is active (the full
interactive viewer, showing the active comp); the others show their own comp's frame at that
comp's current time (CPU or GPU frames, their own textures) and become active on a click or
their tab, their comp becoming the active comp.
- view.newViewer (View menu, the Composition panel menu, Alt+Shift+N): a new viewer of the
active comp next to the one in use, which is locked. Opening a comp while the active viewer
is locked shows it in an unlocked viewer, or a new one when every viewer is locked. Closing
the active viewer hands over to another with its comp. Tabs are named after their comp, with
their own lock and swatch; Preview full screen maximizes the active viewer.
- UI state: viewers (comp per viewer) and active_viewer (serde).
- Tests: ui_viewers (lock on New Viewer, routing, click to activate, all locked → new viewer,
close hand-over, tab titles; an ignored snapshot). The bounded-drag frames test asserts what
holds when freed addresses are reused. Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FrameKey's identity is the comp's content (frames::comp_content), not the project revision:
an edit keeps the cached frames of comps it doesn't touch and undo finds the frames of the
state it returns to. The identity hashes the addresses of the comps a comp draws (~0.1 ms on
the 1500-layer test project; the full content hash took ~117 ms), by value the footage, solids,
proxies and project settings; with expressions, every item. Frames::content_of keeps a
project snapshot per identity with frames (or in flight), so Arc::make_mut always copies an
edited comp and its address can't be reused meanwhile; past 64 identities the least recently
used go with their frames (long drags).
- drop_stale is gone (frames of other contents are evicted least-recently-shown first).
- disk_cache::comp_content_key covers items' proxies and Use Proxy (it served stale frames).
- Tests: frames (content keys, edits elsewhere and undo, bounded drags, in-place edit safety),
ui_ram_preview (end to end), the disk cache proxy key. Docs: roadmap (M4 done), parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- paint_logo draws docs/brand/artcraft-mark.png (the README's / getartcraft.com's ArtCraft
mark) as supplied, decoded once into a mipmapped texture, instead of a drawn placeholder.
- The "More ArtCraft apps" sibling and help.sibling named DrawCraft (storytold/drawcraft, which
doesn't exist): it is VectorCraft (storytold/vectorcraft); provenance comments in the path
crate follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Collapsed precomps: the renderer records the precomp layers a nested comp is drawn through;
motion-blur sub-samples re-evaluate their transforms (when their motion blur is on) and map
times down through each one, so the precomp layer's own motion blurs its nested layers. The
containing (outermost) comp's shutter applies to collapsed layers. The GPU compositor gets
this through Renderer::placement.
- Shape and text layers whose content changes within the shutter (compared by its cache key)
draw their source at Samples Per Frame times and average them; the cache key covers every
sub-sample's content.
- Tests: tests_motion_blur_depth (collapsed vs flat, the containing shutter, shape content vs
the same move by Position); each fails without the change.
- Docs: roadmap, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- effect.pickColor {effect, param|prop, x, y (layer px), average?}: the colour of the effect's
input at a point, set on the parameter (one undo step). Keying effects' Effect Controls
eyedroppers use it, since the shown frame is already keyed; Ctrl/Cmd+click averages 5x5.
- effects::aliases / name_matches: Effects & Presets and effect.list (MCP list_effects) find
Key Light by After Effects' name ("Keylight (1.2)").
- End-to-end test: a green-screen plate with an uneven screen, spill and soft edges keyed via
effect.apply "Keylight (1.2)", pick, Clip Black / White and a render; UI eyedropper test.
- Docs: parity, agents (green-screen recipe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- J / K (Go to Previous / Next Keyframe) and Ctrl+Alt+A (Select All Keyframes) act on what the
Timeline shows, as in After Effects: the keys of its revealed properties, layer and comp
markers and the work area (they used every property of the selected or all layers, revealed
or not). The commands take `visible: [{layer, prop}]`, which the Timeline fills in; agents
calling them without it keep the previous behaviour.
- Graph Editor ▸ Auto-Select Graph Type (an "Auto" button, on by default as in After Effects):
the speed graph when every property shown is spatial (Position, Anchor Point…), else the
value graph; choosing Value or Speed turns it off.
- Docs: parity, agents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Go to Next / Previous Keyframe (K / J) also stops at the work area's start and end, as in
After Effects.
- With one keyframe selected, the Info panel shows its property, time and value instead of
"1 keyframes selected".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keys copied from several layers and pasted onto as many selected layers go first to first,
second to second, as in After Effects; every target used to receive every copied property
(the later ones overwriting the earlier).
- Edit ▸ Paste Reversed Keyframes mirrors the clipboard over its span and pastes it through
`keys.paste`, so it now follows the same rules as Paste: the layer's time stretch, the
selected target property, Hold-only properties, value conversion and roving keys (its own
simplified paste ignored them and kept existing keys' easing on a time collision).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Double-clicking a keyframe of a numeric property (or its context menu ▸ Edit Value…) opens
the property's value dialog for that key, as in After Effects (it only moved the current
time); other keys still move the current time to them.
- The keyframe context menu has Select Equal Keyframes, Select Previous Keyframes and Select
Following Keyframes (`keys.selectEqual` / `selectPrevious` / `selectFollowing`).
- Roving keys draw as small dots in the Timeline, like After Effects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `keys.transform {fromStart}`: each step of a merged drag gives the whole transform since the
drag started and applies to the keys as they were then. Keys squeezed onto one frame on the
way (Alt-drag scaling, the transform box) are no longer merged for good, and a slow
transform-box move, whose per-frame offsets rounded to nothing, now moves the keys.
- Dragging a key in the Graph Editor moves every selected key in time and value (it moved only
the grabbed one), by how far the pointer went from where the button went down (the key no
longer jumps under the pointer); Shift keeps the drag to the time or the value axis.
- Shift+click toggles a key in or out of the selection; Ctrl+click switches Linear ↔ Auto
Bezier and Ctrl+Alt+click toggles Hold, as in the Timeline.
- The transform box and the Timeline's Alt-drag scale send whole-gesture values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A drag (keys.move steps sharing a merge key) applies its total offset to the keys as they
were before it (`Session::key_move`, `prop::shift_keys`): dragging over another key of the
property no longer deletes it; only the drop position replaces a key, as in After Effects.
- Keys of time-stretched layers move with the pointer: offsets are applied in comp time and
snapped to comp frames (drags and Alt+Arrow nudges; they moved by the layer-time distance).
- Shift+click toggles a key in or out of the selection (`keys.select {toggle}`); Ctrl+click
switches Linear ↔ Auto Bezier and Ctrl+Alt+click toggles Hold.
- Keys of locked layers can't be selected (clicks, marquee, Select All Keyframes), so nothing
edits them.
- Toggle Hold off returns an eased key to Bezier (it always restored Linear).
- Shift-dragging a key snaps to everything the current time snaps to — other keys, layer in /
out points, layer and comp markers, the work area — and a tooltip shows the key's time and how
many frames it moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every 10 seconds the UI thread re-read the system's memory for the cache budgets, and on
Windows that starts PowerShell (`Get-CimInstance Win32_OperatingSystem`) and waits for it:
about a second, sometimes two, during which the whole app froze. The reading now runs on a
background thread (`sysinfo::MemoryWatch`); `Session::memory_tick` takes the last reading and
starts the next one, never waiting. Console programs started for it no longer open a console
window (`CREATE_NO_WINDOW`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web app reopens the last visit's project and, when it had unsaved changes, marked it
modified with `bump()`. Since M3.9 the modified mark follows the saved snapshot, and a bump on
the very project just opened counts as "back at the saved state", so restored changes looked
saved (no `*`, no unsaved-changes prompt). `Session::mark_unsaved` forgets the saved snapshot
instead: the project stays modified until saved, whatever is undone. Regression test included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Following AGENTS.md's Never crash rules for the M3 / M4 additions:
- Cache Frames When Idle panicked (`clamp` with min > max) when the work area was inverted
(start set past the end); an empty work area now has nothing to cache.
- `file.newCompFromSelection`: the sequence overlap times the layer count overflowed for a huge
`overlapDuration` (wrapping to a wrong comp length without overflow checks); negative or
non-finite values fall back to 1 s and the product saturates. The lead item is read with
`get()` instead of indexing.
- Layer Settings' enablement reads the first selected layer with `first()`.
- Regression tests for both (each failed before the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>