* Dev profile: line tables only for workspace crates, no debug info for dependencies
* xtask ci: dev-profile gate, nextest, tests without examples, wasm --keep-going
* One integration-test binary per crate
* xtask ci: run effectcraft-gpu's shared-device tests in one cargo test process
* xtask ci: run every stage and report all failures
* xtask ci: name the stage type (clippy type_complexity)
* xtask ci: run every test pass and report all failures
* cargo fmt
The text editor's Select All and Undo arms ignored Alt, so a Deselect All
rebound to Cmd+Alt+A or Cmd+Alt+Z was taken as Select All or Undo while a
text edit was open.
The arms now match exactly the chords the editor owns (Cmd/Ctrl+A, and
Cmd/Ctrl+Z with or without Shift); their Alt variants fall through to the
Deselect All binding match. Chords the editor owns still win over a rebound
Deselect All, so Undo keeps working while typing.
Test: a UI kittest rebinds Deselect All to each Alt variant and checks that
Cmd+A and Cmd+Z still edit the text while the rebound chord ends the edit.
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days / days_from_civil copies (log
timestamp, ISO metadata dates, Align Video to Data timestamp parsing, the
Numbers effect's date, the Home and Render Queue date columns, and the
benchmark report date) with chrono as a workspace dependency. Output formats
are unchanged and pinned by the existing tests. AGENTS.md now says time goes
through chrono.
Follow-up to storytold/cadcraft#1785
* Keep 5-digit years unprefixed; derive Numbers weekday from the same date
chrono's %Y writes a '+' before years past 9999, where the old code wrote
{y:04}. timestamp, iso_date, format_utc, utc_stamp and xtask's utc now write
the year with {:04} again and let chrono format the rest, so a far-future
clock keeps the plain zero-padded year.
date_from_days took its weekday from the raw input modulo while the
year/month/day fell back to the epoch for day counts outside chrono's range,
so an unrepresentable count printed a fallback date paired with the input's
weekday. The weekday now comes from the same resolved date, so both agree.
Tests cover the year-10000 timestamp and the out-of-range Numbers date.
* Keep the whole date in format_ago for five-digit years
format_ago cut format_utc to 10 bytes, so a year past 9999 lost its last
digit (10000-01-0). Take the date up to the space instead. Tested.
* Keep Numbers dates exact past chrono's range
date_from_days converted the day count by building a Unix timestamp and
calling DateTime::from_timestamp, which falls back to the epoch (and so
reads 1970-01-01, a Thursday) once the date leaves chrono's NaiveDate
range near year ±262143. Numbers' `value` slider is a plain scalar with
no range guard, so a keyframed count reaches that: Long Date of
100_000_000 showed "Thursday, January 1, 1970" instead of the real
"Monday, September 13, 275790" -- exactly the plausible unrelated-date
fallback the review rejects, and the derived weekday matched the stand-in
date, not the input.
Route it through the 400-year Gregorian cycle like pdfcraft 9d4cb3ca:
chrono converts the day within one cycle (num_days_from_ce of the rebased
rest, always in range) and the whole cycles are added back as years, in
i128 so i64::MIN/MAX can't overflow. chrono stays the only calendar
logic. The weekday still comes from the resolved date, so it always
agrees with it.
Tested: date_from_days_past_chrono_range pins the pre-chrono dates and
weekdays at/over chrono's bound on both sides of the 2000-01-01 epoch
(and that the extremes don't overflow); numbers_formats adds the same
through the public numbers_text entry (Long/Short Date). cargo test
-p effectcraft-effects, fmt --check and clippy -D warnings all clean.
* time: preserve out-of-range years instead of the epoch
`DateTime::from_timestamp(..).unwrap_or_default()` changed the existing output:
inputs the old hand-rolled civil-from-days converted (e.g. `10_000_000_000_000`
s → 318857-05-20 17:46:40) collapsed to the epoch. Restore the old conversion
domain with a cycle-based helper: reduce by whole 400-year Gregorian cycles
(146_097 days), convert the in-range remainder with chrono, and carry the year
back as i128 (saturating to the output type). Only the year leaves chrono's
range; month, day and time of day stay exact. chrono remains the only calendar
logic (NaiveDate::from_num_days_from_ce_opt); no new features.
Sites (UTC):
- crates/engine logging.rs `timestamp` (u64 ms): far-future clock keeps its year.
10e12 s → "318857-05-20T17:46:40.000Z", u64::MAX ms → "584556019-04-03T14:25:51.615Z"
(was "1970-01-01...").
- crates/engine media_browser.rs `iso_date` (u64): 10e12 s → "318857-05-20T17:46:40Z",
u64::MAX → "584554051223-11-09T07:00:15Z".
- crates/ui-egui panels/home.rs `format_utc` (i64): 10e12 → "318857-05-20 17:46",
i64::MIN → "-292277022657-01-27 08:29".
- crates/ui-egui panels/render_queue.rs `utc_stamp` (u64): 10e12 → "318857-05-20
17:46:40", u64::MAX → "584554051223-11-09 07:00:15".
- xtask bench.rs `utc` (u64): 10e12 → "318857-05-20 17:46 UTC", u64::MAX →
"584554051223-11-09 07:00 UTC"; added a test.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
While a text layer was being edited, Edit > Deselect All and its shortcuts
selected every character (or did nothing) instead of ending the edit and
deselecting the layer.
- deselect_all now commits the open edit first, through the same route as
text.endEdit: a created-but-empty type-tool layer is discarded, anything
typed is kept, and no extra undo step is introduced.
- While the in-canvas editor holds the keyboard, the editor forwards the
command's currently assigned shortcut (and any function-key chord) so
Deselect All reaches the command from the comp; bare-letter bindings stay
typing, and Select All still selects characters.
- The Project-panel focus redirects (invocation and menu enabled-ness) stand
down while a text edit is open, so the menu and shortcut end the edit even
when focus stayed on Project after Layer > New > Text.
Tests: three engine cases (commit + discard semantics), six UI kittest cases
(chords, F2, rebinding, menu enabled-ness, Project focus), RED at base.
A raster footage layer's Timeline menu now also has to leave out Create Shapes from Vector Layer. The viewer check keeps the solid selected, so the menu has to follow the layer under the pointer rather than the selection. With the viewer's caps merge made a no-op, the test fails at the viewer check.
file.projectSettings takes frameStart (0|1), the Project Settings dialog shows Frame Count, and scripts get app.project.framesCountType and displayStartFrame. The setting was already read by the Frames and Feet + Frames time display; nothing could change it.
Double-clicking a property name in Effect Controls did nothing. The test double-clicks the name and checks the Timeline marks the property revealed and shows its row. Without the fix nothing is revealed.
The focused panel's outline was painted before the panel body, so body fills hid it. The test finds the outline in the painted shapes and checks no filled rect painted after it reaches its one-pixel ring. Without the fix a body fill overlaps the ring.
Right-clicking vector footage in the Timeline or the Composition viewer offered no vector conversion. The test imports an SVG and checks the item is absent for a solid, present on the SVG layer in both menus, and converts it. Without the fix the menu item is missing.
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.
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.
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.
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>
- English (and Auto where offered) stays first as the source language and fallback; the rest sort by native name with accents folded, so Čeština is under C and Magyar under M; Cyrillic and CJK names follow the Latin ones
- The list used the registry order, which is the order the languages were added
Alt/Option-drag a Project item onto a Timeline layer, or press Ctrl+Alt+/ (Cmd+Opt+/) with layers and an item selected, to swap the layer's source. The layer keeps its name, timing, transforms, keyframes, expressions, effects, masks, switches, parenting and track matte; a source with audio adds Audio properties when missing. Precompositions can't be nested inside themselves.
AVLayer.replaceSource now runs the same engine command and closes the replaceSource part of #813.
Looking up to the whole RAM preview for uncached frames let the workers spread over the preview
while the playhead waited. On the GPU each worker then built a Classic 3D atlas for its own set
of layers, and on a cold start of the #1812 project the device ran out of memory far more often
(first pass 1-56 % of real time instead of 63-81 %). The window now keeps its old width but
starts at the first frame ahead that isn't cached, so a loop or a restart still renders on at
once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Index-units test now opens the Units popup in the Timeline and picks
Index, asserts the stored Units value, then reads the painted Start, End
and Offset, so it also fails when the Units control doesn't update the
session.
The playback prefetch only looked ahead*3 frames (96 with 16 workers) for uncached frames.
After a loop or a restart from the start those were all cached, so the workers stood idle until
the playhead came within that window of the end of the cached run, then the preview stalled.
A playing preview now looks as far ahead as the RAM preview holds (and the range spans) for
the next uncached frames, still queuing at most ahead at a time; eviction already spares the
frames the playhead reaches first (#595).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel catalog's \"Type\" is the kind (Camera Settings, the Project column), so
the Type page drew as \"kind\" in Chinese; its title now comes from the
app.settings menu row (文字), as the Settings submenu draws it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
Start, End and Offset are read as painted in the Timeline: with % under
Units = Percentage, without it under Units = Index. Fails on the old
suffix rule ("0.0%").
With Units set to Index the selector already counts characters (words,
lines), but the timeline kept showing Start, End and Offset as
percentages ("3.0%"), so switching looked like it did nothing. They are
now shown without the % sign while Units is Index
(render::text::in_index_units).
layer.setSwitch with no value toggles relative to the first selected layer in
composition order, so with a mixed selection (A checked, C unchecked) clicking
C's box could leave C unchanged. The checkbox now sends the value it toggles
to, as the switches do.
The label swatch's menu, the Blending Mode menu and Preserve Transparency
passed the clicked layer alone, so a colour or a mode had to be set one layer
at a time. They now act on the whole selection, with the Modes column edits
going only to the selected layers that have video, as its cells do.
The preserve-transparency checkbox also gets an automation id, like the cells
around it.