0.18.2: centre crosshair + locked-track selection + eye-on-lock

- Optional centre crosshair: dashed horizontal + vertical lines
  through the effective canvas centre, togglable from the HUD next
  to the crop / mask-preview / stage-preview buttons. Scales with
  zoom and stays visible inside clip-edit.
- Locked clip tracks now behave as if they weren't on the stage —
  the ci-group is listening:false + draggable:false, so selection,
  drag, and transform all pass through. deselectIfLocked also
  clears a clip-instance selection that just got locked out
  (directly or via a parent group).
- Eye toggle stays clickable on locked layer / clip-track rows —
  previous pointer-events:none rule only whitelisted the lock
  button. Hide-in-editor is orthogonal to lock.

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 15:23:13 +02:00
1 parent 5af0cdb3d4
commit 21cf6e9a12
7 files changed
+371 -19

No files matched your search

+74 -1
View File
@@ -5,6 +5,7 @@ import {
PhCircle,
PhCornersIn,
PhCrop,
PhCrosshair,
PhCursorClick,
PhFrameCorners,
PhMaskHappy,
@@ -56,6 +57,10 @@ const viewX = ref(0)
const viewY = ref(0)
const cropObjects = ref(false)
const maskPreview = ref(false)
// Dashed horizontal + vertical lines through the center of the
// effective canvas — a composition guide the user can toggle on/off
// from the HUD. Not persisted; local to this editor session.
const centerCrosshair = ref(false)
// Stage-preview mode: when false (default) buttons behave like normal
// assets — draggable, no hover/click interaction. When true the canvas
// turns into a live preview with buttons responding to input.
@@ -593,6 +598,41 @@ const clipGroupConfig = computed(() => {
}
})
/** Horizontal + vertical centre-crosshair line configs. Null when
* the toggle is off. Lines span the effective canvas so the centre
* is exactly where layer keyframes default to (canvas.width/2,
* canvas.height/2). Stroke + dash are scaled by `1 / zoom` so they
* stay 1 screen-pixel thick and the dashes stay the same visual
* length at any zoom level. `listening: false` keeps the lines out
* of hit-testing — they're purely a visual guide. */
const centerCrosshairConfigs = computed(() => {
if (!centerCrosshair.value) return null
const w = effectiveCanvas.value.width
const h = effectiveCanvas.value.height
const cx = w / 2
const cy = h / 2
const z = zoom.value || 1
const strokeColor = 'rgba(79, 140, 255, 0.7)'
const strokeWidth = 1 / z
const dash = [6 / z, 4 / z]
return {
horizontal: {
points: [0, cy, w, cy],
stroke: strokeColor,
strokeWidth,
dash,
listening: false,
},
vertical: {
points: [cx, 0, cx, h],
stroke: strokeColor,
strokeWidth,
dash,
listening: false,
},
}
})
const backgroundConfig = computed(() => {
// In button-asset edit: checkerboard-ish transparent background so the
// user sees where the button's canvas ends.
@@ -935,6 +975,20 @@ function isLayerEffectivelyLocked(layer: Layer): boolean {
return false
}
/** Clip-track counterpart to `isLayerEffectivelyLocked`. Returns true
* when the ClipTrack's own `locked` is set OR the track sits inside
* a locked group. Used to disable the ci-group's listening + drag so
* a locked clip behaves as if it weren't on the stage — the user
* can't select, drag, or transform its instance. */
function isClipTrackEffectivelyLocked(trackId: string): boolean {
const track = store.scene.clipTracks?.find((t) => t.id === trackId)
if (track?.locked) return true
for (const g of store.scene.groups ?? []) {
if (g.locked && g.childIds.includes(trackId)) return true
}
return false
}
function isLayerGhostLocked(layer: Layer): boolean {
return isLayerEffectivelyLocked(layer) && !showLocked.value
}
@@ -1280,6 +1334,12 @@ function imageConfig(
* events funnel to the outer group and drag/transform apply as a
* unit. */
function clipInstanceGroupConfig(inst: ClipInstance) {
// A locked clip track must behave on the stage as if its instance
// weren't there at all — clicks pass through, drag is off, and the
// transformer can't latch on. `listening: false` on the group
// cascades to every descendant (frame rect + inner shapes), so a
// single flag covers the selection, drag, and transform paths.
const locked = isClipTrackEffectivelyLocked(inst.clipTrackId)
return {
id: `ci-group:${inst.id}`,
x: inst.x ?? 0,
@@ -1287,7 +1347,8 @@ function clipInstanceGroupConfig(inst: ClipInstance) {
scaleX: inst.scaleX ?? 1,
scaleY: inst.scaleY ?? 1,
rotation: inst.rotation ?? 0,
draggable: true,
draggable: !locked,
listening: !locked,
}
}
@@ -3479,6 +3540,10 @@ function onResizeHandlePointerUp(e: PointerEvent) {
</template>
</v-group>
<v-rect :config="canvasBorderConfig" />
<template v-if="centerCrosshairConfigs">
<v-line :config="centerCrosshairConfigs.horizontal" />
<v-line :config="centerCrosshairConfigs.vertical" />
</template>
<v-rect
v-for="h in canvasResizeHandles"
:key="`canvas-resize-${h.edge}`"
@@ -3627,6 +3692,14 @@ function onResizeHandlePointerUp(e: PointerEvent) {
<PhMaskHappy v-if="!maskPreview" :size="13" />
<PhPencilSimple v-else :size="13" />
</button>
<button
class="zc-btn center-cross-btn"
:class="{ active: centerCrosshair }"
v-tooltip="centerCrosshair ? t('canvas.centerCrosshairOff') : t('canvas.centerCrosshairOn')"
@click="centerCrosshair = !centerCrosshair"
>
<PhCrosshair :size="13" />
</button>
<button
class="zc-btn stage-preview-btn"
:class="{ active: stagePreview }"
+5 -1
View File
@@ -4871,7 +4871,11 @@ onBeforeUnmount(() => {
color: var(--text-dim);
}
.track.locked .track-name .lock-btn {
.track.locked .track-name .lock-btn,
.track.locked .track-name .eye-btn {
/* Keep the eye toggle clickable on locked tracks — muting a layer
in the editor is orthogonal to locking it, and asking the user
to unlock first just to toggle visibility is annoying. */
pointer-events: auto;
}