Commit Graph
4310 Commits
Author SHA1 Message Date
329ba63f8d Timeline: hover texts for the icon-only column headers and layer switches (#3096)
* Timeline: hover texts for the icon-only column headers and layer switches

The Timeline's column header is painted as one click target, so its
icon-only headers (A/V Features, Label, #, the eight switches and the
Modes column's T) had nothing to hover and nothing explained them.

HEADER_TIPS lists each one with a short name and an explanation in our
own words. Hover-only widgets on top of the header show the text, carry a
named ColumnHeader AccessKit node and register automation ids
(timeline.header.av.*, timeline.header.switches.*,
timeline.header.modes.preserveTransparency). The header's clicks and
right-click column menu still work underneath. The layer rows' A/V and
switch toggles and the Preserve Transparency box show the same texts.

The accessibility audit's Timeline baseline drops from 89/42 to 72/25
because the switches now have accessible names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Timeline headers: accessible description, Preserve Transparency text, English menu test

The header's node is named before its hover text is added, so screen readers get the
explanation; Preserve Transparency says the layer follows the alpha below it, partial alpha
included; the hover test sets English before checking the translated Hide This item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge main: keep both sets of timeline tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Timeline tests: close the test before the merged one

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-06 18:55:35 +01:00
FredAppsandClaude Opus 5.5 fb62b9d7e3 Project panel: take the Name column's fit out of show() (#3173)
show() went one over the cognitive complexity limit once #3094 landed on
current main, which failed clippy on every later commit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:27:42 +01:00
Valentin KleinandClaude Sonnet 5.5 519769299f Cryptomatte: the Layer popup lists the footage's Cryptomatte layers (#473) (#3020)
* Cryptomatte: the Layer popup lists the footage's Cryptomatte layers (#473)

The popup was the constant CryptoObject / CryptoMaterial / CryptoAsset. It now lists the
Cryptomatte layers of the layer's OpenEXR footage, and those three when there are none. The
stored value keeps its meaning (0-2 the standard layers, 3 and up the file's other layers in
name order), so older projects render the same.

Refs #473

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Cryptomatte Layer popup: timeline editor, one menu helper, probe and fallback tests (#473)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Cryptomatte Layer popup: give further layers of a standard kind a custom slot

A.CryptoObject keeps slot 0; B.CryptoObject and later ones get 3+ instead of
being dropped, so they can be selected, picked and rendered.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format; note same-kind layers in the footage docs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format the merged code with rustfmt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:38:13 +01:00
Valentin KleinandClaude Sonnet 5.5 fdcc715829 Viewer exposure on the full float range of 32 bpc frames (#2937)
* Viewer exposure on the full float range of 32 bpc frames (#2797)

32 bpc viewer frames on the desktop are rendered without the HDR step (skip_hdr_step,
#2916) and kept as half floats in the RAM preview. A display pass on egui-wgpu's device
turns each frame into one RGBA8 texture with mips per viewer, in this order: decode,
exposure in linear light, Compand / Tone Map, encode, display colour, Show Channel,
rounding. Before, exposure was an 8-bit lookup applied after tone mapping, clamping and
the display conversion, so -2 EV could not bring values above 1.0 back.

- engine: viewer::DisplayPass describes the pass as data; its pixel() is the CPU
  fallback (no device) and the shader's reference. It replaces exposure_lut /
  display_transform. FrameEncoding records a frame's curve and the HDR step it still
  needs. 32 bpc snapshots are taken without the HDR step; snapshot_rgba8 applies it.
- ui-egui: FrameImage::CpuF16 (half crate, F16C at run time), byte accounting at 8 bytes
  a pixel in the cache and the prefetch estimates; display_gpu.rs + display_gpu.wgsl;
  main viewer, snapshot and passive viewers draw through the pass; the 8-bit image other
  panels read is made on demand with the HDR step; the Info panel shows float values.
- Half-float frames are not written to the disk cache yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Viewer display pass: f16 disk cache entries, window-device gating, review fixes

- Disk cache: half-float frame entries (format byte in the entry header, byte planes +
  LZ4); 8-bit entries keep format 0 and stay valid. 32 bpc viewer frames are written and
  read again, and the timeline's disk bar sees them. A damaged half-float entry is
  deleted.
- The display pass runs on the window's egui-wgpu device, with or without the GPU
  compositor (still building, failed, GL fallback). Its pipelines are made inside error
  scopes; a backend that rejects them gets the CPU pass instead of a blank viewer.
- A new LUT of the same size (another ICC profile, Preserve RGB, another display) runs
  the pass again.
- f32 to f16: one pass per L1-sized block, the NaN / infinity fix-up on the bits, no
  fill of the buffer (4K: 4.7 ms, as the 8-bit conversion).
- Mips of odd-sized levels include the last column / row; the snapshot's half floats
  and texture go with the snapshot; no placeholder texture ids; u64 size checks.
- Tests: LUT profiles and negative values against the shader, Bake reruns, the passive
  viewer's colour, f16 disk round trips; exact comparisons allow 1 LSB on software
  rasterizers. Docs: the display pass and the f16 disk entries in architecture.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Viewer f16 disk-cache test: temp dir removed on failure too, wait for the queued disk write

Review points: the test's directory was only removed by its last statement
(now tempfile::TempDir), and the frame is released before its disk write is
queued, so flush() could return early (now waits for the entry).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Viewer: oversized half-float frames use the CPU display pass, so exposure/HDR come before the averaging down

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Viewer: fmt and a debug assert for the half-float upload path

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Disk cache: persist_missing also writes half-float frames

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Passive viewer: oversized half-float frames take the CPU display pass instead of leaving a stale texture

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:37:23 +01:00
Valentin KleinandClaude Opus 5.5 8948a7cdc9 Masks: set the corner roundness of a Rounded Rectangle mask (#3077)
* Masks: set the corner roundness of a Rounded Rectangle mask (#3067)

layer.addMask takes a roundness (corner radius in px) for shape rounded, and the new
mask.setRoundness changes it afterwards on a rectangular mask.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Mask roundness: refuse ellipses, keep feather points and key correspondence

mask.setRoundness accepted any closed path whose vertices lie on its bounding
box, so an ellipse was replaced by a rectangle. It now accepts only a plain
rectangle (four corner vertices, straight sides) or a rounded one (eight
vertices, straight sides alternating with circular corners).

The rebuilt path keeps the old start vertex and direction, and its feather
points, so an animated path's other keys and the feather points still line
up. A change between square and rounded corners changes the vertex count; it
is refused when the path is keyed or has feather points instead of leaving
keys with different counts or dropping the points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:37:17 +01:00
Valentin KleinandClaude Opus 5.5 be8b5fd8d4 Layer: Quick Set Anchor, nine anchor points with Position compensation (#3080)
* Layer: Quick Set Anchor, nine anchor points with Position compensation (#2967)

layer.quickSetAnchor moves the Anchor Point of 2D layers to a corner, edge midpoint or the
centre of their content and shifts Position and its keys so nothing moves. Layer > Transform >
Quick Set Anchor lists the nine points.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Quick Set Anchor: skip layers with an animated Anchor Point, Scale or Rotation

The anchor is set at the current time and the Position shift is worked out
from the current Anchor Point, Scale and Rotation, so on a layer where any of
them is keyed or has an expression the layer would move at other frames.
Such layers are skipped like 3D and locked ones (an error when none is left).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Quick Set Anchor: skip layers with a Position expression or Auto-Orient along the path

An expression on Position (or the separated X/Y) overrides the shifted value,
and Auto-Orient along the path adds a rotation the shift does not account for;
both would make the layer jump, so they are skipped like 3D layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Quick Set Anchor: error when no selected layer has content bounds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Quick Set Anchor: test separated, keyed Position keeps the layer in place

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:37:11 +01:00
Valentin KleinandClaude Sonnet 5.5 9a32a51793 Viewer: Alt-drag duplicates the dragged layers (#3076)
* Viewer: Alt-drag duplicates the dragged layers (#2968)

Alt/Option-drag of the selected layers in the Composition viewer duplicates them and drags
the copies; the copy and the drag are one undo step. edit.duplicate takes a merge key for it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Alt-drag: duplicate from a 3D gizmo arrow, no double move of a selected child

Alt-dragging a gizmo arrow moved the original; it now duplicates and moves the
copy along the axis. A selected layer whose parent is dragged too follows the
parent, so its own Position no longer gets the drag a second time (with or
without Alt).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:37:05 +01:00
Valentin KleinandClaude Opus 5.5 bb920d68ec Project panel: the Name column fits long names and resizes (#3094)
* Project panel: the Name column fits long names and resizes

The Name column was 40 % of the panel and at most 260 points wide, with
no way to resize it, so long item names (compositions of an imported
After Effects project) ended in "..." even in a wide panel.

It now fits the longest listed name, never narrower than before and at
most the panel's width less the Label column (the optional columns keep
scrolling under it). Dragging the column's edge in the header resizes
it, a double-click on the edge fits the names again. The width is kept
in UiState::project_name_w (None = fit the names); the edge has the
automation id project.nameColumn.edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Project panel: the Name column leaves room for the optional columns; no name fit for a manual width

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:59 +01:00
Valentin KleinandClaude Sonnet 5.5 43a47a5d6a Timeline: one selection box spans the layer list and the time graph (#3075)
* Timeline: one selection box spans the layer list and the time graph (#3056)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Timeline box test: say what it tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Timeline box: a box kept in the time graph selects keys only

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format the timeline box test and box_targets signature

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Timeline box test: drive the real drag across the outline and the time graph

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:36:54 +01:00
Valentin KleinandClaude Opus 5.5 7051fc1f58 Color Stabilizer and Linear Wipe: match After Effects (sample positions, feather) (#3135)
* Color Stabilizer: sample the reference where the points were at the reference time (#3125)

After Effects 2026 samples the reference frame at the positions the
Black / Mid / White Points had at the reference time. With keyframed
points it therefore corrects even a static picture: our importer sample
(colstab-43-keys, Levels, Black Point (64, 90) -> (151, 99) over 1 s)
renders at 0.5 s as the Levels map out = a + b*v of the ramp with
b 1.2935..1.2965 and a -0.2365..-0.2350, which is the Black Point's
current sample (107.5, 94.5) sent to the reference sample at (64, 90).

EffectCraft sampled both frames at the current points, so the card
stayed unchanged (slope 1). The samples now read the points at the
reference time through the host (params_at), falling back to the
current points without one. CPU and GPU share the sampler
(color_stabilizer_maps).

Sample Size's unit is not pinned by the samples (every size 1..40 leaves
the card unchanged in After Effects; that only bounds the sampled area)
and is left as it is.

Part of #3125

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Linear Wipe: After Effects' edge position and feather profile (#3125)

Measured on our importer samples (linwipe-*, 2026-11-05, After Effects
2026, 320 x 180):

- The edge sits at lo + completion * (hi - lo) whatever the feather (the
  set-all render's 50 % line is at -5.25, predicted -5.266); 0 % keeps
  the layer and 100 % clears it even with Feather 32000. EffectCraft
  extended the travel by the feather, 13 px off at Feather 16.
- The feathered edge is the hard matte blurred with the layer's edges
  repeated: three box passes of radius 6 per axis fit Feather 16
  (sigma 6.35..6.6, taken as Feather / 2.5), including the extra
  opacity where the edge meets the layer's left side. EffectCraft drew
  a linear ramp Feather px wide.
- Without feather the edge pixel is anti-aliased (143/255; coverage
  gives 0.6). EffectCraft drew a hard step.

set-all alpha: before max 195, 4646 px (8.1 %) off by more than 2/255;
after max 15, 529 px (0.9 %), all along the layer's left side (at most
2 elsewhere). Feather 0 renders: max 112 -> 10 on the edge column.

CPU: coverage matte, raster::gaussian_blur (3 box passes, edges
repeated); GPU: the same matte from gen_linear_wipe (new modes), its
gaussian_blur, then a multiply, so both match. Sigma is capped at twice
the layer's longer side; NaN values leave the layer as it is.

Projects saved before keep the old ramp: a hidden legacyFeather
parameter that migrate::legacy_default sets on upgrade (as for Ripple
and Block Dissolve).

Part of #3125

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Linear Wipe on the GPU: 100 % clears the layer before the angle is read

A NaN angle left the layer unchanged on the GPU while the CPU cleared it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:48 +01:00
Valentin KleinandClaude Opus 5.5 93cce09f3c .aep import: name the After Effects version in the report; 25.x and 24.x copies import like 2026 (#3136)
* AEP import: the report names the After Effects version of the file (#3115)

svap holds the version of the After Effects that wrote a project, head
bytes 4 to 7 the version the file is written for; Save a Copy As 25.x /
24.x of After Effects 2026 keeps svap and writes the older version into
head. AeVersion decodes the packed word (26.2.1x2), AepFile gains
file_version, saved_by_version and head_revision, and ImportReport gains
aeVersion (fileVersion, savedBy, tested; "unknown" when the file does not
say). A version outside 24 to 26 is imported as usual and gets a
limitation line, not a loss.

The 12 corpus families saved as 2026, 25.x and 24.x copies import to the
same project and report; docs/aep-format.md lists what the copies change
in the container. The fixture tests are ignored until the corpus holds
the files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: doc comments on the version tests and helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import docs: tested is a major-version classification from copies saved by After Effects 2026

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import docs: the importer reports detected losses, not every loss

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* aep tests: compare the imported settings across version copies too

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:41 +01:00
Valentin KleinandClaude Opus 5.5 ab3674cc2b .aep import: map HDR Highlight Compression, Echo, Linear Wipe, Offset and Color Stabilizer (#3119)
* AEP import: map HDR Highlight Compression, Echo, Linear Wipe, Offset and Color Stabilizer

The importer reported these five effects as lost. They now become
EffectCraft's effects of the same name with every parameter After Effects
stores (ranges, defaults and popup items from our own After Effects 2026
project, documented in docs/aep-format.md 3.7):

- ADBE HDR ToneMap -> ec.utility.hdrcompression (Amount)
- ADBE Echo -> ec.time.echo (Echo Time, Number Of Echoes, Starting
  Intensity, Decay, Echo Operator by item name)
- ADBE Linear Wipe -> ec.transition.linearwipe
- ADBE Offset -> ec.distort.offset (Blend With Original is a stored 0..1
  fraction, a percentage here)
- ADBE Deflicker -> ec.color.colorstabilizer (fresh Stabilize default
  Levels, points, Sample Size)

A table row can name the range EffectCraft's effect works in
(ParamMap::limit): Echo values or keys beyond 255 echoes or a Decay of 1
are imported as stored and reported.

Tests build each effect into the committed synthetic.aep (fx_inject.rs,
which also checks its pard layout reads back). On the sample project the
effect losses go from 75 to 3 (the out-of-range Echo values).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: doc comments on the quick-win test helpers and tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: Echo keyframe regression for the range loss

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:34 +01:00
Valentin KleinandClaude Opus 5.5 fee50dbfd1 Scripting: After Effects match names for 14 effects and Casts Shadows (#3122)
* #3113: use After Effects' effect match names in scripting

Checked every row of crates/script/src/matchnames.rs against After Effects
26.2 (app.effects, canAddProperty and the layer property trees). Fourteen
effect names were wrong: twelve did not exist (Pixel Motion Blur is
ADBE OFMotionBlur, Shadow/Highlight is ADBE ShadowHighlight, Ellipse is
ADBE ELLIPSE, ...), and Noise and Photo Filter pointed at After Effects'
hidden obsolete versions (now ADBE Noise2 and ADBE PhotoFilterPS). A
light's Casts Shadows is ADBE Casts Shadows.

The old names stay accepted for lookup (addProperty, property(), and
parameters such as "ADBE Noise-0001"), so existing scripts keep working;
.matchName emits only the After Effects name. A names-only fixture of
After Effects' first-party match names backs the new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Scripting: match names that changed only in case still find effects and parameters

property("ADBE Ellipse") and "ADBE Grow Bounds-0001" found nothing after the names became
ADBE ELLIPSE / ADBE GROW BOUNDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:28 +01:00
Valentin KleinandClaude Sonnet 5.5 9408d70da0 Disk cache: stamp footage files for the salt off the UI thread (#3106)
* Disk cache: stamp footage files for the salt off the UI thread

Importing an After Effects project whose footage includes long image
sequences on a network share froze the desktop app for 15-27 s. The
import itself takes about 0.25 s; the time went into the edit's bump(),
where refresh_disk_salt() stats every footage file (13,466 sequence
frames in the measured project, ~15 s) on the UI thread whenever a disk
cache is attached. File > Open and footage reload hit the same path.

With Session::disk_salt_background (set by the desktop app) the salt is
taken on a worker thread and picked up by poll_disk_salt() from the UI
tick. Until it lands the layer cache runs memory-only, so keys under the
old salt (other files, or another project's items) are never read. A
newer footage change supersedes a salt still being taken. Headless
sessions keep taking it synchronously.

A settings change that keeps the disk cache folder no longer re-stamps
every footage file either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: opening a project drops the footage salt being taken

A salt job still running for the old project was kept when the new project named the
same footage, so the salt stamped before the files changed stayed in use.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:19:27 +01:00
Valentin KleinandClaude Opus 5.5 9fc73122ea Home screen: leave it when a menu command opens a composition (#3092)
* Home screen: leave it when a menu command opens a composition

File > Import > After Effects Project... from the Home screen imported the
project (the toast said so), but the Home screen stayed on top, so it
looked as if the import had done nothing. Only the Home screen's own
buttons left it.

run_engine now leaves the Home screen when a command succeeds and changes
the active composition to one that is open, the rule scheduled script
tasks already follow. Frontend events run afterwards, so Help > In-App
Tutorials and New Project from Template still show it. A command that
opens no composition (or fails) keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Home screen: also leave it for a project that replaces one with the same composition id, a picked file and the New Composition dialog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:19:21 +01:00
Valentin KleinandClaude Sonnet 5.5 549a4d0540 Import: EDL clips without a SOURCE FILE link the file beside the list (#2836) (#3019)
* interchange: probe EDL media the reader flags offline (#2836)

An EDL line without a SOURCE FILE comment only names its clip. The reader
guesses the path beside the list and flags the media offline, and the
importer never probed offline media, so the file next to the EDL stayed
missing. Offline media is probed like the rest; a path that is not there
still becomes a placeholder.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* interchange: probe guessed paths of EDL media only (#2836)

An OTIO missing reference also gets a path built from the clip name and
is flagged offline, so a same-named file beside the document would have
replaced the intended clip silently. Only EDL imports probe offline media.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* interchange: confine guessed EDL paths to the list's folder before probing (review point)

CodeRabbit: an EDL clip without SOURCE FILE gets a path from its clip name; a
name like ../x.mov or an absolute one could link a file outside the EDL folder,
and rebase_to_file_timecode probed it too. Such guessed paths are now not
probed and stay placeholders; SOURCE FILE paths are kept as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:19:14 +01:00
Valentin KleinandClaude Sonnet 5.5 ae068ff931 EXR export: write the output space's chromaticities attribute (#3038)
* EXR export: write the output space's chromaticities attribute (#3037)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* EXR chromaticities: tests for unmanaged and PQ ProEXR, every space once

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* EXR export: note why the comp background is added before decoding (review point)

CodeRabbit asked to convert Comp.background to the output space before
compositing. Every output path (Image::to_rgba8_over) adds the background to
the output-encoded frame as it is, so the EXR now matches the other outputs;
converting would make the EXR colour differ from the PNG of the same comp.
Comment only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format the merged code with rustfmt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:19:05 +01:00
Valentin KleinandClaude Sonnet 5.5 ee7c51740d effects: Write-on and Stroke Brush Size goes up to 200 px (#2980) (#3021)
After Effects 2026 accepts 0 to 200 for Brush Size of both effects (a
script setting 201 is refused with "out of range 0 to 200"); the range
here ended at 50. The slider keeps its 0 to 50 travel; larger values are
typed.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:18:58 +01:00
Valentin KleinandClaude Opus 5.5 0f94a1f983 Skip the startup update check when started for an agent (#3097)
* Skip the startup update check when started for an agent

An app started with --control or EFFECTCRAFT_CONTROL_PORT still checked
for updates at start, and the modal "newer version available" prompt
blocked agent runs until a person dismissed it. Agent launches already
skip crash recovery and the Home screen; now they skip the startup update
check too. Help > Check for Updates is unaffected (it goes through
updates_ui::check / check_requested, independent of the startup flag).

Documents what a control-port launch skips in docs/control-protocol.md and
the main.rs module doc, with a unit test for the startup decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Control protocol docs: a window-start failure still shows a blocking message box

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:18:52 +01:00
Valentin KleinandClaude Sonnet 5.5 779df34e13 contributors: set names for vangel76 (#3041)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:18:46 +01:00
FredApps 8be83ab559 Merge pull request #3162 from storytold/house-rules
Copy the shared house rules into AGENTS.md
2026-11-06 14:59:41 +01:00
FredApps 357c72ea33 Copy the shared house rules into AGENTS.md.
The blocks come from craft-contrib at dde09ea31e2e457f5b4653a6e76f7cdf55944bdf. CLAUDE.md imports AGENTS.md. The house-rules workflow checks the copy.
2026-11-06 11:50:01 +01:00
ash 7fc1be0bf2 Report offline footage and recover automatically when media returns (#3161)
* 3118: report and recover offline footage consistently

* 3118: preserve manual reload and refresh data watch paths

* 3118: document availability and reload contracts
2026-11-06 11:38:57 +01:00
Valentin Klein ce9eaec650 Add After Effects project export (#2594)
Add After Effects project export with chunks built in code, explicit loss reporting, engine and UI integration, and regression coverage.
2026-11-06 09:23:16 +01:00
ash bc4b96a5fc Reveal newly created mask controls in the Timeline (#3149)
* 3145: Reveal newly created mask controls in the Timeline

* 3145: Document mask reveal contracts and regression cases

* 3145: Make stale-event regression cases exhaustive
2026-11-06 08:53:04 +01:00
FredApps 3102b6e632 Merge pull request #2915 from vangel76/perf/exr-output-options
Render Queue OpenEXR: choose compression and half or 32-bit float
2026-11-06 02:26:23 +01:00
FredApps e2010893d2 Merge pull request #3130 from vangel76/cryptomatte-matte-only
Cryptomatte: Matte Only puts the matte in RGB with alpha 1, as After Effects does
2026-11-06 02:26:11 +01:00
vangel76andClaude Opus 5.5 4934de892d Merge upstream/main into cryptomatte-matte-only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:53:40 +01:00
vangel76andClaude Sonnet 5.5 f9b860838c Merge upstream/main into perf/exr-output-options
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 00:49:29 +01:00
FredApps 3768ff40a3 Merge pull request #3134 from vangel76/aep-tdsb3-values
.aep import: read effect values stored as default-state records
2026-11-06 00:20:44 +01:00
FredApps d1e6393bce Merge pull request #3133 from vangel76/fx-hdr-compression
HDR Highlight Compression: match After Effects' compression curve
2026-11-06 00:17:21 +01:00
FredApps 8b63d4308c Merge pull request #2916 from vangel76/fix/exr-scene-linear-output
EXR export: no compand / tone mapping, and the output space's own curve (#2913)
2026-11-06 00:12:32 +01:00
FredApps 39af86e9f4 Merge pull request #2929 from vangel76/perf/aux-footage-hint-474
perf(media): tell the media pool which EXR footage effects read (#474)
2026-11-06 00:12:06 +01:00
FredApps 35ce9b2694 Merge pull request #2920 from vangel76/perf/exr-decode-cache-474
Multi-layer EXR: one decode per frame shared by the picture and EXtractoR / Cryptomatte, picture-only decode for footage alone (#474)
2026-11-06 00:10:05 +01:00
4a89e68f9f Content-Aware Fill: spill the range and stream the output (#2570 phases 2-4) (#3079)
* Content-Aware Fill: bit-pack holes, spill frames to disk, stream outputs (#2570 phases 2-4)

* Content-Aware Fill phases 2-4: bit-pack holes, spill frames, stream outputs (clippy clean)

* Address CodeRabbit review: spill safety, streaming correctness, perf, memory guard (#2570)

* Address the remaining CodeRabbit review findings on Content-Aware Fill (#2570)

A non-streaming fill returned empty frames, progress could jump backwards between
the sink and the fill, the memory guard ignored the membrane frames still resident
for the flow pass (and wasm's in-RAM copies), and the GPU fill stages ran outside
an error scope, so an OOM retired the shared context instead of falling back to
the CPU for that frame.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Content-Aware Fill: sample spilled frames without decoding them, check spill errors (#2570)

Tracing sampled spilled frames by decoding whole frames under the shared cache lock, so
parallel traces across neighbouring frames evicted each other and mostly paid a full-frame
read while every other thread waited. Sample only the 2x2 bilinear support with positioned
reads on an open handle instead, and report a failed spill read in the no-hole branch
rather than streaming black frames as a successful fill.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Content-Aware Fill: spill membrane/propagated frames, unquantized input, surface scratch errors (#2570)

Flow and propagation now write their intermediate frames to scratch spills beside
the output instead of holding the whole range as float RGBA, and the input spill
uses 32-bit floats whatever the project depth so the fill sees the same values as
the old in-memory path. Spill reads/writes and scratch creation report through a
shared error slot that run_plan surfaces instead of masking disk failures as a
cancellation. Sampling uses positioned reads; the guard models the packed masks
plus per-thread scratch.

* Content-Aware Fill: report a membrane-spill write failure instead of cancelling (#2570)

* Content-Aware Fill: include the flow fields in the memory guard (#2570)

* Content-Aware Fill: scale the memory guard with workers and check scratch space (#2570)

The guard's scratch term was a fixed 8 frames, which under-counted a wide
machine with Lighting Correction by several times. It now models the sequential
synthesis stage plus a few frames per worker (from available parallelism) and
takes Lighting Correction into account.

Native fills write three f32 copies of the range to scratch beside the output,
so a range that cannot fit on that volume is now refused up front with the
free space in the message, before any frame is rendered.

Tests cover the worker/lighting budget, the scratch formula, a shared error slot
between the input and secondary spills, an uncreatable spill folder, and the
engine surfacing a spill failure as an error instead of a cancellation.

* Content-Aware Fill: resolve a relative output folder for the scratch check (#2570)

A bare relative output name has an empty parent, so the walk-up found no
existing ancestor and skipped the free-space check. Resolve it against the
process directory first, and cover that in the test.

* Do not reformat the GPU fill tests in the spill branch (#2570)

The file was already not rustfmt-clean upstream, so the formatter only added
noise to a file this PR no longer needs to touch: phase 1's call-site changes
are already on the base branch.

* Content-Aware Fill: measure peak RSS and correct the per-worker scratch model (#2570)

The ignored measure_peak_rss_of_a_fill test fills a spilled 640x360 range and
reports VmHWM. On this 18-worker machine 90 frames peak at 340-470 MB across
runs (35 MB with RAYON_NUM_THREADS=1) and 600 frames at 582 MB, so the scratch
slope is ~7 frames a worker, not the 2 the first guard revision assumed.
Scaling the summary down showed a clean linear slope at 8 and 4 workers, and
Lighting Correction stayed below the flow stage's peak.

Budget 8 frames a worker (9 with Lighting Correction), just above the measured
top of the range, and adjust the guard test to the real numbers. At 600 frames
the measured peak is 582 MB against ~11.1 GB for the old five-copies model, a
19x reduction.

* Contributors: credit Madhav Agarwal in the About window

Adds the self-chosen real and display name for @gmrmad1. The generated
contributors.json picks it up once the PR is merged.

* Address CodeRabbit: size the guard from the rayon pool; migrate GPU tests (#2570)

fill_workers used available_parallelism, but the parallel fill stages run on
the rayon pool, which RAYON_NUM_THREADS can size larger. Use
rayon::current_num_threads so the scratch budget follows the threads that
actually run; wasm keeps rayon's single-thread fallback.

The GPU inpaint tests still used the pre-spill FillInput API. The earlier
'do not reformat' revert dropped their API migration along with the
formatting, leaving cargo test -p effectcraft-gpu uncompilable. Restore the
Frames::Memory / HoleMask / sink migration and verify the 15 inpaint tests.

* Fix gate: flatten the wasm prop lookup and keep the budget wrapper test-only (#2570)

* Address CodeRabbit review: spill error check, global-pool preflight, docs (#2570)

* Include RAM-backed spill in the preflight and guard streaming reads (#2570)

* Resolve relative spill paths, prefer stacked mounts, guard the Edge Blend sink (#2570)

* Make the fill spill anonymous and test the streaming claims (#2570)

The spill was a named file in the output folder that only Drop removed, so
an out-of-memory kill or a force-quit left a multi-gigabyte
effectcraft-fill-* file in the user's project folder, and a failed set_len
after keep() leaked it too. The spill is now an anonymous temporary file
(tempfile_in), which the system reclaims when its last handle closes; every
read and write is positioned on that one handle (pwrite/seek_write join the
existing pread/seek_read), and the folder is kept only so the propagated
frames spill beside the input.

Also:
- Edge Blend checks for a failed read before handing a frame to the sink,
  so a black fallback frame never streams.
- The propagation workers stop as soon as the spill records an error.
- HoleMask::get/set use get()/get_mut(), so a mask whose public words are
  shorter than len cannot panic.

New tests cover the PR's headline claims: a spilled range fills
bit-identically to the same frames in RAM (returned and streamed), the sink
sees frames strictly in order, a truncated spill fails with a recorded
error and streams nothing, a failure mid-stream stops before the next frame,
propagation stops after a failure, the spill has no name in its folder, and
HoleMask round-trips at word boundaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Content-Aware Fill: clean up a stopped fill and count two scratch copies (#2570)

- A fill that fails or is cancelled after frames started streaming deletes
  the PNGs it wrote, and the output folder when the fill made it and it is
  left empty (OutputFiles, dropped after the spill so Windows can remove
  the folder).
- After a successful fill, run_plan checks that every frame has a file, and
  apply_plan/add_fill_layer return an error on an empty range or file list
  (a job worker's reply) instead of indexing [0].
- scratch_need charges two f32 copies (32 B/px): flow_pairs drops the
  membrane spill before the propagated one is created, so the third copy
  never coexists.
- The spill-creation error carries a stable "spill: cannot make the scratch
  file" marker; the panel test asserted on the OS wording, which on Windows
  is "Cannot create a file when that file already exists".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Content-Aware Fill: a stopped fill leaves files it didn't create (#2570)

A fill into a given outputDir that already holds an earlier sequence
overwrites its frames as it goes (as before). The cleanup of a failed or
stopped fill deleted every path it wrote, including those earlier frames, so
a layer still using that sequence lost them. It now deletes only the files
the fill created (CodeRabbit on #3079).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:51:11 +01:00
skylar taylor-barrick a408d990b2 Export: use conformant ProRes alpha run termination (#2225)
* #293: use conformant ProRes alpha run termination

Pin the direct ProRes encoder to FilmCraft e4be1577 while preserving all other codec pins. Add a full MOV export regression for final alpha runs across both 4444 profiles, alpha modes and blur settings.

* #293: document ProRes alpha export regression

* export: repin FilmCraft to the merged ProRes alpha fix and move the round-trip regression into roundtrip.rs

All FilmCraft deps pinned to d5ead4ba (squash merge of filmcraft #2991). H.264 pinned movie bytes move 104341 -> 104342: only the encoder version string in the SEI (0.8.0 -> 0.12.0) and five size fields change.
2026-11-05 22:45:23 +01:00
MarekSpodBiedryandskylar 7ce61c68fc Grow the buffer to the pinned quad so Corner Pin draws outside the layer rectangle (#1938) (#1967)
* Grow the buffer to the pinned quad so Corner Pin draws outside the layer rectangle (#1938)

* Corner Pin: grow through pad_sides, skip adjustment layers, clamp to the pixel budget (#1938)

grow_sides returns the four sides for Buf::pad_sides on both paths, replacing grow_to/regrow and the inline GPU copy. Growth is skipped on adjustment layers, cut proportionally to the shared pixel budget (and 16384 per side) instead of giving up, and covered by tests for a far pin, an adjustment layer and a degenerate quad.

* Corner Pin: give the grow_sides test its own doc comment (#1938)

* effects: Corner Pin keeps a finite buffer for non-finite pins and grows without popping as a pin moves far away

* effects: rustfmt the Corner Pin non-finite pin test

---------

Co-authored-by: skylar <hi@skylar.cc>
2026-11-05 21:52:10 +01:00
vangel76andClaude Opus 5.5 0a103c055c HDR Highlight Compression test: 20 PNG codes up to 1, 0.1 % above
The reference test now checks the bound the PR states in the code space of the renders
(the sRGB curve x 6553.5) instead of a linear 2e-3, which allowed about 28 codes at the dark end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:31:17 +01:00
vangel76andClaude Opus 5.5 e25025545a AEP import: an effect record is read only in the written or the default state
A record in another tdsb state is reported as damaged data again, as before the
default-state change. The synthetic test checks Levels' recorded popup directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:28:55 +01:00
vangel76andClaude Sonnet 5.5 ab6e76973a ProEXR test: a non-sRGB output curve (Rec. 709) decodes with its own curve
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 18:47:54 +01:00
vangel76andClaude Opus 5.5 3a9188e0fb HDR Highlight Compression on the GPU: a NaN Amount keeps the layer, as on the CPU
Doc comments on the touched helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:11:29 +01:00
vangel76andClaude Opus 5.5 af7044d7db HDR Highlight Compression: check the curve at Amount 25, 50 and 75 (#3123)
New After Effects 2026 renders of the same EXR at Amount 25, 50 and 75
(our own 1110-extra sample, 16-bit PNGs) confirm the transfer curve that
was fitted at 84, 90 and 100. The worst channel error over all pixels is
4.0, 5.5 and 4.3 of 65535, against 997, 2415 and 3867 for the old soft
knee. The code is unchanged.

The measured-reference test now covers all six Amounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:39:00 +01:00
vangel76andClaude Opus 5.5 b9182972b2 HDR Highlight Compression: match After Effects' measured transfer curve (#3123)
The effect used a soft knee on the brightest channel and left everything
below the knee unchanged. After Effects compresses the whole range. Fitting
our own 16-bit renders of an HDR ramp and coloured patches (Amount 0, 84,
90, 100) gives:

  Y    = max(0.30 R + 0.59 G + 0.11 B, 0)
  comp = min(c / (1 + Y), 1)                    per channel
  out  = ((1 - t) c^0.4 + t comp^0.4)^(1/0.4)   t = Amount / 100

A grey value x maps to x / (1 + x) at Amount 100. The worst error against AE
drops from 985 to about 2-4 of 65535 at Amounts 100, 90 and 84. Amount 0
stays identical.

CPU and GPU change together. Tests: a reference table of measured values,
GPU parity at Amount 90, and over-range input that exercises the clip at 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:02:26 +01:00
vangel76andClaude Opus 5.5 e730da1435 AEP import: read effect values recorded in the default state (tdsb 3)
After Effects records some effect parameters as value records in the
default state (tdsb 3) that still hold the value it uses. A survey of 839
corpus and sample files finds 25 such effect records, all in Lumetri
Color (its hidden controls) and Cryptomatte (Selection Mode on); every one
differs from its pard default and reads back in After Effects as recorded.
No effect record in the default state holds a value that does not count.

The general reader now takes them: EffectParam::recorded_value (the same
name, signature and body as in the Lumetri import) reads a record whatever
its state, value() and is_default() use it, and the importer's mapped
parameters read it, so such a record imports like a written one instead of
being reported as an unreadable value with the default used.
stored_value keeps to written records.

Layer, camera, light, shape and text properties in the default state all
hold After Effects' defaults or values with no effect on that layer
(the point light cone, a polygon's inner radius, a camera's opacity), so
their readers are unchanged. docs/aep-format.md records both findings.

Closes #3126

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:51:53 +01:00
vangel76andClaude Opus 5.5 50cbbfe310 Cryptomatte: Matte Only puts the matte in RGB over alpha 1 (#3127)
With Matte Only on, Cryptomatte wrote the matte into all four channels:
a premultiplied white only as opaque as the matte. After Effects'
Cryptomatte plug-in shows Matte Only as an opaque grey picture, the
matte in RGB with alpha 1 (its Matte Only render of 2026-11-05 is opaque
black where nothing is selected). The CPU effect and the GPU shader now
both give (m, m, m, 1).

Projects saved before keep their render: a hidden Matte Only In Alpha
switch (off for new instances, presets and AEP imports) is turned on by
migrate::legacy_default when an older instance is upgraded on open, and
then keeps the matte in every channel on CPU and GPU.

Tests: the CPU path for a partly covered pixel, an unselected one and
outside the aux area, in both forms; the upgrade of a saved instance;
the GPU parity test over every display with Matte Only off, on and on
with the legacy switch; the host pick tests read whole pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:38:36 +01:00
1786c24523 expr: an expression cannot rebind a prelude name for the next one (#2133) (#2176)
* expr: an expression cannot rebind a prelude name for the next one (#2133)

One runtime serves every expression on a thread and user code runs as a script
at global scope, so `linear = function () {}` rebound the name for every later
expression, and for every later project in the session. #1387 closed the `__`
globals, the model constructors and the prelude's own prototypes; the rest of
the global surface is here: the prelude's own 104 helpers, the built-ins that an
expression and the prelude both call, and the prototypes those hang off.

Freezing a prototype costs an expression the right to shadow an inherited name
on its own object: once the inherited data property is non-writable, `o.x = …`
is dropped in sloppy mode rather than creating an own property, which is the
override mistake. Every name that was shadowable before is therefore replaced
by an accessor whose setter writes an own property on the receiver, emulating
OrdinarySetWithOwnDescriptor so the substitution does not show through.

The repair covers every own writable, configurable data property of every
prototype the block reaches, not a list of commonly shadowed names: a list
leaves `a.map = …` throwing and `a.push = …` and `a.join = …` silently running
the native method, which is worse, because the expression keeps working and
quietly computes something else.

`name` and `message` on an error's prototype are the one exception, and they
are frozen rather than repaired. The host renders a thrown error by reading
both off the object with no context to call a getter with, and nowhere safe to
call one: the time budget is checked between polls of the evaluation and
formatting runs after that returns, so a getter of the expression's own would
run outside it, bounded only by the loop and recursion limits at some hundreds
of millions of iterations. `e.name = 'MyError'` is dropped in silence as the
price of keeping user code out of the failure path.

The setup code reads every intrinsic it needs before the first prototype is
rewritten, and indexes arrays rather than calling their methods. Repairing
`Array.prototype` turns `concat` and `forEach` into accessors, and an earlier
attempt at this used both while walking the remaining globals, which left
every prototype after `Array.prototype` unfrozen and the prelude failing to
load at all.

Measured on macOS arm64, release, min of 3 x N, against this base: an
inherited-name read costs ~68 ns where it is now an accessor, which is +18%
on a loop doing a hundred of them and +2% to +7% on ordinary expressions.
Extending the repair past `Object.prototype` is what the array-method read
pays for; nothing else moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* expr: the prelude's lock answers delete and defineProperty too (#2133)

Tests for the two routes to a global binding the existing ones did not cover
against a prelude function, asked for in review. `writable: false` answers an
assignment and a `var`; it is `configurable: false` that answers these, and
they fail differently: a `delete` of a non-configurable property evaluates to
false in sloppy mode, while a `defineProperty` over one throws, so that is the
one route whose refusal the user sees.

Also records which mode decides that, because it was asked and the answer is
not the obvious one: an expression cannot reach strict mode at all.
`Runtime::script` wraps the text in `try{…}catch(__e){throw __e}`, so a
`"use strict"` the user writes is an expression statement inside a block
rather than a directive prologue and has no effect. A refused write is
therefore always the silent kind, never a TypeError.

No change to the prelude; these pass as it stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* expr: freeze Math and JSON plainly instead of repairing them (#2176)

Math and JSON are namespaces, not prototypes: nothing inherits sin or
parse, so the accessor repair keeps no shadowing there and only puts a
getter call on every Math.* read of a per-frame expression. Freeze them
directly and lock the two bindings so Math = {} cannot replace them for
the next expression either. Prototypes keep the repair.

---------

Co-authored-by: Anubhav-Rai <adobearakap@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 15:21:59 +01:00
FredApps b2eb3f7877 Merge pull request #3091 from ZIDAN44/fix/1826-marker-bin-double-click
Timeline: a double-click on the composition marker bin adds a marker at the current time (#1826)
2026-11-05 15:13:25 +01:00
FredApps 98c08c791e Merge remote-tracking branch 'origin/main' into pr 2026-11-05 14:50:07 +01:00
Zinadin ZidanandClaude Opus 5.5 0c7cf039b9 Timeline: a bin click is pressed and released on the bin, while the bin has the pointer (#1826)
A press on the ruler right beside the marker bin, released a few points
away on the bin, is still a click, and it counted as one on the bin; two
of those added a marker. A click now counts as the bin's only when both
its press and its release are on the bin, in a frame when the bin is
enabled and has the pointer (not a popup above it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:48:34 +06:00
Zinadin ZidanandClaude Opus 5.5 133dcddf4d Timeline: the marker bin pairs its own clicks, in the order they come (#1826)
The bin counted a double-click from egui's flag for the whole frame and
the number of releases over it, so three clicks in one frame, on the bin,
beside it and on it again, still added a marker. It now walks the
frame's pointer events itself, by egui's rules for a click and a
double-click, and adds one only for two clicks in a row that are both on
the bin. It also counts while the bin is dragged or its drag has just
stopped, not only while the pointer is over it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:26:39 +06:00