Commit Graph

236 Commits

Author SHA1 Message Date
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
bflatastic dfb0227468 M5.14: J / K stop at the work area; the Info panel shows a selected keyframe
- Go to Next / Previous Keyframe (K / J) also stops at the work area's start and end, as in
  After Effects.
- With one keyframe selected, the Info panel shows its property, time and value instead of
  "1 keyframes selected".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:05:03 +07:00
bflatastic 43bf2906d3 M5.13: keyframe paste: several copied layers paste onto as many layers in order; Paste Reversed pastes like Paste
- Keys copied from several layers and pasted onto as many selected layers go first to first,
  second to second, as in After Effects; every target used to receive every copied property
  (the later ones overwriting the earlier).
- Edit ▸ Paste Reversed Keyframes mirrors the clipboard over its span and pastes it through
  `keys.paste`, so it now follows the same rules as Paste: the layer's time stretch, the
  selected target property, Hold-only properties, value conversion and roving keys (its own
  simplified paste ignored them and kept existing keys' easing on a time collision).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:02:16 +07:00
bflatastic 9ea1f1325d M5.12: double-click a keyframe to edit its value; Select Equal / Previous / Following Keyframes; roving keys as dots
- Double-clicking a keyframe of a numeric property (or its context menu ▸ Edit Value…) opens
  the property's value dialog for that key, as in After Effects (it only moved the current
  time); other keys still move the current time to them.
- The keyframe context menu has Select Equal Keyframes, Select Previous Keyframes and Select
  Following Keyframes (`keys.selectEqual` / `selectPrevious` / `selectFollowing`).
- Roving keys draw as small dots in the Timeline, like After Effects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:58:03 +07:00
bflatastic daed03305a M5.11: Graph Editor: drags move every selected key, Shift keeps an axis, Ctrl / Ctrl+Alt clicks; drags and the transform box never merge or lose keys
- `keys.transform {fromStart}`: each step of a merged drag gives the whole transform since the
  drag started and applies to the keys as they were then. Keys squeezed onto one frame on the
  way (Alt-drag scaling, the transform box) are no longer merged for good, and a slow
  transform-box move, whose per-frame offsets rounded to nothing, now moves the keys.
- Dragging a key in the Graph Editor moves every selected key in time and value (it moved only
  the grabbed one), by how far the pointer went from where the button went down (the key no
  longer jumps under the pointer); Shift keeps the drag to the time or the value axis.
- Shift+click toggles a key in or out of the selection; Ctrl+click switches Linear ↔ Auto
  Bezier and Ctrl+Alt+click toggles Hold, as in the Timeline.
- The transform box and the Timeline's Alt-drag scale send whole-gesture values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:52:20 +07:00
bflatastic 9929b5f9ac M5.10: keyframes behave like After Effects: drags never destroy keys, stretched layers, Shift/Ctrl/Ctrl+Alt clicks, locked layers, Hold off
- A drag (keys.move steps sharing a merge key) applies its total offset to the keys as they
  were before it (`Session::key_move`, `prop::shift_keys`): dragging over another key of the
  property no longer deletes it; only the drop position replaces a key, as in After Effects.
- Keys of time-stretched layers move with the pointer: offsets are applied in comp time and
  snapped to comp frames (drags and Alt+Arrow nudges; they moved by the layer-time distance).
- Shift+click toggles a key in or out of the selection (`keys.select {toggle}`); Ctrl+click
  switches Linear ↔ Auto Bezier and Ctrl+Alt+click toggles Hold.
- Keys of locked layers can't be selected (clicks, marquee, Select All Keyframes), so nothing
  edits them.
- Toggle Hold off returns an eased key to Bezier (it always restored Linear).
- Shift-dragging a key snaps to everything the current time snaps to — other keys, layer in /
  out points, layer and comp markers, the work area — and a tooltip shows the key's time and how
  many frames it moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:40:42 +07:00
