revert macro-clips button feature (keep markers + scripts)

The button authoring ended up too far from the intended model after
three correction passes. Pulling it out entirely so we can rebuild
from a clean spec. Markers, script tracks, script frames, the Action
editor, the Script DSL and the runtime for all of the above stay —
they work.

Removed:
- types: ButtonState, StateTransition, ButtonHitArea, ButtonClip,
  ButtonInstance, setButtonState/setButtonDisabled Action variants,
  Scene.buttonClips/buttonInstances
- store: activeButtonClip* getters, all add/remove/setButton* actions,
  seedDefaultButtonClip, enterButtonEdit/setActiveButtonState/
  exitButtonEdit, button-label resolution in actionRuntime
- useButtonRuntime composable, buttonBlend helper,
  ButtonInstanceDialog, sidebar Buttons section, Timeline button
  rendering + pointer handlers, CanvasStage button rendering +
  pointer state machine, exportPlayer button runtime + hit-testing
- DSL commands state/enable/disable and their parse errors
- 35 button-related tests + helpers; stale testing doc; stale design
  doc

Re-applied three uncommitted fixes that were unrelated to the button
work:
- clicking a marker no longer jumps the playhead (only drag / dblclick
  act on it)
- script tracks get a right-click context menu (rename + delete,
  color in the existing palette)
- onTracksPointerDown excludes .script-lane / .script-frame so
  clicks on empty lanes or on frame dots reach the frame-open path
  instead of being swallowed by box-select pointer capture

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 23:37:49 +02:00
1 parent 616973b0fa
commit 31604052ca
16 files changed
+84 -2809

No files matched your search

