Dragging an expression's pick whip always replaced the whole
expression, also while it was being edited (and the editor then wrote
its old text back over the reference). As in After Effects, a pick
whip pressed while the expression is being edited now puts the
reference at the cursor, replacing the selected text, and the editing
goes on with the cursor after the reference. Without an edit in
progress it replaces the expression as before.
`prop.pickWhip` takes the text being edited (`expression`) and the
selection in it (`range`, in characters) to insert there.
Tests: the_expression_pick_whip_inserts_at_the_cursor_while_editing
(ui_inline_edit), pick_whip_inserts_into_the_expression_being_edited
(engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a Puppet tool, a click off the pins picked the topmost layer under
the pointer by its own bounds, and selected it. So a click where the
deformation had stretched the mesh past the layer's bounds went to the
layer behind (selecting it and giving it a Puppet effect), and any
click on another layer switched to it.
While selected layers have a Puppet effect, a click (or a press that
might start a pin marquee) now works only on them, as in After
Effects: on the layer whose mesh, as deformed now, is under the
pointer (the pin lands on the deformed shape), else the one whose own
art is (a new mesh); elsewhere nothing happens and the selection
stays. Without a rigged layer selected, the tools pick and rig the
layer under the pointer as before.
`puppet::contains` tests a point against a deformed mesh; `puppet.addPin`
uses it too (its own test only held for one triangle winding).
Test: puppet_pins_go_on_the_deformed_mesh_and_the_tool_stays_on_the_rigged_layer
(ui_viewer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect eyedroppers (other than keyers', which pick from the effect's
input) sample the composite the viewer shows; a test covers that on
main. But they, and the Info panel, read the shown frame as if it
covered the whole comp. With a Region of Interest, or the Extended
Viewer of a 3D view, the frame covers only that area, so they read a
pixel somewhere else. They now map the point through the area the
frame covers (`shown_region`, `frame_pixel`).
Tests: the_eyedropper_samples_under_the_pointer_in_a_region_of_interest
(ui_effect_controls), eyedropper_samples_straight_colour (fx_widgets).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a Bezier handle in the Graph Editor changed only that key's
ease. As in After Effects, the same side's handles of every selected
key now move with it, by as much as the pointer moved (time and value
or speed alike), in one undo step. Each handle keeps its offset from
where the drag began, so a handle stopped at a limit (100 % influence)
doesn't drag the others on.
Test: a_graph_handle_drag_moves_the_handles_of_every_selected_key
(ui_keyframes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each half of a keyframe glyph had a 1 px near-black border, so keys
that overlap read as a tangle of borders. Keys are now flat, as in
After Effects: each half is filled and edged in the key's own colour
(the hairline closes the seam between the halves), so overlapping keys
read as one shape. The Light theme's keyframe grey is darker so
unselected keys still stand out on its light time graph without the
border.
Test: keyframes_are_drawn_without_a_dark_outline (icons).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in After Effects, pressing a property's stopwatch toggles it, and
dragging on over the stopwatches above or below gives every property
the pointer passes the state the first one got (animated or not),
whatever its own, in one undo step. Alt+click still adds an
expression.
The gesture is the layer switches' press-and-drag from #238, moved
into `switch_gesture` and shared by both. `prop.toggleAnimation` takes
`merge` so the drag folds into one undo step.
Tests: dragging_over_stopwatches_sets_them_all (ui_timeline_switches),
stopwatch_toggles_with_a_merge_key_are_one_undo_step (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#284 reported no cache indication in the Timeline. Main has one: a
green line under the work area bar for the frames cached at the
viewer's resolution, view and options (blue for disk-cached frames).
This test pins it down: once the viewer's frame has rendered, the
Timeline paints the green line from the start of the work area.
Test: the_timeline_shows_the_cached_frames_in_green (ui_ram_preview).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shift-clicking a layer's name selected the layers from the last one
clicked to it, but Shift-clicking a layer bar in the time graph only
added that layer. Bars now select the same range (shared with the
name column: `shift_range`), and a click on a bar becomes where the next
range starts, as a click on a name does.
Test: timeline_shift_click_on_layer_bars_selects_the_range
(ui_workflow_selection).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scrubbing asks for the viewer frame under the pointer at every step.
Each newer request only demoted the earlier ones to prefetch, so every
frame the pointer passed was still rendered, oldest first, holding the
frame threads the frame under the pointer needed. A newer viewer frame
of a comp now drops that comp's viewer frames that haven't started
(and releases them, so they can be asked for again). Frames already
rendering finish into the cache (rendering can't be interrupted
mid-frame), and the viewer keeps its last frame until the new one is
in. Prefetch frames and other comps' viewer frames stay queued.
Test: a_newer_viewer_frame_drops_the_ones_still_waiting (frames).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ruler moved the current-time indicator only once the pointer had
dragged past egui's drag threshold, or on release for a click. As in
After Effects, the time now jumps to the pointer as soon as the button
goes down in the ruler, then follows the drag.
Test: a_press_in_the_ruler_moves_the_current_time (ui_timeline_zoom).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renders on macOS came out as `Comp 1.mov.ecproj`, and an image
sequence's every frame as `Comp 1_00001.png.ecproj`; another output
was `Comp 2.mov.mov`.
Cause: the save dialogs for anything but a project went through the
project dialog hook, which filters to `.ecproj`; macOS's save panel
appends the allowed extension to any other name. 0.4.0's Render Queue
Choose File… used it (#192 moved Output To to its own dialog since),
and Save Frame As, Save Current Preview, Export Lottie, Save a Copy As
XML, Collect Files, Watch Folder and the Premiere export form's Browse
still did. Nothing then fixed the name: Output To kept `.ecproj` (no
format claims it), and changing the format replaced only the last
extension (`Comp 2.mov.ecproj` → `Comp 2.mov.mov`). `[compName]` of a
comp named `Comp 2.mov` also doubled it.
Fix: `Hooks::save_dialog` asks the dialog for the default name's own
extension (`pick_save_file`), the project dialog only for projects or
on hosts without another (the web); the desktop dialog has no filter
for a name without an extension (folders). `OutputFormat::with_extension`
makes an output path end in the format's extension exactly once
(project extensions and repeats of the format's dropped, a missing one
added; `logo.png.mov` kept), used by Output To, format changes and
`resolve_output`, so existing projects and templates render to the
right name too. Output To picks the file type from the extension
before a stray `.ecproj`.
Test: output_names_end_in_the_format_extension_once (project),
output_to_ends_in_the_format_extension_once (engine) and
save_dialogs_offer_the_file_extension (ui); the engine and ui tests
fail with the fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A requested rate of 0 would divide by zero in the resampler; it now
reads through the plain path, which returns silence for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model::read bounded each tensor but nothing else, so a tiny hostile
.tflite could make Model::run abort on a huge CONV_2D patch allocation,
or keep it busy for minutes. Read now works out from the static shapes,
with checked arithmetic, what one run takes and refuses a model over a
fixed budget: kernel side (32), one operator's working space (2^22
floats), multiply-adds (2^33), values written (2^30), floats alive at
once (2^29) and constant values decoded or laid out at load (2^27).
The shapes run() computes itself now have to match the declared ones
(PAD, CONCATENATION, MAX_POOL_2D channels), and a zero dimension no
longer lets the others grow without bound.
Reported by KazW, with the suggested load-time budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A degenerate detection box (any NaN coordinate, or zero area) has an
overlap of 0 with itself, so `top` was partitioned into the remainder and
the loop never shrank `dets`. `top` now always joins its own cluster, so
every pass removes at least one detection. Well-formed boxes behave as
before (their self-overlap is 1).
Reported with the fix by KazW.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep serving during long renders, recover failed workers, and remove only files opened by the interrupted export. Cover progress, cancellation, EOF and output ownership with regression tests.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Output Module offers 32 Bit Float for AIFF and the queue reports F32, but
audio_file silently wrote 24-bit PCM because plain AIFF has no float. Write
an AIFF-C file instead: FORM type AIFC, an FVER chunk (AIFC Version 1), and a
COMM chunk naming the fl32 compression type, with big-endian IEEE float
samples in SSND. 16- and 24-bit stay plain AIFF.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The DMG opened as a plain Finder window. It now shows the app icon's figure on
the EffectCraft colour, with EffectCraft.app and the Applications link side by
side.
- packaging/macos/dmg/: background.svg (source; text outlined, the figure an
<image> of assets/app-icon) and dmg-layout.DS_Store (Finder view settings:
window, icon size 128, positions, background), written by generate.py on any
OS, byte-for-byte reproducible, with nothing from the machine that ran it.
- package.sh renders the SVG with resvg (1x + 2x) into a HiDPI TIFF at package
time, so no bitmap is committed; release.yml installs resvg 0.48.1 (pinned,
--locked) on the macOS runner. The image is still built with hdiutil
makehybrid.
- The volume name drops the version: the layout finds the background through
an alias that includes the volume name. The DMG file name keeps the version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clippy 1.99 flags chunks_exact_mut with a constant size; the skip offset
was also an unchecked slice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A filesystem without lock support returned an error from try_lock, which
read as 'held' and disabled crash detection for good.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With one Position pin and no Bend pins, every rotation and scale about the
pin is free in the ARAP energy. The 1e-9 regulariser (about 1e-12 after the
edge-length scaling) is too weak to hold them, so the CG solve turned
round-off into a visible rotation of several pixels, depending on where the
pin landed. The exact solution is the rest mesh translated by the pin's
offset, so deform() returns it directly in that case.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The EXR sequence writer and comp.saveFrameAsExr clamped colour to 0..1 before the sRGB decode, so a 32 bpc project lost every over-range value. The lower bound stays at 0 and alpha stays clamped. Tests cover Rgba and Rgb sequences and saveFrameAsExr.
On FreeBSD the only adapter is llvmpipe over GL, which GpuContext::new
declines for its backend before checking limits; accept either reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed #321 (the alternative Simplified Chinese catalog) row by row against Adobe's
Simplified Chinese After Effects documentation and kept what the documentation confirms:
- Adobe's "混合模式和图层样式" page names the blend modes verbatim, so 相乘 (Multiply), 相加
(Add), 相减 (Subtract), 相除 (Divide), 动态抖动溶解 (Dancing Dissolve), 模板 Alpha /
模板亮度 / 轮廓 Alpha / 轮廓亮度 (Stencil / Silhouette) and 冷光预乘 (Luminescent Premul)
come from it. 滤色, 深色, 浅色, 实色混合 and 明度 stay, because that page gives those names too.
- "设置、选择和删除关键帧" gives 指数比例 (Exponential Scale), 消隐 (Shy), 对齐 (Snapping) and
the 选定 wording; "预合成、嵌套和预渲染" gives 合成微型流程图; the motion-graphics templates
page uses 基本图形 for the panel but 动态图形模板 for the template; the keyboard-shortcuts
page gives 双立方 (Bicubic sampling), 灰度系数 and 主要.
- The 16 label colours now carry the 色 suffix the Chinese edition uses (浅绿色, 淡紫色, 桃红色,
紫红色, 海泡沫, 深绿色), plus a few Adobe-standard phrasings: 置于顶层 / 置于底层, 与图层相同,
色彩管理, Internet 标准 RGB, 另存为模板, 时间伸缩, 减少项目, 单独尺寸, 倾斜, 关.
- Kept mine where those pages contradict #321 (屏幕 for Screen, 较深的颜色, 较浅的颜色,
纯色混合, 发光度, 资源管理器 for Finder, 整理工程 for Dependencies, 素材 for Material), where
they drop the spaces Adobe's own pages use around Latin text (3D 图层, RPF 摄像机导入,
在 Finder 中显示), and where the sibling apps' catalogs agree with this file.
`TRADITIONAL_CHINESE` mirrors the same changes (37 rows). Credit to @cjcwwww for #321, where
these rows come from.
`effectcraft-cli render --out proj/out/d.mp4 --project proj/x.ecproj` wrote
`proj/proj/out/d.mp4`: the Render Queue resolves a relative output against its own base, while
`--project` and every other CLI path are relative to the working directory. The CLI now makes
`--out` absolute against the working directory before queueing it, so the file lands where the
shell user expects; an absolute path is unchanged, and `rendered[].output` in the JSON result is
the absolute path. Render Queue outputs set inside a project (and `render --queue`) resolve as
before: tokens expanded, then `export.defaultOutputFolder`, the project's folder, the working
directory.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- New TRADITIONAL_CHINESE catalog: the same (command, English label) keys as JAPANESE and
SIMPLIFIED_CHINESE, one entry per fixed menu row (618 rows). The wording follows the
Traditional Chinese catalogs PhotoCraft, PdfCraft, VectorCraft and LightCraft already ship
(`檔案`, `開啟`, `儲存`, `圖層`, `遮色片`, `影格`, `預設集`, `快速鍵`, …).
- Match System maps a Traditional Chinese locale (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) to
`zh-hant` instead of English, now that there is a catalog for it.
- `simplified()` and `traditional()` replace the Simplified-only `chinese()`; the macOS Services
item is 服务 in Simplified and 服務 in Traditional.
- `general.language` gains 繁體中文; `docs/preferences.md` documents the locale mapping.
- Tests: all three catalogs must cover the same fixed TREE entries; the shape check covers both
Chinese catalogs; label/undo/panel-title/submenu/native-menu behaviour in zh-hant with file and
layer names kept verbatim; the Traditional locale mapping; the preference round-trips.
Face::has_char counted any non-zero cmap entry as coverage. Some fonts map
whole scripts to empty glyphs (a decorative Latin font installed on a Linux
machine maps あ, 水, ア and 。 to glyphs with no contours), so once such a
face was loaded, fallback_for could pick it and Japanese text rendered as
nothing; cjk_falls_back_to_a_system_family failed whenever another test had
loaded that face first.
has_char (and file_covers, used for faces not yet loaded) now also require
the glyph to draw something: an outline with at least one segment, a colour
glyph, or a bitmap in any strike. Characters that are blank by nature
(whitespace, and the default-ignorable code points such as soft hyphen,
joiners and variation selectors) still count as covered by any mapping.
Face::glyph is unchanged, so shaping with the chosen face is as before.
Test: an_empty_glyph_does_not_cover_its_character builds a minimal TrueType
font in code (no font file added to the repo) mapping U+E000 to an empty
glyph and U+E001 to a triangle, and checks has_char and fallback_for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3vVft4VXvvy8SpbbUceTW