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) {
+
+
+
+
+