* fix(ui): Remove Attributes says that ticked attributes are removed (#3789)
The report's effect stayed because it was unticked in the dialog to turn
it off; a ticked box means "remove". The dialog now says so above the
attributes. The test covers the hint, an unticked effect being kept, and
an effect offered ticked and removed with the defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): Paste / Remove Attributes offer only the effects they act on (#3789)
The dialogs listed Essential Sound's effects (Dialogue's noise reduction,
EQ…), which the commands keep, so a ticked box promised a removal that
did not happen. The engine's rule is now public as
clip_ops::is_attribute_effect and the dialogs use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With no colour tags, FilmCraft decodes with BT.709 transfer and
primaries and the matrix by frame size, and probe and media.colorInfo
showed those values with nothing saying they were defaults, so an
untagged file looked the same as one tagged BT.709.
VideoStreamInfo gets color_assumed (optional, left out when unknown):
which of the matrix, transfer and primaries no tag in the file gave.
MP4/MOV and Matroska take it from the same sources as the colour, APV
from its colour description, and AVI, MXF and Y4M mark all three.
probe prints it as video.color_assumed and media.colorInfo as assumed,
next to the unchanged metadata.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #3948: time interpolation applies when a clip's frame rate differs from the sequence's
A 24 fps clip in a 29.97 sequence at 100 % rendered the same with Optical
Flow or Frame Blending as with Frame Sampling, repeating one frame in five:
interpolation_blend gave up whenever the speed was 100 %. It now gives up
at 100 % only when the media's frame rate matches the sequence's, so it
takes the sequence rate too. Clips at the sequence's own rate still never
fetch a second frame.
* #3948: a still image gets no in-between frame
A still reports 29.97 fps and returns the same picture at every time, so
in a sequence at another rate it now passed the frame rate check: Optical
Flow ran on two copies of one frame, and the GPU plan fell back to the CPU
for nothing. media_interpolation skips still sources, and the item layer,
media_image and the GPU plan all ask it.
* #3948: an in-between frame never comes from past the clip's source out
A reversed clip starts 1 tick before its source out, so on a source frame
boundary interpolation_blend paired its last frame with the frame at the
source out, at a weight near 1: the first frame showed media the trim had
cut. A forward clip could end the same way (23.976 on 29.97, 5 frames
ending on a source frame blended in 20 % of the next one). Speed changes
had this already; rate conforms at 100 % now take the same path. When the
later frame starts at or past the source out, the clip shows its last
frame alone. The later frame always starts after the source in, so the
other end needs nothing.
A moves[] entry gave its time only as integer ticks. Any other form, or
a missing clip or track, dropped the entry: alone it failed with "need
`moves`", next to other entries only those moved and the call said ok.
`time` now goes through tick_or_time_p like timeline.place's sourceIn,
and an entry without a usable clip, track or time is refused by its
index before anything moves.
* fix(export): an image-sequence export reports the stills it wrote
An export to `a.png` as an image sequence writes `a000.png`, `a001.png` and so on, but the
job's result, `file.exportMedia`'s `path` and the CLI's "exported" line all named `a.png`,
which does not exist.
The report now names the first still, the name `file.import` and `probe --image-sequence`
take for a sequence. `file.exportMedia` and `export.quick` name it as `path` up front, from
the frame count `export.resolve` uses, and add `lastPath`. The CLI prints both. A single
file keeps its path.
Fixes#4009
* fix(automation): a cancelled image-sequence export is cleaned up by its stills' name
Since `file.exportMedia` names an image sequence's first still as `path`, the MCP cancel
clean-up took `a000` as the stem: it left `a001.png` and every later still, and the `a.srt`
sidecar, behind. The result now also carries `basePath`, the name the stills are numbered
from, and the clean-up goes by it.
* fix(automation): an image-sequence cancel clean-up keeps the base name and finds .jpeg stills
An image-sequence export numbers every still, a single frame included, so a file under its base
name (`a.png`) is never its own: cancelling one deleted a user's `a.png` if it was saved during
the export. The stills of a `.jpeg` sequence were left behind instead, since the extension was
read as a format name, and `jpeg` names Motion-JPEG there; `file.exportMedia` keeps `.jpeg`
for a JPEG sequence.
* fix(automation): a cancelled image sequence removes only the stills it numbered
The clean-up matched any `<stem><3+ digits>.<ext>` written during the export, so cancelling an
export to `a.jpeg` could delete `a1000.jpeg`, the first still of an export to `a1.jpeg` in the
same folder. It now takes the digits and the last index from the result's `lastPath` and removes
only those names: `a000.jpeg` to `a120.jpeg` for a 121-frame export.
metadata_fields subtracted a frame from mark_out, so the Out Point read
one frame before the Out mark. Everywhere else mark_out is the start of
the last frame kept: source_range adds a frame to get the end, and the
Project panel's Video Out Point shows it as is.
* interchange: '..' climbs out of a relative document folder (#3998)
join popped a '..' only while the base had more than one part, which kept
an absolute path at its root but also kept a relative folder's first part:
`edit` + `../media/a.mov` gave `edit/media/a.mov`. A relative base now loses
any folder to '..' and keeps a leading '..' past its start; an absolute base
still stops at its root.
* engine: a document given by a relative path has its media resolved once (#4001)
interchange::import passed the document's folder as given, so a document
opened by a relative path gave media paths relative to the working directory,
and MediaPool::follow then took them from the document's folder a second time
(edit/edit/sub/a.mov). The folder now comes from the document's absolute path,
as doc_path::resolve already takes it.
* interchange: a '..' after a base that ends in '..' is kept (#3998)
std::path::absolute keeps '..' on POSIX, so a document opened as
`edit/sub/../cut.xml` (or `../cut.xml`) gives a base that ends in '..'.
join treated that as the root of an absolute path and dropped the media
path's own '..', looking for `edit/sub/../media/b.mov`. Now only a real
root (`/`, `C:`) absorbs a '..'.
* interchange: a document folder's own '..' is resolved before its media paths (#3998)
Keeping a '..' after a base that ends in '..' let a media path climb above
the root: importing ../cut.xml from /work gave /work/../../media/b.mov,
which craft-docpath refuses. join now resolves the base's own '.' and '..'
first with the same rule as the media path's (take off the last folder,
keep it past the start of a relative path, drop it at an absolute root), so
both /work/edit/.. and /work/.. with ../media/b.mov give /media/b.mov.
* interchange: join reads a folder's root as craft-docpath does, and drops empty names (#3998)
join counted one root part, so `..` could take the share off
`\\srv\share\..\edit`, and it kept empty names, so `./` + `clip.mov` gave
`/clip.mov` and a repeated separator gave `..` an empty name to remove.
The folder's root is now read as craft-docpath reads it (none, `/`, `C:\`,
drive-relative `C:`, `\\host\share`, `//host/share`, `\\?\C:`,
`\\?\UNC\host\share`). Below it, empty names and `.` are dropped and `..`
takes off the last name, is dropped at an absolute root and kept past the
start of a relative folder. One table-driven test covers the path forms.
* lumetri.presetThumbnails: write through the host and keep the grid to the presets
The PNG was written with std::fs::write instead of the session's Services,
the path every other export takes (#379), so on hosts without a filesystem
it trapped and elsewhere it ignored the host's file handling. The grid was
also as wide as `columns` asked (up to 32) whatever the number of presets:
there are ten, and 32 columns of 1920 px cells made a 61 572 x 1 084 image,
over a gigabyte of f32 pixels and a 267 MB PNG for one query. Columns now
stop at the number of presets shown. Found by fuzzing commands with hostile
parameters (it took over 10 s per call in a debug build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* render: the preset grid copies thumbnails with checked slices
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
CEA-608 has no code for some characters (the ellipsis, CJK) and the SCC
and MCC writers dropped them, or sent a close basic-set one (’ as '),
without saying so. The CEA-708 service in MCC sent ? the same way, for
instance for an em dash.
charset_warnings lists each such character once, with how often and
the first caption that has it, using the writers' own tables, and
captions.export returns the list as warnings. The list is capped at
20 characters per stream plus one summary line.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
TTML and DFXP exports always wrote xml:lang="en", whatever the caption
track's language. The document now carries the language: export takes
the track's, TTML import reads xml:lang (a language tag of at most 35
characters, else a warning), and captions.import takes an optional
language that wins over both.
* engine: AAF/OMF export takes bitDepth as "24" too, and refuses anything but 16 or 24 (#4019)
plan_from read bitDepth with u64_p, which takes only numbers, so "24" (the
form describe lists, "16|24") fell back to 16 without a word, and `as u16`
let 65560 wrap to 24. It now takes 16 or 24 as a number or a numeric string
and refuses any other value.
* engine: the bitDepth refusal test covers OMF export too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* interchange: EDL media relinked with Align Timecode lands on the file's own time (#3990)
An EDL's source times are timecodes, but its media items had no start
timecode, so Align Timecode had nothing to align from and a clip relinked
after an offline import kept its timecode as media time (the file's last
frame). EDL media now starts at timecode 0, and the relink shift reads each
timecode at its own file's rate, as the import-time rebase does.
* engine: EDL relink test checks the placeholder is listed missing, not its offline flag (#3990)
#4000 stops flagging EDL clips named only by clip name offline, so the setup asks media.findMissing instead, which lists the item either way.
* engine: the EDL relink test also runs with 25 fps media (#3990)
make_movie_at takes the frame rate. Under a 24 fps EDL a 25 fps file's timecode must be read at 25 fps: with the old shift (old_rate.tick_of(b - a)) the clip lands at -149 s instead of 1 s.
An EDL line with no `* SOURCE FILE:` guesses its file from the clip name in
the EDL's folder. When that file wasn't there the importer flagged the clip
offline, which Link Media reads as "made offline": the folder search, Relink
Others and the prompt on open all skip it. It is now a missing file, as an
unfound `* SOURCE FILE:` is.
replacing_the_project_drops_pending_tracking_jobs asserted the discarded job
reported cancelled or finished within ~100 ms. jobs.list has no cancelled
field, so on slow CI runners the worker had not finished yet and the test
failed (b3aeb7c3, 53b937ab). Check the cancel flag that discard sets
synchronously instead.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Apply background mask tracking in headless sessions
Fixes#3302
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: drop pending mask-tracking jobs when the project is replaced
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix: drop mask-tracking jobs started on a replaced project via document epoch
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* History: each step shows the icon of the tool or command that made it
Each undo and redo step now keeps the id of the command that made it,
through undo, redo, merged and grouped steps and the history limit.
The History panel paints that command's icon before each label: a
timeline tool's icon for the commands the tool runs, FilmCraft's own
icon for other commands that have one, and a plain square for the rest
and for edits made outside a command. Undone steps draw their icon dimmed
like their labels. The Open row has no icon. history.list is unchanged.
* History: ripple trims show the Ripple tool's icon
A ripple trim's undo step records its own id, timeline.trim.ripple,
whether it comes from a Ripple tool drag, a ripple edit point in trim
mode or a dynamic ripple trim, and the History panel shows the Ripple
tool's icon for it. Trim mode nudges, Extend Edit to Playhead and dynamic
trims record the edit point's kind (timeline.trim, timeline.roll or the
ripple id) instead of the outer trim command, so a regular trim shows the
Selection tool's icon and a roll shows the Rolling tool's icon. Trim
Previous and Next Edit to Playhead show the Selection tool's icon.
* History: icon targets on a part-scrolled row match the drawn icon
The History panel registered each row's icon target from the row rect after clipping it to the scroll area, so on a row partly scrolled out the target moved off the drawn icon, and an icon scrolled out entirely was still registered. The target is now the drawn icon's rect clipped to the scroll area, registered only while part of it shows.
* History: Apply Default Transitions to Selection shows the plain square
The trim. family gave trim.applyDefaultTransition the Rolling tool's icon, although the command adds transitions at the selected edit points and makes no rolling edit. It now shows the plain square, as the sequence transition commands do.
* fix(ui): the Project panel's Zoom sizes List thumbnails and the Freeform canvas (#3774)
The footer's Zoom slider only sized Icon view's cards. In List view it now sets
the thumbnail size (the row height follows it) when Thumbnails are shown, and
is disabled when they are not, as there is nothing for it to size. In Freeform
view it scales the canvas: cards keep their stored positions and sizes, are
drawn scaled, and a drag moves a card by its distance on the unscaled canvas.
The default size draws both views as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): a zoomed List thumbnail keeps room for the item's name (#3774)
With large thumbnails the thumbnail covered the name and ran into the next
column. The Name column is now drawn wide enough for the thumbnail and
120 points of name; its saved width is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): zoomed Freeform commands use the width the cards are drawn at (#3774)
The card menu (Stack, Unstack, Clip Size), Align to Grid and Save Arrangement
sent no width, so the engine laid unplaced cards out at its 800-point default
and could save positions from different rows than the zoomed canvas shows.
They now send the same unscaled width the view lays the cards out at, and the
engine's parameter lists name the width they already read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): Freeform card and canvas menus act on their own bin (#3774)
A card's menu now stacks, unstacks and resizes only the selected cards of
its own bin, which its canvas width lays out; a selection that also holds
items of another bin left them laid out at this canvas's width. Save
Arrangement saves the bin of the canvas it was opened on, which can be a
floating bin window, instead of the bin the main panel shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: Add shared custom colors and complete marker editing
* #1435: refine native colors and range endpoint flags
Use the macOS Colors panel for custom label colors and reuse the established egui picker on other hosts. Preserve draft confirmation, exact sRGB values, native text shortcuts and project ownership. Compact the swatches and center the add control, split range glyphs across their endpoints, localize the new controls, protect newer marker edits from stale dialogs, and cover the behavior with rendered UI and hit-test regressions.
Co-authored-by: Codex <noreply@openai.com>
* #1435: preserve imported palettes and marker double-click ownership
Union saved OTIO palettes across timelines with deduplication, bounded capacity,
first-name preservation and explicit overflow warnings. Keep null authors
consistent with omitted fields, and consume marker double-clicks before the
work-area gesture so opening the editor does not alter project history.
Correct the CSV URL column and schema documentation. Add six regressions for
palette retention/capacity, author handling, and physical overlapping gestures.
Validation: full cargo xtask ci (3666 passed, 45 skipped), 42 rendered UI tests,
and strict workspace/all-target Clippy on Rust 1.99.1.
Co-authored-by: Codex <noreply@openai.com>
* #1435: retain OTIO swatch names and generator labels
Ignore orphan name entries when retaining saved palette names so input ordering
does not hide valid names after the first 64 entries. Keep names bounded by the
retained palette and preserve existing valid names.
Include generator preset labels alongside custom label colors in OTIO metadata.
Add regressions for orphan-prefixed name maps and three generator kinds with
preset/custom colors through actual export/import round trips.
Validation: both regressions failed before fixes; all 72 interchange tests and
full cargo xtask ci pass (3668 tests, 45 skipped), plus strict workspace/all-target
Clippy on Rust 1.99.1.
Co-authored-by: Codex <noreply@openai.com>
* #1435: preserve distinct OTIO generator item identity
Use exported FilmCraft item IDs for generator media identity so identical
content can retain distinct source names, labels and custom colors. Repeated
references to one source item still share a single imported media entry.
Preserve content-based fallback for missing or invalid item IDs. Cover black
video, bars and tone, and color mattes with distinct items/shared references,
plus legacy and malformed identity metadata.
Validation: distinct-generator regression failed before fix; legacy compatibility
passed. All 74 interchange tests and final full cargo xtask ci pass (3670 tests,
45 skipped); strict workspace/all-target Clippy also passes on Rust 1.99.1.
Co-authored-by: Codex <noreply@openai.com>
* #1435: align Edit Marker layout and add safe marker navigation
Give the editor a wide aligned form, larger Notes area, side action column,
and grouped palette with vertical marker types. Expand the Web Link field
with the form and keep actions accessible in small windows.
Previous/Next saves valid edits as one Undo step and stays with the original
sequence/source owner, including coincident markers. Invalid drafts stay open;
unchanged navigation and Done add no history. Keep native/shared color pickers,
Author, Notes, URL validation and immediate marker creation.
Validation: all 45 rendered sequence/marker UI tests pass, including five new
navigation/owner/validation/layout regressions. Native macOS layout, picker
Cancel and exact single-step Undo restoration verified. Full standard cargo
xtask ci passes 3675 tests (45 skipped), doctests and all remaining gates;
strict Rust 1.99.1 workspace/all-target Clippy also passes.
Co-authored-by: Codex <noreply@openai.com>
* #1435: refine Edit Marker proportions and action alignment
Narrow the dialog to 680 logical pixels and keep the core order Name,
Time/Duration, Notes, Color and marker type. Place Author after the type-specific
options as secondary metadata. Preserve our palette, custom picker and range controls.
Use consistently sized, centered rounded action buttons with a primary Done action.
Shorten Delete Marker to Delete and remove the navigation helper and unused translations.
Keep the existing navigation save/validation/Undo semantics and automation IDs.
Validation: all 45 rendered marker/sequence UI tests pass, including small-window
action accessibility. Actual macOS layout and no-op Previous/Next/Cancel verified.
Full standard cargo xtask ci passes 3675 tests (45 skipped), doctests, formatting,
Clippy, translations, layers, assets and 49 WASM crates. Strict Rust 1.99.1
workspace/all-target Clippy passes separately.
Co-authored-by: Codex <noreply@openai.com>
* #1435: tighten marker layout and validate text editing
Use a narrower aligned form with Author below Name, bounded scrolling Notes,
left-aligned colors, centered actions and an inline Web Link URL/open icon.
Remove the legacy Flash Cue choice while preserving imported marker types.
Constrain timecode typing/paste to complete numeric colon-separated fields,
validate drop-frame labels and frame bounds, and disable Done for invalid timing.
Preserve high-frame-rate timecodes and point-marker semantics when Duration is
unchecked. Route native/menu clipboard and text Undo commands to focused fields;
explicit engine commands retain project Undo and selection semantics.
Validation: standard cargo xtask ci passes 3679 tests (45 standard skips),
doctests, fmt, strict workspace/all-target Clippy, i18n, layers, 69 assets and
49 WASM crates. Also passed 48 rendered marker/sequence tests and strict Rust
1.99.1 Clippy. Actual macOS clipboard, bounded Notes, point save and exact Undo
were checked. Windows/Linux desktop interaction remains pending.
Co-authored-by: Codex <noreply@openai.com>
* M0: Wait for frame-worker recovery before asserting
Do not treat a nonempty request queue as proof of worker death in the
panic-recovery regression. Keep waiting for all injected faults and enough
post-fault completions, then drain the queue without adding more seeks.
Retain the exact fault-count, successful-recovery and empty-queue assertions
and bound the wait with a two-minute deadline.
The inherited test failed twice in independent fork CI. It did not reproduce
locally: baseline passed 20 normal and 10 delayed-report runs; the repair
passed 50 normal and 10 delayed-report runs. No product or marker UI changes.
Validation: standard cargo xtask ci passed all 4485 tests (52 standard skips),
doctests, formatting, strict workspace/all-target Clippy, i18n, layers,
69 assets and 49 WASM crates.
Assisted-by: OpenAI Codex
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <codex@openai.com>