-207
View File
@@ -8,22 +8,12 @@ import { importImageFile, loadImage } from '@/lib/fileLoader'
import { useClipartStore } from '@/stores/clipart'
import { drawSceneFrame, loadSceneImages } from '@/lib/render'
import type {
Action,
AnimatableProps,
Asset,
ButtonClip,
ButtonInstance,
ButtonState,
Keyframe,
Layer,
Point,
} from '@/types/scene'
import {
applyButtonStateChange,
useButtonRuntime,
} from '@/composables/useButtonRuntime'
import { evalAction } from '@/lib/actions'
import { blendButtonStates } from '@/lib/buttonBlend'
import { t } from '@/i18n'
import { useKfSelection } from '@/composables/useKfSelection'
import { showLocked } from '@/composables/useShowLocked'
@@ -197,167 +187,6 @@ const canvasBorderConfig = computed(() => ({
const zoomPercent = computed(() => Math.round(zoom.value * 100))
const buttonRt = useButtonRuntime()
interface ButtonRenderItem {
bi: ButtonInstance
clip: ButtonClip
state: ButtonState
disabled: boolean
layers: Array<{
layer: Layer
asset: Asset
props: AnimatableProps
}>
hit: { width: number; height: number; offsetX: number; offsetY: number }
}
function sampleStateLayers(
clip: ButtonClip,
state: ButtonState,
): ButtonRenderItem['layers'] {
const layers = clip.states[state] ?? []
const out: ButtonRenderItem['layers'] = []
for (const layer of layers) {
const asset = store.scene.assets.find((a) => a.id === layer.assetId)
if (!asset) continue
out.push({ layer, asset, props: sampleLayer(layer, 0) })
}
return out
}
function blendStates(
clip: ButtonClip,
from: ButtonState,
to: ButtonState,
u: number,
): ButtonRenderItem['layers'] {
return blendButtonStates(clip, from, to, u, (id) =>
store.scene.assets.find((a) => a.id === id),
)
}
const activeButtonInstances = computed<ButtonRenderItem[]>(() => {
// Force re-evaluation on every transition RAF tick.
void buttonRt.transitionTick.value
const out: ButtonRenderItem[] = []
const instances = store.scene.buttonInstances ?? []
const clips = store.scene.buttonClips ?? []
const clipById = new Map(clips.map((c) => [c.id, c]))
const now = performance.now()
for (const bi of instances) {
if (store.playhead < bi.startTime) continue
const clip = clipById.get(bi.buttonClipId)
if (!clip) continue
const disabled = buttonRt.isDisabled(bi.id, bi.disabled)
const state = buttonRt.getState(bi.id, bi.disabled)
const tr = buttonRt.sampleTransition(bi.id, now)
let rendered: ButtonRenderItem['layers']
if (tr && !tr.done && !disabled) {
rendered = blendStates(clip, tr.from, tr.to, tr.u)
} else {
rendered = sampleStateLayers(clip, state)
}
// Hit area: explicit hitArea on the clip, or fall back to the bbox of
// the first rect-like asset in the normal state.
let hit = clip.hitArea
? {
width: clip.hitArea.width,
height: clip.hitArea.height,
offsetX: clip.hitArea.offsetX ?? clip.hitArea.width / 2,
offsetY: clip.hitArea.offsetY ?? clip.hitArea.height / 2,
}
: null
if (!hit) {
for (const l of clip.states.normal) {
const a = store.scene.assets.find((x) => x.id === l.assetId)
if (!a) continue
if (a.kind === 'rect' || a.kind === 'image') {
hit = {
width: a.width,
height: a.height,
offsetX: l.offsetX ?? a.width / 2,
offsetY: l.offsetY ?? a.height / 2,
}
break
}
if (a.kind === 'circle') {
hit = {
width: a.radius * 2,
height: a.radius * 2,
offsetX: (l.offsetX ?? 0) + a.radius,
offsetY: (l.offsetY ?? 0) + a.radius,
}
break
}
}
}
if (!hit) hit = { width: 120, height: 40, offsetX: 60, offsetY: 20 }
out.push({ bi, clip, state, disabled, layers: rendered, hit })
}
return out
})
function buttonGroupConfig(bi: ButtonInstance) {
return {
id: `btn:${bi.id}`,
x: bi.x ?? 0,
y: bi.y ?? 0,
listening: true,
}
}
function buttonHitRectConfig(item: ButtonRenderItem) {
return {
x: 0,
y: 0,
width: item.hit.width,
height: item.hit.height,
offsetX: item.hit.offsetX,
offsetY: item.hit.offsetY,
fill: 'rgba(0,0,0,0.001)', // nearly transparent but hittable
listening: !item.disabled,
name: 'button-hit',
}
}
function buttonChildConfig<T extends Record<string, unknown>>(cfg: T): T {
return { ...cfg, draggable: false, listening: false }
}
function transitionsFor(bi: ButtonInstance) {
return store.scene.buttonClips?.find((c) => c.id === bi.buttonClipId)?.transitions
}
function onButtonMouseEnter(bi: ButtonInstance) {
const cur = buttonRt.getState(bi.id, bi.disabled)
if (cur === 'normal') applyButtonStateChange(bi.id, 'hover', transitionsFor(bi), bi.disabled)
}
function onButtonMouseLeave(bi: ButtonInstance) {
const cur = buttonRt.getState(bi.id, bi.disabled)
if (cur === 'hover' || cur === 'pressed')
applyButtonStateChange(bi.id, 'normal', transitionsFor(bi), bi.disabled)
}
function onButtonMouseDown(bi: ButtonInstance) {
const cur = buttonRt.getState(bi.id, bi.disabled)
if (cur === 'hover') applyButtonStateChange(bi.id, 'pressed', transitionsFor(bi), bi.disabled)
}
function onButtonMouseUp(bi: ButtonInstance) {
const cur = buttonRt.getState(bi.id, bi.disabled)
if (cur !== 'pressed') return
applyButtonStateChange(bi.id, 'hover', transitionsFor(bi), bi.disabled)
const actions = bi.onClick
if (actions && actions.length) runButtonActions(actions)
}
function runButtonActions(actions: Action[]) {
const rt = store.actionRuntime()
for (const a of actions) evalAction(a, rt)
}
const assetMap = computed(() => {
const m = new Map<string, Asset>()
for (const a of store.scene.assets) m.set(a.id, a)
@@ -2523,42 +2352,6 @@ onBeforeUnmount(() => {
/>
</template>
</template>
<template v-for="item in activeButtonInstances" :key="`bi-${item.bi.id}`">
<v-group
:config="buttonGroupConfig(item.bi)"
@mouseenter="onButtonMouseEnter(item.bi)"
@mouseleave="onButtonMouseLeave(item.bi)"
@mousedown="onButtonMouseDown(item.bi)"
@mouseup="onButtonMouseUp(item.bi)"
>
<v-rect :config="buttonHitRectConfig(item)" />
<template
v-for="(entry, bli) in item.layers"
:key="`${item.bi.id}-${entry.layer.id}-${bli}`"
>
<v-rect
v-if="entry.asset.kind === 'rect'"
:config="buttonChildConfig(rectConfig(entry.layer, entry.asset, entry.props))"
/>
<v-circle
v-else-if="entry.asset.kind === 'circle'"
:config="buttonChildConfig(circleConfig(entry.layer, entry.asset, entry.props))"
/>
<v-text
v-else-if="entry.asset.kind === 'text'"
:config="buttonChildConfig(textConfig(entry.layer, entry.asset, entry.props))"
/>
<v-line
v-else-if="entry.asset.kind === 'polygon'"
:config="buttonChildConfig(polygonConfig(entry.layer, entry.asset, entry.props))"
/>
<v-image
v-else-if="entry.asset.kind === 'image' && imageConfig(entry.layer, entry.asset, entry.props)"
:config="buttonChildConfig(imageConfig(entry.layer, entry.asset, entry.props)!)"
/>
</template>
</v-group>
</template>
</v-group>
<v-rect :config="canvasBorderConfig" />
<v-line v-if="rotationArmConfig" :config="rotationArmConfig" />