Commit Graph

259 Commits

Author SHA1 Message Date
bflatastic 468ed0b297 Logging: RUST_LOG prints to stderr, and start-up logs its milestones (#234)
The logger kept warnings in memory and wrote to a file only after Help
▸ Enable Logging, and it ignored RUST_LOG. An app that never opens its
window (macOS 27 on Apple Silicon, #234) therefore left no trace. Now
RUST_LOG (a level, or env_logger-style `target=level` directives)
also prints what it lets through to stderr, including eframe, winit
and wgpu. The desktop app logs its start-up milestones: opening the
window, the graphics adapter, the app created, the first frame drawn,
the window being asked to close, and the window closed.

Test: rust_log_filters_by_level_and_target (engine logging). Checked
by running the app with RUST_LOG=info and quitting it over the control
channel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:32:16 +07:00
bflatastic 89e075d1b1 Shapes: Tools bar Tool Creates Shape / Mask and Fill / Stroke Options (#227)
The Tools bar had Fill and Stroke swatches and a Stroke Width, kept in
the interface only, so agents couldn't set them and new shapes could
only be a solid fill with a solid stroke. As in After Effects:

- With a shape layer selected and a shape tool or the Pen active, Tool
  Creates Shape / Tool Creates Mask choose whether drawing adds shapes
  to the layer or masks on it (Fill and Stroke hide while drawing
  masks).
- Clicking the word "Fill" or "Stroke" opens Fill Options / Stroke
  Options: None, Solid Color, Linear Gradient or Radial Gradient, the
  blend mode and the opacity. The swatches show the kind; Alt-click
  steps through the kinds.
- New shapes and Pen paths are painted with them: a Gradient Fill or
  Gradient Stroke across the shape for the gradient kinds.

The options are engine state (EditorState.shape_tool) set with the new
shape.toolOptions command; shape.newShape and shape.newPath use them
unless given paint parameters (which now include fillType, fillBlend,
fillOpacity and their stroke twins; layer.newShape takes them too). A
Pen path without paint parameters now takes the Tools bar's stroke
(none by default) instead of a fixed 2 px white one. layer.addMask also
draws rounded rectangle, polygon and star masks, so every shape tool
makes a mask on a selected footage or solid layer (rounded rectangles,
polygons and stars made a new shape layer).

Test: shape_tool_options_paint_new_shapes,
masks_of_every_shape_tool_kind,
tool_creates_mask_draws_a_mask_on_the_selected_shape_layer,
fill_options_paint_the_next_shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:03:38 +07:00
bflatastic 0048569682 Shapes: Duplicate duplicates the selected shape items, not the layer (#227)
Edit > Duplicate (Ctrl+D) with a shape group or other shape item
selected in the Timeline duplicated the whole shape layer. As in After
Effects it now duplicates the selected items inside their group, each
copy above its original and numbered on ("Rectangle 1" gives
"Rectangle 2"), and selects the copies. With only layers selected the
layers are duplicated as before.

Test: duplicate_with_shape_items_selected_duplicates_them_in_place,
ctrl_d_duplicates_the_selected_shape_group_in_its_layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:03:38 +07:00
bflatastic 95bb346c89 Shapes: Copy, Cut and Paste shape items between shape layers (#227)
Edit > Copy with a shape group (or a path, fill, stroke or path
operation) selected in a shape layer's Contents copied the whole layer,
and Paste made a new layer. As in After Effects, Copy now copies the
selected items, Paste puts them into each selected shape layer's
Contents (above its selected item, in that item's group, else on top,
with names kept unique there: "Rectangle 2"), and Cut removes them, so
Cut + Paste moves them. Pasting with no shape layer selected is an
error rather than a new layer. Copying anything else clears the shape
item clipboard, as it does the other clipboards.

Test: shape_contents_copy_cut_and_paste_between_shape_layers,
ctrl_c_and_ctrl_v_copy_a_shape_group_into_another_shape_layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:03:38 +07:00
bflatastic 9968950b71 Shapes: drawing with a shape tool adds to the selected shape layer (#227)
With a shape layer selected, the Rectangle, Rounded Rectangle, Ellipse,
Polygon and Star tools made a new shape layer for every shape. After
Effects draws into the selected shape layer instead: the shape becomes a
new group on top of its Contents ("Rectangle 2", ...), placed by the
group's Transform. The new shape.newShape command does that (in the
layer's space, so a moved or scaled layer still gets the shape where it
was drawn), and makes a new shape layer only when no unlocked shape
layer is selected. The viewer's shape tools use it; masks on other
selected layers are unchanged. layer.newShape and the Pen's
shape.newPath share its layer and Contents helpers.

Test: shape_tools_draw_into_the_selected_shape_layer,
shape_tool_draws_into_the_selected_shape_layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:03:38 +07:00
bflatastic c4c28682eb Timeline: dragging over layer switches sets them all (#227)
Each layer switch toggled on its own click only, so hiding or locking
many layers took a click per layer. As in After Effects, pressing a
switch in the A/V Features or Switches column now sets it at once, and
dragging up or down gives the same switch of every layer the pointer
passes the state the first one got, whatever their own state was
(rows passed in a quick drag included). The whole drag is one undo
step: layer.setSwitch takes a `merge` key like other gesture edits.
A single click still changes only that layer, so clicking the eye of
one selected layer doesn't change the others (Layer > Switches still
sets all selected layers).

Test: dragging_over_layer_switches_sets_them_all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:59:40 +07:00
bflatastic fd572949f3 Settings: Language ▸ Match System follows the system language (#229)
The interface language was English unless Japanese was chosen in
Settings ▸ General ▸ Language. Language now has a Match System choice
(`general.language` = `system`), the new default: the operating
system's interface language where EffectCraft has a translation
(Japanese), else English, as After Effects installs in the system's
language. English and Japanese can still be chosen; a language chosen
before stays. The browser build stays in English with Match System,
since it has no Japanese font of its own.

The system language comes from sys-locale (MIT OR Apache-2.0, no
dependencies on the desktop platforms), on desktop builds only.

Tests: match_system_uses_the_system_language_where_there_is_a_catalog
(i18n), interface_language_is_validated_persisted_and_backward_compatible
(engine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:58:11 +07:00
Claude a831d12975
Puppet: dragging pins keeps the layer under the Puppet cached (#212)
Every pin move re-rendered the Puppet's input (source, masks and the
effects before it) at full size on the UI thread and deep-copied it
for the mesh overlay: the cache key of a layer's input hashed all of
its effects, so moving a pin of the Puppet changed the key of its own
input. The input key now covers only the effects that run before the
Puppet, and the overlay shares the cached buffer.

Test: input_keys_ignore_later_effects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
2026-10-07 22:13:19 +00:00
Claude b4d2d58eeb
Masks and Project panel: selecting a mask selects its points; linked Mask Feather; selection box (#203)
- Selecting a mask in the Timeline (its row or Mask Path), or a shape's
  Path, selects all its points, so dragging one in the viewer moves the
  whole mask, as in After Effects. Delete still removes the mask, and
  selecting another property deselects the points.
- Mask Feather has the chain link (Constrain Proportions, on by
  default, Alt edits one value) and no longer takes negative values;
  Scale's link shows as a toggle too.
- Dragging in the Project panel's empty area draws a selection box
  (Shift / Cmd adds); a click there deselects.

Tests: selecting_a_mask_selects_its_points,
mask_feather_values_are_linked_and_not_negative,
dragging_in_the_empty_area_box_selects_items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
2026-10-07 22:13:19 +00:00
Claude b26f55a775
Shapes and timeline: toolbar Fill / Stroke edit selected shape layers; Alt+Shift+P reveals; click below layers deselects (#205)
- With a shape layer selected, the toolbar shows its Fill, Stroke and
  Stroke Width (with any tool) and editing them changes the layer's
  fills and strokes, as in After Effects (new shape.fillStroke, one
  undo step per picker or drag). Shapes drawn without a stroke get one
  when a stroke colour or width is set, so changing the stroke now works.
- Alt+Shift+P (A, S, R, T) reveals the property it keyed on each layer.
- A click in the time graph below the last layer deselects the layers
  (it only deselected keyframes), so a shape tool draws a new shape
  layer instead of a mask on the selected image.

Tests: toolbar_fill_and_stroke_paint_selected_shape_layers,
toolbar_stroke_edits_the_selected_shape_layer,
add_position_key_reveals_position,
clicking_below_the_layers_deselects_them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
2026-10-07 22:13:19 +00:00
Claude b8b13b1cec
Timeline: shape layers get the Contents "Add:" menu; Classic 3D shows Change Renderer (#206)
Trim Paths, Merge Paths, Repeater and the other path operations
existed (layer.addShapeItem) but no menu added them to a shape layer.
Like After Effects, a shape layer's Contents row now has "Add:" with
Group, Rectangle, Ellipse, Polystar, Path, Fill, Stroke, Gradient Fill,
Gradient Stroke and the path operations; with a shape group selected
they go into it. layer.addShapeItem gains the empty Path and Gradient
Stroke kinds.

A 3D text or shape layer in a Classic 3D comp now shows Geometry
Options with "Change Renderer…", which opens Composition Settings on
its 3D Renderer tab (Advanced 3D comps show the extrusion options).

Popup menus are as tall as the window allows and open upward near its
bottom: the 20-entry Add menu was cut at 420 px, hiding Trim Paths.

Tests: shape_contents_add_menu_adds_path_operations,
classic_3d_shape_layers_offer_change_renderer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
2026-10-07 22:13:19 +00:00
Brandon Thomas 3af4649429
Rename PrintCraft to PdfCraft (storytold/pdfcraft) (#207)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:37:26 -04:00
BFlat 1db3698719 Window: saved workspaces are listed in Window ▸ Workspace (#191)
Save as New Workspace stored the layout and `window.workspace` could
switch to it, but the Workspace submenu was a fixed list of the
built-in workspaces, so a saved one never appeared and could not be
chosen from the menu.

The submenu now has a `savedWorkspaces` dynamic node after the
built-ins (DynCtx carries the saved names), in the in-window and native
menus, with a check mark on the current one as the built-ins have.

The in-window submenu was also too short to show it. egui lays out a
new submenu in a default 400 pt area, and the scroll area inside took
that height and kept it, so Window ▸ Workspace (514 pt) opened with its
last rows (Undocked Panels and below, where saved workspaces go)
scrolled out of view. Submenus now open at their full height and scroll
only when taller than the window.

Tests: saved_workspace_is_listed_in_the_workspace_menu (picks it from
the menu bar), saved_workspaces_are_in_the_native_workspace_menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:17:28 +07:00
bflatastic c1e13b1f66 Export: AV1 WebM refuses an explicit RGB + Alpha request (#166)
apply_output checked `channels: rgba` against the module before it
applied `webmCodec`, and choosing AV1 then quietly dropped the channels
to RGB. So `render --format webm --webm-codec av1 --channels rgba` (one
renderQueue.add) succeeded and wrote an opaque AV1 stream.

The codec options now apply before Channels, so the existing "has no
alpha channel" error (how an unsupported channels request is already
reported) refuses the combination; for WebM it names the codec: "AV1
WebM has no alpha channel (VP9 WebM has)". Choosing AV1 alone on an
RGBA VP9 item still switches it to RGB, as changing the format does.

Test: av1_webm_refuses_requested_alpha.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:48:17 +07:00
bflatastic 247187872d Render Queue: Output To an .mp4 file keeps an HEVC or AV1 module (#179)
apply_output switched the format whenever the Output To extension mapped
to a different format, and `.mp4` maps to H.264 only, so
renderQueue.setOutput with an .mp4 path (even the item's own default
name) turned an HEVC or AV1 module into H.264 and the render wrote an
H.264 stream.

The extension now changes the format only when the current format
doesn't write it (OutputFormat::extension): HEVC, AV1 and H.264 all write
.mp4, so they stay; `.mov` on H.264 still picks ProRes and `.mp4` on
ProRes still picks H.264.

Test: output_to_keeps_a_format_that_writes_the_extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:48:17 +07:00
bflatastic 5d2083b64c Comp: comp.new / comp.settings no longer advertise an `advanced3D` key (#177)
Their params docs wrote the renderer as `renderer? classic3D|advanced3D`.
Without a `:`, the doc parser reads `|` as "or another key", so
`advanced3D` became an accepted key in the schemas and the unknown-key
check: `comp.new {"advanced3D": true}` succeeded and made a Classic 3D
comp.

The docs now use the value-list form the other commands use,
`renderer?: classic3D|advanced3D`. The key is removed rather than
implemented: it would only duplicate `renderer` (and could contradict
it). A scan of every command's schema found no other key that came from
a value list.

Test: renderer_values_are_not_parameter_keys (not in the schema, rejected
as unknown, `renderer` still works on both commands).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:48:17 +07:00
bflatastic 8421489167 Expressions: prop.get reports a syntax error that disabled the expression (#163)
prop.setExpression keeps an expression with a syntax error but disables
it, and says why only in its own reply. MCP set_property answers with
prop.get instead, and prop.get reported `evaluated` / `expressionError`
only for enabled expressions, so set_property, get_property and prop.get
all showed the broken expression as if it were installed.

prop.get now reports a disabled expression too: `evaluated` is the
static value that renders, and `expressionError` is its syntax error.

Test: expression_syntax_errors_reach_the_reply (set_property and
get_property carry the SyntaxError; runtime errors and valid expressions
report as before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:48:17 +07:00
bflatastic 3aaa840471 Scripting: script.run reports .jsxbin scripts as unsupported (#176)
`effectcraft-cli script FILE` passes the file's text to script.run, which
ran a compiled .jsxbin script as JavaScript and failed with "SyntaxError:
unexpected '@'" at 1:1, while file.runScript already says .jsxbin isn't
supported.

script.run now refuses a script named `.jsxbin` or whose text starts with
the `@JSXBIN@` header, with file.runScript's message (one shared
constant). The check is in the command, so the CLI with or without
--bridge, MCP run_script and the Script Console all report it.

Tests: engine_entry_points (by header and by name), script_file_and_eval
(the CLI exits 1 with the message and no SyntaxError).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:48:17 +07:00
bflatastic 83f17dda37 Viewer: Pan Behind snaps the anchor to its own layer (#147)
The Pan Behind drag passed its layer to the snapper's exclude list, which
dropped all of that layer's targets: its corners, edge midpoints and centre
as well as the anchor being dragged. The layer's box doesn't move in comp
space while the anchor pans (Position compensates), so those points are
valid targets.

The engine's per-layer snap targets are now one function, layer_targets,
which can leave out the anchor point. The gesture records its layer's
targets without the anchor as the drag begins and passes them to the
snapper (snap_with) as extra targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:44:46 +07:00
bflatastic 31fc517baa Edit: Delete removes selected animators, masks and shape groups (#151)
Selecting a property group also selects its layer, and edit.clear only
looked for selected keys, mask vertices, puppet pins and effects before
deleting the selected layers, so Delete with a text animator, mask or
shape group selected deleted the whole layer.

edit.clear now removes the selected removable groups (text animators,
masks, shape items, trackers) of the active comp in one undo step before
falling back to layers. It shares the removal with prop.removeGroup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:32:40 +07:00
bflatastic 1edf858180 Project: new layers take their item's label colour (#153)
build::default_label picked a new layer's label from its type alone
(comps Sandstone, images Lavender, ...), ignoring the label of the Project
item it was made from.

A layer made from an item (footage, comp, solid, 3D model) now copies the
item's label when it is created; every path that adds an item to a comp
(layer.addItem from the menus, Project panel, Timeline and viewer drops,
models, precomps, imports) builds its layer there. Layers keep their own
label when the item's changes later. Layers without an item keep the type
defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:32:40 +07:00
bflatastic 6b8200ff78 Timeline: reveal shortcuts leave unselected layers alone (#160)
U (and P, S, R, T, A, E, M, F, L) kept one reveal filter for the whole
Timeline and replaced the set of open layers with the selected ones, so
revealing on one layer closed every other layer and dropped their
reveals.

Each layer now has its own filter (TimelineState::layer_reveal; an open
layer without one shows its whole tree). A shortcut applies to the
selected layers (all layers with none selected) through apply_reveal and
leaves the others as they are; `reveal` stays the last shortcut's filter
for toggling and Shift-adding. U's results replace only the target
layers' earlier picks, so U after UU shows the keyframed properties
again. anim.reveal tells the Timeline which layers it looked at, so a
layer with nothing keyframed closes. Twirling a layer by hand, Ctrl+`
and Collapse All clear the filters they cover.

Test: reveal_shortcuts_leave_unselected_layers_alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:02:01 +07:00
bflatastic c4f8e50201 CLI: --format hevc / av1 write HEVC / AV1; an unknown comp is named (#154, #155)
#154: renderQueue.add (and render.addOutputModule) applied `format`, then
dropped it before applying the output path, so `--format hevc --out
x.mp4` saw the .mp4 extension and switched the module back to H.264
(and ProRes / VP9 for .mov / .webm). An explicit format now wins and a
mismatched extension follows it (x.webm → x.mp4 for AV1), as choosing a
format in the Output Module renames Output To; an extension that agrees
with the format keeps the name as given. The format error lists hevc and
av1 everywhere (one FORMATS string).

#155: a `comp` that names no composition (name or id) failed with "no
active composition". comp_id now reports it as `no composition "Nope"`
(EngineError::NoSuchComp), and comp_for_queue passes that on instead of
collapsing it.

Tests: an_explicit_format_wins_over_the_output_extension and
an_unknown_comp_is_named_in_the_error (engine). Checked with the CLI:
the files are hevc/hvc1 and av1/av01 and decode cleanly with ffmpeg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:28:12 +07:00
BFlat a36eabee5a
Merge pull request #142 from dexterlabs1/contribution/parent-cycle-current-review
Prevent parenting hangs on malformed loaded ancestor chains
2026-10-07 07:26:44 +07:00
BFlat 2a817d8c72
Merge pull request #135 from storybored379/fix/unicode-label-colors
Settings: handle invalid Unicode label colours gracefully
2026-10-07 07:19:40 +07:00
Dexter f5b98f8199 fix(engine): bound parenting validation on malformed ancestor chains 2026-10-06 18:54:13 -05:00
Dexter 2e02523fe9 test: tolerate installed variants of bundled font families 2026-10-06 16:40:12 -05:00
bflatastic 8ef1a28bc4 Viewer: drop Project items, files and effects onto the comp (#85, #88)
The Composition viewer ignored drags from other panels. Now, as in After
Effects:

- A Project item dropped on the viewer becomes a layer centred where it
  lands (above the selected layer, like any new layer); while dragging, the
  viewer is outlined and the layer's frame follows the pointer.
- Media Browser files do the same; files dropped from the OS file manager
  do too when the platform reports where the pointer is during the drag
  (otherwise they import as before).
- An effect from Effects & Presets goes on the layer under the pointer,
  which is outlined while dragging.

layer.addItem and layer.newModel take `position` ([x, y] comp px).
file.import takes `addToComp` with `time`, `index` and `position`;
mediaBrowser.import shares it (file::add_to_comp): layered files go in as
their comps, the comp they were dropped in stays open, and items that
can't be layers are reported in `errors`. viewer::layer_at is the hit
test pick() and the drop share.

Tests: ui_drag_drop.rs (project item and effect dropped on the viewer)
and imported_files_go_into_the_comp_where_they_were_dropped (engine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:31:13 +07:00
bflatastic 1a0940d50a Timeline: Project items land where they are dropped (#89)
A Project item (or Media Browser file) dropped in the Timeline always
went above the selected layer, starting where new layers start.

Now, as in After Effects, a line shows where it goes in the stack (above
the first layer row whose middle is below the pointer, else below the
last), and over the time graph a marker with a timecode tag shows its In
point, which follows the pointer (frame-snapped) or, with Shift, the
current time. The layer reorder drag shares the insertion logic.

layer.addItem and layer.newModel take `index` (1-based stack position);
mediaBrowser.import passes `time` and `index` on, one layer under the
other.

Tests: project_items_land_where_they_are_dropped_in_the_timeline
(ui_drag_drop.rs, real pointer drags: outline, time graph, Shift, below
the last layer) and add_item_at_a_stack_index_and_time (engine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:18:38 +07:00
bflatastic 93856382e7 Preview: Audio, Lock and Shy keep the cache; playback with audio waits for frames (#103)
Toggling a layer's Audio, Lock or Shy switch (or Hide Shy Layers) threw
away every cached frame: the RAM preview identity hashes each comp's
address, which any edit replaces, and the layer and disk cache keys
hashed every switch. None of these change pixels.

- Switches::pixels, Layer::same_pixels and Comp::same_pixels (exhaustive
  field lists, so a new field must be considered) and Comp::pixel_form.
- Layer cache keys hash Switches::pixels; the disk cache's comp content
  key hashes Comp::pixel_form.
- Frames::identity: when a new revision's comps draw the same pixels as
  those of the comp's last new identity, that identity (and the project
  holding its comps' addresses) is kept, so the frames stay, also when the
  switch is toggled back.

With audio on, playback skipped frames that weren't rendered yet. Now the
sound waits: until the frames ahead (to the end of the preview, or what
fits in the RAM preview) are cached, every frame shows as it renders,
silently, like a preview without audio; then the sound starts in sync.
Reaching an uncached frame stops the sound instead of skipping frames.

Tests: audio_lock_and_shy_switches_keep_the_frames (frames.rs, nested
comps and toggling back), audio_lock_and_shy_keep_cached_layers (engine),
the disk key in tests_disk_cache, and
preview_with_audio_shows_every_frame_and_sounds_once_cached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 01:46:35 +07:00
BFlat c750b299cf
Merge pull request #118 from storytold/claude/system-fonts-rmnw4m
G2: font menus list the installed fonts
2026-10-07 00:37:13 +07:00
storybored379 202417dab3 Settings: handle invalid Unicode label colours gracefully
Use checked hex slices so prefs.set follows the existing invalid-colour fallback when a component splits a UTF-8 character. Cover invalid Unicode placements and valid mixed-case colours through the command.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-10-06 09:58:59 -07:00
Claude dda9961889
G2: font menus list the installed fonts
A Windows user reported that the Properties panel's font menu offered only the bundled Inter,
JetBrains Mono and Noto Serif. The system font folders were scanned only when a project asked for
a family the bundled fonts lack, so until then every font menu (Properties, Character, Essential
Graphics) and `file.findMissing {what: fonts}` saw the bundled families alone, on every
platform. Listing the families now scans the system fonts first, and the desktop app starts that
scan on a background thread at launch so the menus are complete when first opened.

The style menus in the Properties and Character panels list the chosen family's own styles
instead of a fixed Regular / Medium / SemiBold / Bold / Italic. Both menus build their lists only
while open.

New query for agents: `text.fonts {query?, rescan?}` (MCP tool `list_fonts`) lists every family
with its styles, origin (bundled / system / user) and own-language name; `rescan` picks up fonts
installed since launch.

Tests: a fresh-process test that the family list includes every family in the platform's font
folders without a prior scan (failed before), and `text.fonts` filtering, rescan and per-family
styles, and the `list_fonts` MCP tool.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EjTYdt76hTG4Kh2CJFnCr
2026-10-06 15:14:55 +00:00
Brandon Thomas 633fbcfaeb Merge PR #71: Persist Japanese menu language in General settings 2026-10-06 08:42:00 -04:00
Brandon Thomas 98872a4690 Merge PR #95: Improve AE-style editing, easing fidelity and Windows GPU rendering 2026-10-06 08:41:33 -04:00
midasdf 25233abbcb G9: persist Japanese menu language without bundled fonts 2026-10-06 16:22:23 +09:00
Dexter 39e6907b5f G2: improve editing reliability and Windows rendering fidelity 2026-10-06 00:00:31 -05:00
bflatastic 9624024dc1 M3.16: deleting Project items that compositions use asks first
As in After Effects, the Delete key and the Project panel's trash button ask before deleting
items that layers in other compositions use, saying how many layers in how many compositions go
with them (Delete / Cancel; Escape cancels). Unused items are deleted at once; agents running
project.delete directly are not asked. project.usage {items?} reports the count, sharing the
deletion's item and usage helpers. Follow-up to #72.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:35:57 +07:00
Brandon Thomas b70c808d60 FreeBSD: system info reports memory, CPU model and OS version
sysctl hw.physmem / hw.pagesize / vm free+inactive page counts, hw.model,
and freebsd-version, with a unit test for the sysctl parser. The FreeBSD
CI workflow itself landed with #69.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:33:06 -04:00
bflatastic e43bf66775 M13.36: face tracking trained model — MediaPipe Face Landmarker on a pure-Rust TFLite interpreter
- effectcraft-segment: a FaceModel interface (find a face in a region, follow it; a Topology
  maps the model's points to the tracker's landmarks and outline), a TensorFlow Lite reader and
  interpreter (float ops, float16 weights folded; matches ai-edge-litert to ~1e-6), and
  MediaPipe Face Landmarker (Google, Apache-2.0: BlazeFace + Face Mesh V2, 3.8 MB, pinned
  SHA-256). The registry gains a task and authors per model; installed weights get a NOTICE.
- effectcraft-track: FaceTracker::new_with uses the model, falls back to the classical fitter
  when it finds no face; chin and jaw come from the outline either way so measurements agree.
- Engine: roto_models becomes models (both tasks); face.models / face.model.* commands,
  Settings ▸ Face Tracking, `wait` on select/install/download for scripts; track.mask reports
  faceModel.
- Credits: ATTRIBUTION.md lists the optional models and the Apache-2.0 specs the code follows.
- Docs: README, ROADMAP (G7), gaps, parity (TRK-3 score unchanged pending G1), agents,
  preferences, architecture, segment README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:39:45 +07:00
bflatastic 7579de21c6 M13.35: Roto Brush trained model — swappable segment module, MobileSAM in pure Rust
- New L1 crate effectcraft-segment: the MaskModel interface (prompts → foreground probability),
  a registry of open-licensed models (licence must be MIT/Apache/BSD/…; URL, size, SHA-256),
  a PyTorch checkpoint reader (zip + pickle subset), dependency-free SHA-256, and MobileSAM
  (TinyViT-5M + SAM prompt encoder / two-way transformer / mask decoder) on rayon and ndarray's
  safe GEMM. Loads the official mobile_sam.pt as published; checked against candle
  (embedding 0.3% RMS, 99.9% equal mask pixels); ~0.7 s/frame encoder at 32 threads, cached
  per frame, ~0.1 s decoder.
- track::roto: segment_with / propagate_with prompt a model with the strokes or the flow-warped
  matte (box, core points, mask); its probability is a strong, unblurred prior for the same
  graph cut (strokes stay hard constraints); a failing model or one disagreeing with the flow
  (IoU < 0.5) is ignored. Prior gets a weight.
- effects::roto: Version 2.0 / 3.0 use the model chosen in Settings (set_model / model_for);
  the model id is part of the chain seed.
- engine: roto_models (roto.models, roto.model.select / install / download / remove), weights in
  the models folder (host::config_dir()/models, shared by the app, CLI and MCP), background
  loading, downloads through the system curl, verified; Settings ▸ Roto Brush page (prefs
  roto.model). host::config_dir moved from the desktop app.
- Tests: segment (kernels vs direct sums, checkpoint reader, SHA-256 vectors, registry), track
  (fake model: prior shapes the cut, failing / disagreeing models ignored; real weights with
  EFFECTCRAFT_MOBILESAM: base IoU 0.989, propagated ≥ 0.980 vs classic 0.973), engine (registry,
  verification, selection; real install + load), MCP.
- Docs: README, ROADMAP, parity (MSK-4 done: 90/2/0), preferences, agents, architecture,
  crates/segment/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:55:02 +07:00
bflatastic 5afc25d0d8 M13.34: audio scrubbing (Ctrl/Cmd-drag the current time)
- audio::AudioScrub: an open output that plays a faded snippet (one frame, 30–100 ms) of the
  comp's mix at each new frame, replacing what is still queued; holding still repeats nothing.
  The mixing is shared with the preview feeder (mix_block); AudioFeed::clear.
- The Timeline ruler: Ctrl/Cmd-drag scrubs audio (EffectcraftApp::scrub_audio opens the output
  through the audio device hook on first use). The Audio panel meters follow the snippets; an
  idle second closes the output.
- playback.scrubAudio {time?} for agents.
- Tests: ui_audio_scrub (snippet length, fades, same-frame no-op, replace not pile up, silent
  comps, Cmd-drag in the ruler, idle close) with a fake device. Docs: parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:18:54 +07:00
bflatastic f013bdd8d7 M13.33: nested comp markers on the precomp layer bar
- commands::markers::nested_markers: a precomp layer's comp markers at their times in the
  containing comp (start and stretch in closed form; with time remapping or a stretch over
  protected regions, the first frame whose source time reaches them), within In–Out.
- markers.nested {layer}: the same list for agents.
- Timeline: drawn on the precomp bar as outlined read-only markers (positions cached per
  revision), hover names them, double-click opens the nested comp at the marker; automation id
  timeline.layer.<id>.nestedMarker.<i>.
- Tests: engine (start, stretch, trim, time remap, non-precomp error), UI (draw, label, open).
  Docs: parity, control protocol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:12:06 +07:00
bflatastic 97390f15fb M13.32: View ▸ New Viewer — several Composition viewers
- PanelKind::Viewer(n): more Composition viewers (panels::viewers). One is active (the full
  interactive viewer, showing the active comp); the others show their own comp's frame at that
  comp's current time (CPU or GPU frames, their own textures) and become active on a click or
  their tab, their comp becoming the active comp.
- view.newViewer (View menu, the Composition panel menu, Alt+Shift+N): a new viewer of the
  active comp next to the one in use, which is locked. Opening a comp while the active viewer
  is locked shows it in an unlocked viewer, or a new one when every viewer is locked. Closing
  the active viewer hands over to another with its comp. Tabs are named after their comp, with
  their own lock and swatch; Preview full screen maximizes the active viewer.
- UI state: viewers (comp per viewer) and active_viewer (serde).
- Tests: ui_viewers (lock on New Viewer, routing, click to activate, all locked → new viewer,
  close hand-over, tab titles; an ignored snapshot). The bounded-drag frames test asserts what
  holds when freed addresses are reused. Docs: parity, control protocol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:00:11 +07:00
bflatastic 26c714db7f M4.14: content-keyed RAM preview frames; proxies in the disk cache's content key
- FrameKey's identity is the comp's content (frames::comp_content), not the project revision:
  an edit keeps the cached frames of comps it doesn't touch and undo finds the frames of the
  state it returns to. The identity hashes the addresses of the comps a comp draws (~0.1 ms on
  the 1500-layer test project; the full content hash took ~117 ms), by value the footage, solids,
  proxies and project settings; with expressions, every item. Frames::content_of keeps a
  project snapshot per identity with frames (or in flight), so Arc::make_mut always copies an
  edited comp and its address can't be reused meanwhile; past 64 identities the least recently
  used go with their frames (long drags).
- drop_stale is gone (frames of other contents are evicted least-recently-shown first).
- disk_cache::comp_content_key covers items' proxies and Use Proxy (it served stale frames).
- Tests: frames (content keys, edits elsewhere and undo, bounded drags, in-place edit safety),
  ui_ram_preview (end to end), the disk cache proxy key. Docs: roadmap (M4 done), parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:39:17 +07:00
bflatastic 2039688245 M13.30: the ArtCraft mark in the Tools bar, About and Home; VectorCraft sibling link
- paint_logo draws docs/brand/artcraft-mark.png (the README's / getartcraft.com's ArtCraft
  mark) as supplied, decoded once into a mipmapped texture, instead of a drawn placeholder.
- The "More ArtCraft apps" sibling and help.sibling named DrawCraft (storytold/drawcraft, which
  doesn't exist): it is VectorCraft (storytold/vectorcraft); provenance comments in the path
  crate follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:49:47 +07:00
bflatastic 05188d036a M4.13: motion blur of collapsed precomps and of animated shape / text content
- Collapsed precomps: the renderer records the precomp layers a nested comp is drawn through;
  motion-blur sub-samples re-evaluate their transforms (when their motion blur is on) and map
  times down through each one, so the precomp layer's own motion blurs its nested layers. The
  containing (outermost) comp's shutter applies to collapsed layers. The GPU compositor gets
  this through Renderer::placement.
- Shape and text layers whose content changes within the shutter (compared by its cache key)
  draw their source at Samples Per Frame times and average them; the cache key covers every
  sub-sample's content.
- Tests: tests_motion_blur_depth (collapsed vs flat, the containing shutter, shape content vs
  the same move by Position); each fails without the change.
- Docs: roadmap, parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:35:30 +07:00
BFlat 1e8bcb519a
Merge pull request #7 from storybored379/pr/comp-zero-frame-rate
Compositions: reject zero and negative frame rates instead of panicking
2026-10-05 12:11:02 +07:00
bflatastic 73b2607c9a M5.16: Key Light: keyer eyedroppers pick from the effect input (effect.pickColor); "keylight" search
- effect.pickColor {effect, param|prop, x, y (layer px), average?}: the colour of the effect's
  input at a point, set on the parameter (one undo step). Keying effects' Effect Controls
  eyedroppers use it, since the shown frame is already keyed; Ctrl/Cmd+click averages 5x5.
- effects::aliases / name_matches: Effects & Presets and effect.list (MCP list_effects) find
  Key Light by After Effects' name ("Keylight (1.2)").
- End-to-end test: a green-screen plate with an uneven screen, spill and soft edges keyed via
  effect.apply "Keylight (1.2)", pick, Clip Black / White and a render; UI eyedropper test.
- Docs: parity, agents (green-screen recipe).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:04:10 +07:00
bflatastic 47f2a5a3e6 M5.15: J / K and Select All Keyframes use what the Timeline shows; Graph Editor Auto-Select Graph Type
- J / K (Go to Previous / Next Keyframe) and Ctrl+Alt+A (Select All Keyframes) act on what the
  Timeline shows, as in After Effects: the keys of its revealed properties, layer and comp
  markers and the work area (they used every property of the selected or all layers, revealed
  or not). The commands take `visible: [{layer, prop}]`, which the Timeline fills in; agents
  calling them without it keep the previous behaviour.
- Graph Editor ▸ Auto-Select Graph Type (an "Auto" button, on by default as in After Effects):
  the speed graph when every property shown is spatial (Position, Anchor Point…), else the
  value graph; choosing Value or Speed turns it off.
- Docs: parity, agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:36:55 +07:00