bflatastic 2d4a0902ea M12.8: the app no longer freezes every 10 s on Windows: the system memory is read in the background
Every 10 seconds the UI thread re-read the system's memory for the cache budgets, and on
Windows that starts PowerShell (`Get-CimInstance Win32_OperatingSystem`) and waits for it:
about a second, sometimes two, during which the whole app froze. The reading now runs on a
background thread (`sysinfo::MemoryWatch`); `Session::memory_tick` takes the last reading and
starts the next one, never waiting. Console programs started for it no longer open a console
window (`CREATE_NO_WINDOW`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:05:17 +07:00
bflatastic a12d4ae1fa M3.9: a browser session restored with unsaved changes stays modified
The web app reopens the last visit's project and, when it had unsaved changes, marked it
modified with `bump()`. Since M3.9 the modified mark follows the saved snapshot, and a bump on
the very project just opened counts as "back at the saved state", so restored changes looked
saved (no `*`, no unsaved-changes prompt). `Session::mark_unsaved` forgets the saved snapshot
instead: the project stays modified until saved, whatever is undone. Regression test included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:33:40 +07:00
bflatastic b21e04b301 M4.12: never crash: hostile input in New Comp from Selection, an empty work area in Cache Frames When Idle
Following AGENTS.md's Never crash rules for the M3 / M4 additions:
- Cache Frames When Idle panicked (`clamp` with min > max) when the work area was inverted
  (start set past the end); an empty work area now has nothing to cache.
- `file.newCompFromSelection`: the sequence overlap times the layer count overflowed for a huge
  `overlapDuration` (wrapping to a wrong comp length without overflow checks); negative or
  non-finite values fall back to 1 s and the product saturates. The lead item is read with
  `get()` instead of indexing.
- Layer Settings' enablement reads the first selected layer with `first()`.
- Regression tests for both (each failed before the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:30:46 +07:00
Brandon Thomas c6488d0cd4
Merge pull request #30 from storytold/pr/never-crash-engine
Never crash: engine without expect() (job polling, commands, menus)
2026-10-04 15:31:40 -04:00
Brandon Thomas 6f52a0aab8
Merge pull request #19 from storytold/pr/never-crash-project
Never crash: project crate without expect()
2026-10-04 15:30:22 -04:00
Brandon Thomas a69f417d63
Merge pull request #28 from storytold/pr/never-crash-guard
Never crash: last-resort guard around command execution, and a logging panic hook
2026-10-04 15:28:05 -04:00
bflatastic 0fe6db00e8 M4.9: Preserve frame rate when nested or in render queue, Preserve resolution when nested
Composition Settings ▸ Advanced has both options (they were missing from the model, the
renderer and the dialog):
- Preserve frame rate: nested in another comp, the comp shows only its own frames
  (`EvalCtx::nested_time` floors the precomp layer's source time to the nested comp's frame
  rate; used for the nested frame, its proxy, aux channels, collapsed transformations and the
  viewer's internal wireframes), and the Render Queue renders it at its own rate whatever the
  Frame Rate override (`RenderSettings::rate`).
- Preserve resolution: nested in a comp rendered at Half / Third / Quarter, it renders at full
  size and the buffer's scale places it.
- `comp.settings {preserveFrameRate, preserveResolution}`, `comp.info` reports them, scripts
  read and set `CompItem.preserveNestedFrameRate` / `preserveNestedResolution`, Pre-compose
  copies them, and the dialog registers `dialog.comp.preserveFrameRate` /
  `dialog.comp.preserveResolution`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:33:14 +07:00
bflatastic fa27d621e0 M4.8: Edit ▸ Purge clears what it names: disk and snapshot purges keep the RAM preview, memory purges drop decoded footage
- Every purge emptied the viewers' RAM preview; Purge ▸ All Disk Cache and Snapshot now leave
  it (Image Cache Memory, All Memory, All Memory & Disk Cache, All and All 3D Cache still
  clear it).
- The memory purges also drop the decoded footage frames (`FootageSource::purge`, the media
  pool's `clear_frames`; decoders stay open), so footage reads decode again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:03:47 +07:00
Brandon Thomas 3d5432c318 Never crash: engine without expect() (job polling, commands, menus)
Removes the engine's production expect() calls: background-job polling (camera, mask, roto,
point and warp tracking, render queue) uses let-else / if-let instead of take().expect("job"),
command helpers bind what they checked instead of re-reading it with expect, the menu bar falls
back to an empty tree (logged) instead of expect()ing that a constant parses, and
autotrace/text animator group lookups no longer expect a just-inserted group. perf.rs, the
benchmark harness behind `effectcraft-cli bench --ops`, opts out with a module-level allow.
Test-only unreachable!() calls become panic!() (clippy.toml allows panics in tests). Denies the
panic lints in the crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:40:46 -04:00
Brandon Thomas 6144ca0804 Never crash: last-resort guard around command execution, and a logging panic hook
Session::execute runs every command (UI, CLI, control channel, MCP, scripts; file import,
export and save are commands too) through guard::guarded, which catch_unwinds a panic that
escapes the command and returns EngineError::Other("command `x` failed with an internal error
(...); it was stopped and your project is still open"). Edits work on a copy of the project, so
a command that panics mid-edit leaves the project untouched. The desktop app also installs a
panic hook that logs every panic through the app logger (then runs the default hook).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:27:33 -04:00
bflatastic 948f06fbf2 M3.14: project files name the version that wrote them, never save unreadable, keep their format
- `Project::to_file_json` writes `savedBy` (the EffectCraft version) ahead of the project and
  checks that the text reads back before anything is written: JSON has no NaN or infinity, so
  such a value made a file that saved but could never be opened. Save now fails with a message
  and the file on disk is left as it was.
- Opening a project saved by a newer EffectCraft warns that settings this version doesn't know
  are lost on saving; `file.open` reports `savedBy`. A file that isn't a project names itself in
  the error.
- Increment and Save keeps the extension (`.ecprojx` stays XML); Save a Copy writes XML for
  `.ecprojx` / `.xml` paths, like Save As.
- Docs: architecture (document model), parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:08:07 +07:00
Brandon Thomas 587d4803fe Never crash: project crate without expect()
Layer::motion_trackers_mut expect()ed the group it had just inserted; it returns an Option
now and track.motion reports an error if the group is somehow missing. Denies the panic
lints in the crate (the project model behind .ecproj files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:58:51 -04:00
bflatastic 6fc39eb133 M3.13: menu bar: Group/Ungroup Shapes in the Layer menu, Window ▸ Learn, flowchart shortcuts, Center In View shortcut, Reveal in Explorer on Windows
- Group Shapes (Ctrl+G) / Ungroup Shapes (Ctrl+Shift+G) sit in the Layer menu itself, as in
  After Effects (they were under Mask and Shape Path).
- Window ▸ Learn opens the Home screen's Learn tab.
- Composition ▸ Composition Flowchart is Ctrl+Shift+F11 and Window ▸ Flowchart Ctrl+F11 (the
  flowchart had the panel's shortcut and the panel none).
- Layer ▸ Transform ▸ Center In View shows Ctrl+Home.
- "Reveal in Finder" reads "Reveal in Explorer" on Windows and "Reveal in File Manager" on
  Linux (`menus::platform_label`).
- Docs: parity (View ▸ New Viewer noted as not yet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:58:00 +07:00
bflatastic a34d0c421b M3.12: Pre-compose keeps the composition's settings; New Comp from Selection is one undo step with After Effects' options
- Pre-compose makes the new composition with the original's pixel aspect, background, motion
  blur and frame blending settings and 3D renderer (`Comp::nested_like`); it kept only size,
  rate and background.
- File ▸ New Comp from Selection records one undo step (two per item before) and takes each
  item's pixel aspect. Several items selected open the New Composition from Selection dialog:
  Single / Multiple Compositions, Use Dimensions From, Still Duration (default from Settings ▸
  Import ▸ Still Footage), Add to Render Queue, Sequence Layers with Overlap, Duration and
  Transition. `file.newCompFromSelection {single, dimensionsFrom, duration, addToRenderQueue,
  sequence, overlap, overlapDuration, transition}`; `layer.addItem {duration}` for stills.
- Docs: parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:48:54 +07:00
bflatastic 89653f37de M3.11: layer commands respect locks, keep children in place on delete, arrange step by step, keep depth and separated dimensions, keep links on duplicate/paste
- Locked layers can't be selected (Timeline click, Select All, Ctrl+Up/Down; shy layers while
  hidden too) and are left alone by Clear/Delete, Arrange, Layer ▸ Transform and Center Anchor
  Point, which fail when every target is locked (`commands::unlocked`, `commands::selectable`).
- Deleting a parent unparents its children where they are, in the same undo step: the
  pick-whip compensation moved out of `layer.setParent` into `layer::parent_fixes` /
  `ParentFix::apply`, which now also handles separated X/Y Position.
- Bring Forward / Send Backward move each selected layer one step (a non-contiguous selection
  was squeezed into one block).
- Center In View / Fit to Comp keep a 3D layer's Z (and Fit keeps Scale/Anchor Z); Reset /
  Center / Fit / Center Anchor Point work with separated dimensions (`layer::update_position`).
- Duplicate and paste keep parents and track mattes, following the copies when both were
  copied; paste dropped them always.
- Docs: agents, parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:33:54 +07:00
bflatastic 753de070a0 M3.10: Layer Settings opens Solid Settings with "Affect all layers that use this solid"; settings dialogs register every control
- Layer ▸ Layer Settings on a solid or adjustment layer opens Solid Settings on the layer's
  solid (it ran an empty command before): name (renames the layer and the solid), size, Pixel
  Aspect Ratio, colour, and "Affect all layers that use this solid". It starts off, as in After
  Effects, so changing a solid other layers share gives this layer its own copy.
  `layer.settings {pixelAspect?, affectAll?}` → `{layer, solid?}`; `layer.newSolid {pixelAspect?}`.
- Nulls ask for their name; text and shape layers have no settings (the entry is disabled).
- Composition Settings / New Composition and Solid Settings register an automation id for
  every control (`dialog.comp.*`, `dialog.solid.*`). Composition Settings takes any typed frame
  rate besides the list, and New Composition starts from the last settings used.
- Colour buttons in both dialogs show the stored sRGB colour (`widgets::srgb_color_button`);
  egui's linear picker showed it lighter.
- Docs: agents, parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:22:09 +07:00
bflatastic e0ed28f55d M3.9: closing a modified project asks to save it first; the modified mark follows undo
- Quit and the window's close button, File ▸ New Project, Open Project, Open Recent, Close
  Project, the demo project and a Home template show Save / Don't Save / Cancel when the
  project has unsaved changes (Save asks for a path when untitled; cancelling that keeps the
  project open). File ▸ Revert asks to discard. Before, all of them dropped unsaved work
  without asking.
- The window title shows the project's path and `*` while it is modified.
- The modified mark follows undo: the session keeps the snapshot it last opened or saved
  (`Session::saved_project`), so undoing, jumping in the History panel or rolling back a batch
  to the saved state clears it.
- An edit that changes nothing (a value set to what it already is) records no undo step and
  doesn't mark the project modified (`Project::same_content`, comparing untouched comps by
  pointer).
- Agents' `engine.execute` / MCP calls are never asked; `ui.menu.invoke` shows the prompt like a
  click (automation ids `dialog.unsaved.save` / `dontSave` / `revert` / `cancel`), and the
  control channel's `app.quit {force: true}` skips it.
- Docs: architecture, agents, control protocol, parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:02:14 +07:00
Brandon Thomas a006ed5765 M13.32: web main-thread stall: Warp Stabilizer plans solved in job workers
warp.status solved the stabilization plan (Subspace Warp eigen-decompositions and mesh fits)
on the page the first time it saw an analysis: ~4.9 s blocking the browser's event loop in
the smoke test. The analysis job now sends the plan summary with its result, and a settings
change has a WarpPlan job solve it (status reports stabilizing meanwhile). Desktop unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 03:40:46 -04:00