Nothing cleared a `Value::Layer` reference when its layer was deleted, so the id stayed behind:
`env_pixels` resolved it to nothing and rendered no environment at all, while the UI showed "None",
which is documented as "fall back to the comp's Environment Layer". The delete path already
unparents children and drops a track matte that pointed at a deleted layer; a layer reference now
goes the same way, in the same history step, so undo restores the layer and the reference together.
This covers every layer-reference property, not only an Environment light's Source: 73 of them
across the effects (Clone Source, Set Matte, Back Layer, Water Surface, Sky, Ground…). Keyframes
carry the same value kind as the property, as in `essential.rs`, and are fixed too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The setters go through `comp.settings`, which defaults to the Composition Settings dialog's
centre anchor, so `comp.width = 800` on a 400-wide comp shifted every unparented layer — and
all of its position keys — by 200 px. A script that resized a precomp then found its pixel
coordinates had moved under it.
After Effects' width and height setters only change the frame; moving the contents is what
"resize from centre" scripts do for themselves. The setters now pass `anchor: 0` (top left).
The dialog, which is where the anchor belongs, is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Composition Settings dialog draws 「ピクセル縦横比」 and 「フレーム縦横比」, and its checkbox reads
「縦横比を 1:1 (1.00) に固定」. Three existing rows said アスペクト比.
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The Timeline drew a layer-valued property (an Environment light's Source, a
Displacement Map's layer parameter) as read-only text holding the raw layer
id, so a light's Source could not be chosen in the UI at all - only through
prop.set from the command line, scripting or MCP. Effect Controls already
had a working drop-down for exactly these values: extract it into one shared
widgets::layer_picker (the current entry, "None" first, then "N. name" for
every layer of the comp, the list built only while the popup is open) and
draw it from both panels.
In the Timeline the pick writes through the same set closure as every other
value arm - an undoable prop.set (layer id, or null for None) - and
registers timeline.prop.<uid>.value like its neighbours, so the row names
the layer instead of exposing its id. Effect Controls keeps its rect,
automation id and write; its closed dropdown's "None", previously a
hardcoded literal, now follows the interface language. The dead "Layer {n}"
id text leaves the i18n catalogs with it.
ui_layer_source_picker drives both panels headlessly (open, pick, undo,
dismiss, current-entry highlight, translated None, picking None to clear a
set value), and an ignored wgpu snapshot test keeps the visual record
reproducible.
Every selector of a comma list cloned the whole declaration block, and every
element scanned every rule, so a sub-megabyte <style> could demand billions of
string pairs. Selectors now index one shared block, the sheets are capped at
20 000 selectors and 50 000 declarations, rules are bucketed by id/class/type
so an element checks only candidates, and matching work over all elements is
charged against a 50M-step budget that fails the import instead of doing it.
Found while testing crates/geom/src/lib.rs:362-390 (pivot check at line 367)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The search test now pins every row (layer, Contents, group, path/fill/
Transform) so a future regression that re-adds the inner Contents row or
reorders the flattening fails loudly. Docstrings cover the functions the
diff touches (group_row, all, rec, push_group) for CodeRabbit's
docstring-coverage pre-merge check.
- scriptui.js: replace the self-comparison NaN check (o.max === o.max) with
the explicit Number.isNaN(o.max) (S1764).
- sw.js: mark the fire-and-forget cache.put() with void and a .catch() so the
promise rejection is handled (S9383).
- prelude.js: return __err(...) from the activeCamera getter so every path
yields a value (S4275); __err always throws, so behavior is unchanged.
Replace Math.random() with crypto.randomUUID() when generating the unique
suffix for the atomic write-then-move temp file. The value isn't security-
sensitive, but a CSPRNG gives a stronger uniqueness guarantee and resolves
SonarCloud javascript:S2245.
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
The draft records the project epoch and comp it was opened for. A script or the
control channel opening a project or activating another comp while it is open
closed nothing before, and OK added the primitive (and switched to Advanced 3D)
in whatever comp was active then, even in a new project reusing the comp id.
FreeBSD aarch64: build with 16 codegen units and no LTO in the emulated release VM, which ran past
the job's 5-hour limit with the single-unit thin-LTO profile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OK runs layer.newPrimitive, which switches a Classic 3D comp to Advanced 3D in the same undo step. The dimension fields are the Geometry Options the engine builds for the chosen type.
The dialog offered four hard-coded light types and reopened an Environment light as Spot, so OK turned it into one. It now lists every light type the engine has, keeps the light's own type, and shows Falloff and Shadows only for lights that have them.
- `Frames::disk_frames` takes a frame range instead of a frame count, still clamped to the existing 100,000-frame limit.
- The Timeline passes the visible range (`disk_bar_range`), including frames only partly in view, and clips the blue bar to the work-area row.
- Membership stays live: nothing is cached, so the bar updates as before when frames are written, evicted or promoted to RAM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`crates/ui-egui/src/frames.rs`:
- The cache remembers the scale of the frame on screen (the active viewer's urgent request). Prefetch and passive viewers don't change it, so a second viewer at another zoom can't evict the active viewer's frames.
- RAM and GPU eviction take frames of other scales first, then fall back to the existing playhead-aware order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both devices EffectCraft creates now ask for `MemoryHints::MemoryUsage`:
- the window device (`apps/effectcraft/src/main.rs`, through a new `device_descriptor` helper);
- the headless compositor device (`crates/gpu/src/context.rs`).
The limits, labels, features and error paths are unchanged. The devices tests create stay as they were.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New `crates/project/src/file_check.rs`: a no-output `Serializer` that walks the value once. It refuses non-finite numbers and nesting past 127 levels, and it allocates only on error.
- `to_file_json_with_view` runs that check, then writes the file. It also refuses a project schema newer than this build, the remaining thing `from_json` would reject in a file we wrote.
- **Debug builds still reparse the text as well**, so a new kind of mismatch still shows up in tests.
The bytes written are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`crates/effects/src/noise3.rs` only:
- a `blend_mode(u32) -> Option<BlendMode>` helper, which the CPU and GPU paths now share;
- the CPU `render` resolves the mode once and passes `Option<BlendMode>` to the per-pixel `composite`.
The semantics are unchanged: None means the noise alone, and an unknown index means Normal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>