The cache bars count the frames of `shown_series`, which rebuilds the
series from the frame key's scale in thousandths (333 -> 0.333). The key's
`opts` hashed the render options with the f64 scale, and the frames had been
rendered at 0.3333..., so the series never matched and the bars stayed empty
at Third (and any resolution that isn't a whole number of thousandths, such
as Custom 1/7).
`opts_hash` now hashes the scale rounded to the key's thousandths, which the
key carries anyway.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Window > Workspace > Save as New Workspace and Save Changes to this
Workspace kept the layout only in memory (`UiState::saved_workspaces` /
`saved_floating`): nothing wrote them, so they were gone after a restart.
They are now written to `workspaces.json` in the settings store (the config
folder on desktop, OPFS on the web) when a workspace is saved, renamed or
deleted, and read back when the app starts; a file that doesn't read is
ignored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
egui turns Ctrl/Cmd+wheel and a pinch into a zoom factor (zoom_delta)
rather than a scroll, and the Timeline only zoomed on Alt+wheel, so
both did nothing over the layers. They now zoom time around the
pointer, like Alt+wheel. (B stays Set Work Area Start as in After
Effects; Split Layer is Ctrl/Cmd+Shift+D.)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The Graph Editor's bottom bar lays out 17 text buttons in one row
(about 1100 px) on a painter clipped to the graph area, so on a
normal-width Timeline the right-hand ones (keyframe interpolation and
easing) were cut off and unreachable. Buttons that would pass the bar's
right edge now go, in order, to a » menu at its end; picking one there
runs the same action on the next frame. Hidden buttons register as
timeline.graph.more.<id> while the menu is open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
A Scale property added to Essential Graphics fell into the generic Point
control: two independent fields with no link, so dragging one value
distorted the layer, unlike After Effects, whose Scale control keeps the
chain link. It now draws the link (on by default) with one field per
axis; linked, an edit scales the others in proportion (Alt edits one),
using the Timeline's constrained() (moved to widgets). The link state is
the property's own, shared with the Timeline's chain link.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The Composition panel's bottom bar drew a hard-coded "Classic 3D"
dropdown that ignored the comp's renderer and did nothing when clicked,
so an Advanced 3D comp still read Classic 3D. It now shows the comp's
renderer (translated) and, as in After Effects, its menu switches it
through comp.settings (one undo step); the automation label is the
renderer's name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
panels/home.rs was in the converted list, but most of its text was drawn
straight from English literals (New Project, Open Project…, the page
tabs, the quick-start tiles, the filter placeholder, the column
headings, the empty states, the Opened/Kind cells and the version line),
so a Chinese interface still showed them in English. They now go through
tr()/tr_args(), with rows in both Chinese catalogs (Traditional's Join
the ArtCraft Discord row was also still English). Automation labels stay
English. The Opened column's plurals are separate rows so each language
words them itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The exr crate reads HTJ2K (OpenEXR 3.4's High-Throughput JPEG 2000) headers
but can't decompress the pixels, and the image paths turned that into a
misleading "no non-deep rgb channels" (multi-layer files) or imported the
file and showed a transparent frame (single-layer files).
`exr_channels::unsupported_compression` reads the headers; import (probe)
and frame decoding fail with "<file>: OpenEXR HTJ2K compression isn't
supported yet: re-save the file with ZIP, PIZ or DWAA compression".
Decoding HTJ2K itself needs a pure-Rust JPEG 2000 Part 15 decoder and is
left for later.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
With the Conformed Audio folder on (the default), the conform thread wrote
whatever `decode_audio` returned, and that returned silence when the source
couldn't be opened or a read failed. The conformed file is found by the
source's path, size and date, so one failed decode (an MP3 the decoder
stumbled on once) kept the footage silent in previews and exports in every
later session, while a renamed copy played.
The conform path now decodes with `try_decode_audio`, which fails instead of
returning silence; a failed conform removes its temporary file, writes
nothing and isn't retried this session (reads go to the decoder). Playback
reads still fall back to silence. CONFORM_VERSION goes to 3 so the silent
files earlier builds wrote are ignored and conformed again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
A running script reached `prefs.set` through `app.run` and could turn on
Settings > Scripting & Expressions > Allow Scripts to Write Files and Access
Network (or turn off Warn User When Executing Files), which the File, Socket
and system.callSystem gates then read: the permission the user withheld was
the script's to take.
Commands a script issues now go through `Session::execute_for_script`, which
marks the session as running for a script for the command and everything it
runs (engine.batch, ScriptUI callbacks, scheduled tasks). While it is,
`prefs.set` refuses any change that would leave the Application Scripting
settings different (compared on the resulting settings, so dotted keys,
whole-page objects and batches are all covered), and a script's
`prefs.reset` keeps them as the user set them. Command scripts read from a
file (File > Scripts > Run Script File... with .json/.jsonl) count as
scripts too. The UI, CLI, control channel and MCP still change them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Every colour input opens one picker laid out like After Effects' Color
Picker: a square and slider for the channel chosen with H / S / B /
R / G / B, the new colour over the original, HSB and RGB fields, a hex
field (#RRGGBB, RRGGBB, #RGB) and OK / Cancel. Changes apply live;
Cancel, Escape and the original swatch restore the starting colour.
The 3D light dialog, Essential Graphics and Layer Styles passed sRGB
values to egui's color_edit_button_rgb(a), which reads them as linear,
so picked colours were stored darker (#808080 became #373737). They and
the other dialogs now use the shared sRGB colour button.
The picker and colour helpers live in color_picker.rs; widgets
re-exports them.
Tests: ui_color_hex.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve Project multi-selection when dropping onto the Timeline or Composition viewer. Keep stack order and drop position, skip non-layer project items, and add pointer-drag regression coverage.