0.15.0: stage context menu + drag-to-resize stage

- Right-clicking the stage opens a small menu with Document settings
  (pops the document drawer) and Resize stage. Closes on outside
  click and ESC.
- Resize stage drops eight accent-coloured drag handles around the
  canvas rect (N/NE/E/SE/S/SW/W/NW). East/south handles just grow or
  shrink the canvas; west/north (and their corners) also shift every
  scene-world coordinate in lock-step so content stays pinned to its
  visible position while the canvas re-aligns around it.
- Enter forces stage-crop on so the user can see where the new edge
  lands; exit restores the previous crop setting. Exit paths: Done
  button in the floating banner, ESC, or the restored crop toggle.
- New store action shiftSceneWorldCoords(dx, dy) covers layer base +
  main/position keyframes + bezier control points + clipInstance x/y
  across every scene page. Clip-library layers, button assets, hit
  shapes and polygon vertex clouds stay put — those live in local
  coord systems, not scene-world space.
- Tests: store coverage (shift across pages, positionKeyframes,
  ctrlX/Y, clipInstance, clip-library untouched, (0,0) no-op) plus UI
  coverage (context-menu items, Resize stage enters mode + renders 8
  handles, ESC and Done both exit, crop forced on and restored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-24 09:10:54 +02:00
1 parent 9fe86b62b9
commit 827d80bfd0
6 files changed
+847 -3

No files matched your search

+40
View File
@@ -776,6 +776,46 @@ export const useSceneStore = defineStore('scene', {
this.project.canvas.width = Math.max(1, Math.round(width))
this.project.canvas.height = Math.max(1, Math.round(height))
},
/** Shift every scene-world-space coordinate across the whole project
* by (dx, dy). Used by the stage resize drag: when grabbing the
* left/top edge the canvas origin effectively slides, and every
* element needs to slide the same amount to stay visually pinned.
*
* What shifts:
* - layer.base.{x,y}
* - each keyframe.props.{x,y} on the main track
* - each keyframe.props.{x,y} on separated position track
* - keyframe.ctrlX / ctrlY (bezier handles live in world space)
* - clipInstance.{x,y} (static per-instance offset)
*
* What stays put: clip-library layers (local to the clip instance
* origin), button-asset internals (their own canvas), polygon point
* clouds on keyframe.props.points (local to the layer origin). */
shiftSceneWorldCoords(dx: number, dy: number) {
if (dx === 0 && dy === 0) return
const shiftProps = (props: Partial<AnimatableProps> | undefined) => {
if (!props) return
if (typeof props.x === 'number') props.x += dx
if (typeof props.y === 'number') props.y += dy
}
const shiftKeyframe = (kf: Keyframe) => {
shiftProps(kf.props)
if (typeof kf.ctrlX === 'number') kf.ctrlX += dx
if (typeof kf.ctrlY === 'number') kf.ctrlY += dy
}
for (const page of this.project.scenes) {
for (const layer of page.layers) {
shiftProps(layer.base)
for (const kf of layer.keyframes ?? []) shiftKeyframe(kf)
for (const kf of layer.positionKeyframes ?? []) shiftKeyframe(kf)
}
for (const ci of page.clipInstances ?? []) {
if (typeof ci.x === 'number') ci.x += dx
if (typeof ci.y === 'number') ci.y += dy
}
}
},
setCanvasBackground(color: string) {
this.project.canvas.background = color
},