Commit Graph
2768 Commits
Author SHA1 Message Date
Hamza a213b7f23a i18n (de): fix two labels that name the wrong thing, settle six terms (#1443)
Two German menu labels described a different command:

- `layer.timeReverse` was "Ebenenreihenfolge umkehren", which reads as reversing
  the stacking order rather than time -- now "Ebene zeitlich umkehren", matching
  `keys.timeReverse`;
- `layer.style.convertToEditable` was "In editierbare Formate umwandeln", and
  "Formate" are formats, not layer styles -- now "In bearbeitbare Stile
  umwandeln".

Where the same English term had more than one German rendering, the one already
in the majority wins, so this settles rather than re-translates:

- "Expression" is "Ausdruck" in seven entries and "Expressions" in the two
  `layer.expressions` ones, which now read "Ausdrücke aktivieren/deaktivieren";
- "Reveal" is "anzeigen" in eleven of twelve entries, so `layer.revealInFinder`
  joins `file.revealInFinder` at "Im Finder anzeigen" -- in every other
  catalogue both commands already share one string;
- "Track" as a verb is "verfolgen" in the other four entries, so `track.mask`
  is "Maske verfolgen" rather than "Maske tracken";
- one spelling of "nach vorne", matching the neighbouring arrange entry.

German also had no platform variants for the file manager. The engine puts the
platform's name into the label before the catalogue sees it, so on Windows the
source is "Reveal in Explorer" and on Linux "Reveal in File Manager"; only
Ukrainian and Czech special-cased those, and a German UI showed the English
string. Added, with a test mirroring `czech_…` and `ukrainian_…`.

Two of the reported six are left alone: "Essential Graphics" is a 3-3 split and
a panel's proper name, and "Null" partly mirrors the English, which itself says
both "Null Object" and "Nulls". Both are translator's choices with no majority
to follow, not corrections.
2026-10-14 21:21:39 +05:00
Nigel Ihza Firdausy ba37283aa4 fix(lottie): serialize Offset Paths miter limits as animatable values 2026-10-14 23:14:18 +07:00
tylerjenningsw 8b536e597b Export: millisecond-rounded footage renders at its rate, each frame once (#1442) 2026-10-14 11:37:36 -04:00
tylerjenningsw 36a905293b Media: tests for the averaged MP4 frame rate (#1442) 2026-10-14 11:33:49 -04:00
tylerjenningsw eb95f732e5 Media: millisecond-rounded MP4 frame times read at the rate they average to (#1442)
Phones and messaging apps store frame times rounded to whole milliseconds
(33/34 ms at 30 fps, 16/17 ms at 60 fps). FilmCraft's reader takes the
median duration, so such clips imported at 30.303 or 58.824 fps, and New
Comp from Selection and renders followed that rate. When the median is no
standard rate, the durations near it are averaged and snapped to the
standard rate within 0.5 %; exact and variable rates keep the reader's
estimate.
2026-10-14 11:33:49 -04:00
zitann2020andDeepSeek V4.1 Flash 66a29505a5 i18n(ja): the Settings dialog in Japanese
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
2026-10-15 00:19:47 +09:00
midasmatterhorn c3b88d0d76 pdf: don't slice glyph names at a non-char boundary in name_to_unicode 2026-10-14 18:01:41 +03:00
wixar172andClaude Opus 5.5 9846c7255d Shapes: a stroke's buffer is padded by the miter limit only where it has miter joins
draw_bounds padded every stroke by width × miter limit / 2 on each side, joins or
not. A straight 1080 px wide wipe used as a track matte became a 4326×6366 layer
buffer (~440 MB as RGBA f32), made anew every frame while its trim animates, then
cached on the CPU and uploaded to the GPU. With 12 frames exported in parallel a
16 GB Mac ran out of memory and froze.

Now the miter limit applies only to miter-joined paths with a join (two or more
segments, or closed); square caps reach √2 half widths, other strokes one; Taper
and Wave keep the widest padding. Rendered pixels are unchanged.

A full-res frame of the affected ad: 1.7 GB → 0.7 GB peak.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:20:02 +03:00
Marco Arnulfo cc29d90943 Import: item.metadata counts a WebM's last frame when its duration is not a whole millisecond (fixes #1429) 2026-10-14 14:22:15 +02:00
Hamza 449e9edd59 Render Queue: resolve a bare project filename against the working directory
`resolve_output` takes the project's folder from `Path::new(path).parent()`.
For a project saved under a bare filename that is the empty path, which the
following `filter` discarded, so a saved project fell through to the
unsaved-project default and relative outputs went to ~/Movies (the user profile
folder on Windows) instead of next to the project.

    --project proj.ecproj      ~/Movies/clip.mp4
    --project ./proj.ecproj    ./clip.mp4

Both spellings name a project in the working directory, so both now resolve
there, as the doc comment on `resolve_output` already describes.

Closes #1427
2026-10-14 17:21:58 +05:00
Marco Arnulfo 78b4fe4170 Import: AIFF files import, including EffectCraft's own AIFF renders (fixes #1428) 2026-10-14 13:44:59 +02:00
Marco Arnulfo 69d17d89af Keyframes: a path pasted onto Position becomes a motion path (fixes #1425) 2026-10-14 13:30:45 +02:00
arnav-hiwarkar 68d928aff3 fix(render): preserve preceding effects in GPU Echo (#1120) 2026-10-14 13:42:59 +05:30
Marco Arnulfo 1b44c5c98a Scripting: Stroke and Hue/Saturation parameters use After Effects' match-name numbers (part of #1405) 2026-10-14 09:15:12 +02:00
arnav-hiwarkar bed289e2a1 fix(presets): identify copy folder action by label
Follow up on #1210: remove the fixed menu index from automation and click handling, and move the preset-folder documentation into the Effects & Presets section.
2026-10-14 12:09:48 +05:30
arnav-hiwarkar 8041329df8 feat(media-browser): add Copy Source Path 2026-10-14 11:42:13 +05:30
Marco Arnulfo ce437747ed Expressions: accepting a function completion inserts the call parentheses (fixes #1393) 2026-10-14 05:28:14 +02:00
tylerjenningsw 8d00db7d90 GPU: test CC Particle Systems II against the CPU with a keyed Birth Rate (#1373)
The keyed Birth Rate comparison is shared (keyed_agree) and now runs on
CC Particle Systems II too. The same two histories bear 90 particles by
0.9 s at different steps: the GPU's particles match the CPU's, and each
state really came from the GPU.
2026-10-13 22:35:37 -04:00
tylerjenningsw 6df1fc17b9 Render: tests for CC Particle Systems II and CC Mr. Mercury bursts and their layer cache (#1373)
Through the renderer, a keyed burst draws as the constant rate during it,
still draws after the rate drops to 0 (unlike a rate of 0 throughout),
and draws as nothing born once Longevity has run out. A Birth Rate
expression on any of the three stepped particle systems keeps the layer
out of the layer cache; without one it is cached.
2026-10-13 22:35:37 -04:00
tylerjenningsw 6fcfc355e3 Render: CC Particle Systems II and CC Mr. Mercury layers with expressions aren't layer cached (#1373)
They now read their Birth Rate at earlier times too, so
effects::reads_param_history names them along with CC Particle World. A
layer key can't see what an expression returned then.
2026-10-13 22:35:37 -04:00
tylerjenningsw 16cc290a47 Particles: tests for CC Particle Systems II and CC Mr. Mercury with a Birth Rate over time (#1373)
For both: a burst lives on after Birth Rate drops to 0, and is drawn
(particles, blobs) where a constant 0 draws nothing. Its particles move
and age, and are gone after their Longevity. Seeking matches a
simulation from 0, and moving an earlier key with the same current rate
and cache key isn't served from stale checkpoints. A constant rate gives
exactly the constant-rate simulation's particles, with or without a
host.
2026-10-13 22:35:36 -04:00
tylerjenningsw f4ab728c0a Particles: CC Particle Systems II and CC Mr. Mercury bear at the Birth Rate of each moment (#1373)
Both simulate from layer time 0, as CC Particle World does, and used
the Birth Rate of the rendered frame for every step. A rate keyed down
to 0 emptied them the frame it got there, although their particles (and
Mercury's blobs) were within their Longevity.

CC Particle World's simulation path becomes Stepped: Birth Rate sampled
at every step (rate_history), a cache key without it, and checkpoints
resumed only with the same rate history. Particle World, Particle
Systems II (60 particles a second per unit, CPU or GPU) and Mr. Mercury
(10 blobs a second per unit, CPU) now all use it. A constant Birth Rate
gives exactly the particles it did.

The constant-rate step, key and simulation (Phys::step, sim_key,
simulate) are now only the tests' reference. Mr. Mercury's physics move
into mercury_phys.
2026-10-13 22:35:36 -04:00
tylerjenningsw d04f88750e GPU: the keyed Birth Rate test tells birth timing from birth count and checks the GPU ran (#1373)
The two histories now bear the same 180 particles by 0.9 s at different
steps (3/s from 0 to 0.5 s, and from 0.25 to 0.75 s). So a checkpoint
check by birth count alone fails the test, not only no check. A
test-only dispatch counter asserts each GPU state really came from the
GPU rather than the CPU fallback.
2026-10-13 21:59:03 -04:00
tylerjenningsw f22ce3d839 Render: tests for a Hold Birth Rate cut under fractional stretch and an expression's history (#1373)
A Hold cut at 0.5 s reads 3 just before and 0 at 0.5 s under 33.3 %,
133.33 % and 200 % stretch, and the frame matches the unstretched one.
A Birth Rate expression reading a Controller layer's Opacity before
0.5 s: changing the Controller changes the 0.9 s frame, and the cached
render matches the uncached one.
2026-10-13 21:59:03 -04:00
tylerjenningsw aa300d7f2e Render: CC Particle World layers with expressions aren't layer cached (#1373)
The simulation checks its checkpoints against the Birth Rate history,
but a cached layer is returned before the effect runs. The layer key
sees the layer's keyframes and its current values, not what an
expression returned at earlier times: a Birth Rate expression reading
another layer could change the history while the current rate, 0, stayed
the same.

effects::reads_param_history names CC Particle World. The renderer
already declines to cache layers whose time effects (ec.time.*) meet
expressions. It now treats Particle World the same way, in the layer key,
the input key (only for the effects that input runs) and the dependency
walk.
2026-10-13 21:59:03 -04:00
tylerjenningsw f263a32ab7 Render: an effect parameter's history is read at its exact layer time (#1373)
FxHost::param_at mapped the requested layer time to comp time, and
EvalCtx::value mapped it back. Under a fractional stretch (33.3 %,
133.33 %) that round trip truncates a tick early, so a Hold key at
0.5 s read its previous value at the step starting there: a Birth Rate
cut to 0 at 0.5 s bore one more step of particles (six).

EvalCtx::value_at_layer_time reads the keyframes at the given layer
time exactly, and still runs an expression at the comp time.
EvalCtx::value now uses it, and param_at reads through it.

EffectHost::param_is_static says when a parameter has no keyframes and
no expression. CC Particle World then skips sampling its Birth Rate at
every step, since it's the same throughout.
2026-10-13 21:59:03 -04:00
tylerjenningsw 25fc51f3ed Timeline: Alt held as a pick whip drops refers by index (refs #1072)
A property or expression pick whip dropped with Alt held, onto the
Timeline or Effect Controls, now sends byIndex: an effect's property is
referred to by its index, as in After Effects.

Test: an_alt_pick_whip_refers_to_an_effect_property_by_index (Alt and
plain drops onto Effect Controls and the Timeline, into the expression
being edited with a dimension, one undo step each).
2026-10-13 21:57:19 -04:00
tylerjenningsw f2cdad9edb Expressions: pick-whip references to effect properties by index (refs #1072)
In After Effects, holding Alt while dropping the expression pick whip
refers to an effect's property by its index, effect("Color Control")(1),
instead of its name, ("Color").

prop.pickWhip takes byIndex: the effect keeps its name and each
property inside it is selected by its one-based place among its group's
children (nested groups too), which is what an expression's (1) reads.
Other properties, dimensions ([1]), insertion at the cursor and match
names (Compact English off) are as before. An index still reads a
renamed property, but another one if properties are reordered.

Tests: pick_whip_by_index_selects_effect_properties_by_their_index
(engine, the references); effectcraft-host pick_whip (by index and by
name read the same values, nested and per dimension).
2026-10-13 21:57:19 -04:00
Marco Arnulfo 7df16a5e20 Effects: 4-Color Gradient keeps the layer's alpha with Blending Mode None (refs #1381) 2026-10-14 02:56:17 +02:00
tylerjenningsw 1cea7d774f GPU: test GPU particles against the CPU with a keyed Birth Rate (#1373)
With a Birth Rate keyed into a burst, the GPU's particles match the CPU
simulation through the burst and after it. A second history with the
same system key, whose burst starts later (so the same ids are born at
other steps), isn't resumed from the first history's GPU checkpoints.
2026-10-13 20:45:21 -04:00
tylerjenningsw 8f03f24247 Render: tests for a CC Particle World burst through the renderer (#1373)
The issue's scene: a Birth Rate keyed 3 until 0.5 s then 0 matches the
constant rate during the burst, still draws the burst at 0.9 s with no
new births, and is empty once Longevity has run out. Moving the earlier
key changes the 0.9 s frame through the layer cache. Layers offset by
0.5 s or stretched 200 % sample their own layer times.
2026-10-13 20:45:20 -04:00
tylerjenningsw d5a1268ead CC Particle World: tests for a Birth Rate that changes over time (#1373)
A burst lives on after the rate drops to 0: as a constant rate during
it, only its particles afterwards, still moving, gone after their
Longevity. Another effect's history isn't used. A ramp bears what each
moment asked for. Seeking backward and forward matches a simulation
from 0. Moving an earlier key isn't served from stale checkpoints with
the same current rate. A constant rate is unchanged. The birth schedule
matches the CPU's ids.
2026-10-13 20:45:20 -04:00
tylerjenningsw bb0e2b57db CC Particle World: Birth Rate decides the births of its own time, so a burst lives on (#1373)
The simulation runs from layer time 0, but it used the Birth Rate of the
rendered frame for every step. A rate keyed from 3 down to 0 simulated
no births at all once it reached 0, so the burst's particles vanished
that frame although their Longevity had not run out. A ramped rate was
simulated as if its current value had always applied.

Each step now samples Birth Rate at its own layer time through
EffectHost::param_at (with the effect's own index). Without such a host
the current value applies throughout, as before. Particles already alive
keep moving and ageing whatever the rate. The carry and particle ids
continue across rate changes.

Birth Rate is left out of the simulation cache key, so an animated rate
no longer restarts the simulation every frame. In its place:

- The CPU state keeps a hash of the rates it was stepped with (hist). A
  checkpoint is resumed only when that matches the current history, so
  moving an earlier key is never served from a stale checkpoint.
- The GPU gets the birth schedule of the sampled rates (births_at). Its
  checkpoints record which births they hold, and are resumed only when
  the request's births before that step are the same.

A constant Birth Rate gives exactly the particles it did. CC Particle
Systems II and CC Mr. Mercury keep their constant-rate simulation.
2026-10-13 20:45:20 -04:00
tylerjenningsw a00a76e7ed Particles: a birth schedule from a rate per step (#1373)
psim::births_at builds the GPU's birth schedule (the birth step of every
particle id) from the rate of each step, with the CPU step's carry
arithmetic. births() for a constant rate now uses it. It stops as soon
as the cap would be passed, before adding a step's births, so a huge
rate can't build a huge list first.
2026-10-13 20:45:20 -04:00
tylerjenningsw 2e654f92c1 Effects: SimCache can resume only from checkpoints reached with the same inputs (#1373)
SimCache::run_where takes a check on each checkpoint (its step and
state). A simulation whose steps depend on inputs outside its key, such
as a parameter's values over time, resumes only from a checkpoint
reached with the same inputs. A fresh checkpoint now replaces one at the
same step, so a changed history doesn't leave stale checkpoints behind.
run() is run_where with no check.
2026-10-13 20:45:20 -04:00
tylerjenningsw 7b0771fe5a Effects: a host evaluates one effect parameter at another layer time (#1373)
EffectHost::param_at(effect_index, id, layer_time) evaluates a single
parameter (by its flattened id, such as birthRate or group/param) of the
effect at that index in the layer's stack. A simulation can then sample
one parameter at every step without flattening the whole effect each
time.

The effect index is passed in, rather than read from the renderer's
running-effect index as params_at does: GPU-batched effects never update
that index, so a batched effect would read another effect's parameters.
The renderer's implementation walks the effect group by match id and
evaluates just that property (keyframes and expressions) at the comp
time of the layer time.
2026-10-13 20:45:20 -04:00
DWAA1660andClaude Opus 5.5 e0bb54e4c6 render: hash the comp scene of selected layers that read it into the owner's cache key (#1252)
A layer selected by another layer's effect (Displacement Map, Blend...) had
its structure, values, matrix and time hashed into the owner's key, but not
the comp camera/lights its own scene-reading effects (Caustics, CC Particle
World...) see. The owner's key included the scene only when the owner itself
ran such an effect, and a cached owner never runs the selected layer's
effects, so moving the camera or a light reused stale owner pixels.

Dependencies::layer now hashes comp_scene for every selected (or nested)
layer that reads it, with the same policy as owner keys.

Fixes #1252

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
2026-10-13 18:05:48 -06:00
DWAA1660andClaude Opus 5.5 4b475f5781 model: budget the memory an FBX import keeps, and share instanced meshes (#1234)
The 1 GiB array limit counted wire bytes, but arrays are kept as f64/i64,
so boolean arrays grew 8x and 32-bit arrays 2x past it, and nothing bounded
the total across arrays. Every Model linked to a Geometry also built its own
copy of the mesh.

- A per-import Budget (2 GiB) is charged 8 bytes per array element before
  the array is decompressed or widened, ASCII files are charged for their
  parsed values, and each built mesh is charged for its vertex and index
  data. Going past it fails the import with a clear error instead of an
  allocation abort. parse_within/read_within take a lower limit for tests.
- Models instancing the same geometry with the same materials share one
  mesh; a different material mapping still gets its own.

Fixes #1234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
2026-10-13 17:44:27 -06:00
DWAA1660andClaude Opus 5.5 3d45046132 model: validate glTF accessor spans before allocating, with checked arithmetic (#1235)
Accessor counts sized the f32/u32 output before the buffer view was looked
at, and offsets, strides and count * components used unchecked arithmetic,
so a few hundred bytes of JSON could request a multi-gigabyte allocation
(aborting the process) or overflow.

- Doc::layout checks count * components, adds the view and accessor offsets,
  rejects a byteStride smaller than an element, and checks that the last
  element ends inside the buffer, all with checked arithmetic, before
  floats() or ints() allocate anything.
- Accessors without a buffer view (all zeros) are capped at 2^24 components.
- idx() and node index lists use usize::try_from instead of 'as usize'; an
  image bufferView's end and a GLB chunk's end are computed with checked_add.

Fixes #1235

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
2026-10-13 17:35:25 -06:00
Kristian MyramandClaude Sonnet 5.5 9f02bd2a72 #1368: dragging an effect lights up the layer it will land on
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 00:59:11 +02:00
DWAA1660andClaude Opus 5.5 79ebd01935 svg: bound <use> expansion by an element budget and skip reference cycles (#1236)
Depth-limited <use> expansion still let a few kilobytes of branching
references expand to 2^N elements. The parser now counts every element it
converts, <use> copies included, against a budget of max(500 000, twice the
file's own elements) and fails the import with Error::Xml once it is spent.
Targets currently being expanded are tracked, so mutual references stop at
the first repeat instead of nesting to the depth limit.

Fixes #1236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
2026-10-13 16:59:08 -06:00
Marco Arnulfo dd5afffee3 Scripting: test that Layer.property() resolves AE's top-level shortcuts (#1348) 2026-10-14 00:55:12 +02:00
Kristian MyramandClaude Sonnet 5.5 be3261419b Viewer: Composition Settings… in the composition's right-click menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 00:46:05 +02:00
DWAA1660andClaude Opus 5.5 b26a340a7e Project: comp_contains walks precomps with a visited set, not recursion (#1237)
Project::comp_contains recursed through precomp layers with no visited
set or depth limit. A damaged project with a precomp cycle that doesn't
pass the queried comp (A and B nesting each other, C unrelated) recursed
until the stack ran out, which aborts the process ("stack overflow,
aborting"): it can't be caught as a command error, so unsaved work is
lost. layer.addItem checks it before adding a precomp, and the renderer
calls it for every nested comp and its audio. A long chain used the
stack the same way, and comps shared by many paths were walked once per
path (exponential for nested diamonds).

It is now an explicit stack with a visited set: same answers (a comp
contains itself; a target reached through a cycle is found), each comp
visited once, no recursion. The renderer already skips a precomp that
contains the comp being rendered, so a cyclic project now renders
instead of crashing; nothing is repaired on load.

Tests: project comp_contains_ends_on_cycles_long_chains_and_shared_
precomps (cycle with an unrelated target, self-nesting, a target through
a cycle, 40 levels of diamonds, a 20 000-comp chain); engine
a_project_with_a_precomp_cycle_opens_adds_and_renders_without_running_
out_of_stack (file.open, layer.addItem both ways, render). On main the
engine test aborts with a stack overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:26:42 -06:00
DWAA1660andClaude Opus 5.5 da73df4b2e Script sockets: per-socket locks, a write deadline, and close interrupts (#1248)
Every socket operation locked one global table and held it through the
blocking write_all/flush, and streams had no write timeout (accepted
ones were explicitly blocking). A peer that stopped reading kept the
script blocked for good, and every other socket call, close included,
waited behind it.

- The table maps handles to Arc<Entry>, each with its own Mutex<Sock>.
  The table is locked only to look a socket up, add or remove one,
  never during network I/O, so one stalled socket doesn't hold up the
  others.
- `write` sets a write timeout from the socket's `timeout` (10 s by
  default, as reads) and reports "write timed out … the other side
  isn't reading" instead of blocking. Failures to set the read or write
  timeout, or to make an accepted stream blocking, are reported instead
  of ignored.
- `close` removes the socket and shuts its stream down through a second
  handle, so a call blocked on it fails at once.

Tests (socket.rs, native only): a 64 MB write to a loopback peer that
never reads fails with "timed out" after a 1 s timeout; while one socket
is blocked writing, another answers at once, and closing the blocked one
returns at once and ends its write. Both fail on main (the write never
gives up; the second socket waits on the table lock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:03:53 -06:00
DWAA1660andClaude Opus 5.5 7a9ca5810a Scripts: a script can't confirm executing a file for itself (#1246)
With Warn User When Executing Files on, file.executeFile asks the
frontend first, and the frontend's answer re-runs it with
`confirmed: true`. That flag was taken from the parameters whoever
called, so a script could pass it itself (app.run('file.executeFile',
{path, confirmed: true}), also inside engine.batch) and the file opened
without the question, as long as the user had allowed file and network
access.

`confirmed` now counts only when the command isn't running for a script
(Session::running_for_script, which #590 uses for the same reason and
which stays set through nested batches). A script's request always gets
the question; the user's answer from the frontend's form still opens
the file; with the warning off nothing changes. The permission gate
itself is unchanged.

Tests: engine a_script_cannot_confirm_executing_a_file_for_itself
(execute_for_script directly and through engine.batch; the frontend's
confirmation; warning off) and the same through real JavaScript in the
script crate (app.run, app.run('engine.batch'), File.execute()). Both
fail on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:47:51 -06:00
DWAA1660andClaude Opus 5.5 8408af323a Automation: confine every command that reads or writes a file it is given (#1243)
essential.templateInfo was missing from confine.rs's READS, so its
`path` got no named check, and the fallback only scans absolute-looking
strings: a relative path (or one through `..`) escaped the read roots
and the command parsed and returned a template outside them.

An audit of every registered engine command with a parameter named
like a file found the same gap in more commands, now classified:
- READS: essential.templateInfo, file.importProject,
  file.watchFolder.poll, footage.relink, layer.newModel,
  mediaBrowser.fileInfo / list / import / removeFavorite,
  face.model.install, roto.model.install, templates.create (`path`).
- WRITES: file.exportTimeline, file.createProxy (`path` / `output`,
  which may be a file-name template, as for render.preRender).
- MIXED_WRITES: templates.create writes `projectPath` and `footageDir`.

Tests (confine.rs):
- every_command_with_file_parameters_is_classified fails when a
  command takes a path, paths, folder, file, output... parameter and is
  neither confined, refused, checked by a lookup, a Render Queue output
  (checked when the queue renders) nor listed in NOT_FILES (property
  paths and the like). New file commands can't slip through.
- relative_and_outside_paths_of_newly_listed_commands_are_refused:
  relative, `..` and outside paths are refused and in-root ones allowed,
  directly and in a batch; writes go only to write roots; createProxy
  accepts a name template. It fails on main (templateInfo with
  `t.ectemplate` passes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:21:14 -06:00
Kristian MyramandClaude Sonnet 5.5 7e6d76406b Merge upstream main into fix/1010-orientation-followup
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 22:03:15 +02:00
Kristian MyramandClaude Haiku 5.5 fdc515f5be #1063: find an undefined name's first use from the failing frame on
A ReferenceError's first use was searched across the whole file, so a mention
earlier in a comment, a string or another function could report the wrong line.
The search now starts at the innermost script frame's line, and covers the whole
file only when there is no frame. Follow-up to storytold/effectcraft#1161.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-13 21:58:52 +02:00
Kristian MyramandClaude Sonnet 5.5 dd36eed8e9 Merge upstream main into fix/1063-text-animator-stacked
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 21:56:33 +02:00