tests: fill macro-clips coverage gaps (+35 tests)
Refactor + new tests triggered by an explicit coverage audit of the
macro-clips feature set.
- extracted blendButtonStates into src/lib/buttonBlend.ts so it can be
tested without mounting CanvasStage; the component now delegates to
it. Behaviour unchanged.
- store coverage: removeMarker / setMarkerTime / setMarkerColor on
unknown ids are no-ops, addMarker accepts an initial color,
removeScriptFrame / setScriptFrameActions / setScriptFrameTime on
unknown ids are no-ops, removeButtonInstance keeps the clip,
addButtonInstance defaults x/y to stage center,
setButtonInstancePosition writes through, setButtonInstanceLabel('')
clears, setButtonClipTransitions stores + deletes.
- button runtime: applyButtonStateChange is a no-op when the target
equals the current state, honors the persisted disabled flag as the
from-state, reset() clears transitions alongside state/disabled.
store.actionRuntime warns on unknown button refs.
- blendButtonStates: matched layer lerps props (incl. fill color),
unmatched from-layer fades out, unmatched to-layer fades in,
u=0/u=1 boundary conditions, missing asset id resolves are skipped.
- DSL edge cases: empty source, comments-only, trailing blank lines,
wrong state ref prefix, disable without arg, jump @ without name.
- tick firing robustness: multi-track frames fire in time order,
actions within a frame fire in list order, scriptFramesInRange
open-closed boundary is exact.
Macro-clips suites now total 76 tests across 11 groups; grand total
277 tests in the runner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6471c446df
commit
22b553e931
3 files changed
+394
-40
No files matched your search
@@ -0,0 +1,55 @@
|
||||
import type { AnimatableProps, Asset, ButtonClip, ButtonState, Layer } from '@/types/scene'
|
||||
import { lerpProps, sampleLayer } from '@/lib/interpolate'
|
||||
|
||||
export interface BlendedLayerRender {
|
||||
layer: Layer
|
||||
asset: Asset
|
||||
props: AnimatableProps
|
||||
}
|
||||
|
||||
/**
|
||||
* Blend the "from" state's layers into the "to" state's layers by layer
|
||||
* name. Matched pairs lerp their sampled (t=0) props; unmatched
|
||||
* from-layers fade out (opacity × (1 − u)), unmatched to-layers fade in
|
||||
* (opacity × u). Used by the canvas renderer and the export runtime
|
||||
* while a button state transition is in progress.
|
||||
*
|
||||
* `u` is expected to be post-easing progress ∈ [0, 1].
|
||||
*/
|
||||
export function blendButtonStates(
|
||||
clip: ButtonClip,
|
||||
from: ButtonState,
|
||||
to: ButtonState,
|
||||
u: number,
|
||||
assetBy: (id: string) => Asset | undefined,
|
||||
): BlendedLayerRender[] {
|
||||
const fromLayers = clip.states[from] ?? []
|
||||
const toLayers = clip.states[to] ?? []
|
||||
const fromByName = new Map<string, Layer>()
|
||||
const toByName = new Map<string, Layer>()
|
||||
for (const l of fromLayers) fromByName.set(l.name, l)
|
||||
for (const l of toLayers) toByName.set(l.name, l)
|
||||
const out: BlendedLayerRender[] = []
|
||||
// Unmatched from-layers render first (behind to-layers) and fade out.
|
||||
for (const l of fromLayers) {
|
||||
if (toByName.has(l.name)) continue
|
||||
const asset = assetBy(l.assetId)
|
||||
if (!asset) continue
|
||||
const base = sampleLayer(l, 0)
|
||||
out.push({ layer: l, asset, props: { ...base, opacity: base.opacity * (1 - u) } })
|
||||
}
|
||||
for (const l of toLayers) {
|
||||
const asset = assetBy(l.assetId)
|
||||
if (!asset) continue
|
||||
const fromLayer = fromByName.get(l.name)
|
||||
if (fromLayer) {
|
||||
const a = sampleLayer(fromLayer, 0)
|
||||
const b = sampleLayer(l, 0)
|
||||
out.push({ layer: l, asset, props: lerpProps(a, b, u) })
|
||||
} else {
|
||||
const base = sampleLayer(l, 0)
|
||||
out.push({ layer: l, asset, props: { ...base, opacity: base.opacity * u } })
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user