From 21cf6e9a1290edf9faacad63e6cfc05cf0c660b8 Mon Sep 17 00:00:00 2001 From: th Date: Fri, 24 Apr 2026 15:23:13 +0200 Subject: [PATCH] 0.18.2: centre crosshair + locked-track selection + eye-on-lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- package.json | 2 +- src/components/CanvasStage.vue | 75 +++++++++- src/components/Timeline.vue | 6 +- src/devtools/tests.ts | 248 +++++++++++++++++++++++++++++++++ src/i18n/messages.ts | 2 + src/stores/scene.ts | 55 ++++++-- src/version.ts | 2 +- 7 files changed, 371 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 4f84adc..163a5cc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruwio", "private": true, - "version": "0.18.1", + "version": "0.18.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/CanvasStage.vue b/src/components/CanvasStage.vue index d9bce85..8c69ab8 100644 --- a/src/components/CanvasStage.vue +++ b/src/components/CanvasStage.vue @@ -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) { + +