macro-clips slice 4d: button canvas runtime + pointer handling
- ButtonInstance gains optional x/y; new instances default to stage center and setButtonInstancePosition updates them - useButtonRuntime composable holds per-instance state/disabled overrides (runtime-only, not persisted). getState layers the runtime disabled override on top of the persisted initial flag. - store.actionRuntime's setButtonState/setButtonDisabled now drive useButtonRuntime instead of mutating the scene, so script-triggered state changes stay ephemeral - CanvasStage renders each visible (playhead >= startTime) button instance inside a <v-group> at (bi.x, bi.y), drawing the current state's layer stack with draggable/listening disabled plus a transparent hit-rect sized to the clip's hitArea or the bbox of the first rect/circle layer. The group wires mouseenter/leave/ down/up into the state machine and fires onClick via evalAction. - exportPlayer runtime mirrors the same thing: getEffectiveButtonState, per-frame buttonHits array, canvas-level mousemove/down/up handlers do their own hit testing in scene space. RUNTIME_VERSION bumped to 5. - tests cover useButtonRuntime defaults/overrides and the store.actionRuntime → runtime-state roundtrip via label resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
024883943b
commit
f61f90d613
6 files changed
+412
-8
No files matched your search
@@ -7,7 +7,19 @@ import { sampleLayer } from '@/lib/interpolate'
|
||||
import { importImageFile, loadImage } from '@/lib/fileLoader'
|
||||
import { useClipartStore } from '@/stores/clipart'
|
||||
import { drawSceneFrame, loadSceneImages } from '@/lib/render'
|
||||
import type { AnimatableProps, Asset, Keyframe, Layer, Point } from '@/types/scene'
|
||||
import type {
|
||||
Action,
|
||||
AnimatableProps,
|
||||
Asset,
|
||||
ButtonClip,
|
||||
ButtonInstance,
|
||||
ButtonState,
|
||||
Keyframe,
|
||||
Layer,
|
||||
Point,
|
||||
} from '@/types/scene'
|
||||
import { useButtonRuntime } from '@/composables/useButtonRuntime'
|
||||
import { evalAction } from '@/lib/actions'
|
||||
import { t } from '@/i18n'
|
||||
import { useKfSelection } from '@/composables/useKfSelection'
|
||||
import { showLocked } from '@/composables/useShowLocked'
|
||||
@@ -181,6 +193,134 @@ 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 }
|
||||
}
|
||||
|
||||
const activeButtonInstances = computed<ButtonRenderItem[]>(() => {
|
||||
const out: ButtonRenderItem[] = []
|
||||
const instances = store.scene.buttonInstances ?? []
|
||||
const clips = store.scene.buttonClips ?? []
|
||||
const clipById = new Map(clips.map((c) => [c.id, c]))
|
||||
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 layers = clip.states[state] ?? []
|
||||
const rendered: ButtonRenderItem['layers'] = []
|
||||
for (const layer of layers) {
|
||||
const asset = store.scene.assets.find((a) => a.id === layer.assetId)
|
||||
if (!asset) continue
|
||||
rendered.push({ layer, asset, props: sampleLayer(layer, 0) })
|
||||
}
|
||||
// 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 onButtonMouseEnter(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'normal') buttonRt.setState(bi.id, 'hover')
|
||||
}
|
||||
|
||||
function onButtonMouseLeave(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'hover' || cur === 'pressed') buttonRt.setState(bi.id, 'normal')
|
||||
}
|
||||
|
||||
function onButtonMouseDown(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur === 'hover') buttonRt.setState(bi.id, 'pressed')
|
||||
}
|
||||
|
||||
function onButtonMouseUp(bi: ButtonInstance) {
|
||||
const cur = buttonRt.getState(bi.id, bi.disabled)
|
||||
if (cur !== 'pressed') return
|
||||
buttonRt.setState(bi.id, 'hover')
|
||||
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)
|
||||
@@ -2346,6 +2486,42 @@ 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" />
|
||||
|
||||
Reference in new issue
Block a user