From a858f94bb7ba4b57c4ec4decedb6fca8a819a869 Mon Sep 17 00:00:00 2001 From: th Date: Fri, 24 Apr 2026 12:26:57 +0200 Subject: [PATCH] 0.17.0: split SVG layer into per-path layers Right-click an SVG-backed layer to split it into one layer per top-level drawable. Each part keeps the original stage position and ends up as a same-size standalone SVG. Also: delete confirm now floats above the welcome dialog (topmost z-index + scoped ESC/Tab via modal stack) and autofocuses its primary button so Enter triggers confirm instead of bubbling back to the underlying row. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- src/components/Timeline.vue | 30 ++ src/components/WelcomeDialog.vue | 31 +- src/components/ui/DialogsHost.vue | 10 + src/components/ui/Modal.vue | 51 ++- src/devtools/tests.ts | 542 ++++++++++++++++++++++++++---- src/i18n/messages.ts | 1 + src/lib/svgSplit.ts | 150 +++++++++ src/stores/scene.ts | 152 +++++++++ src/version.ts | 2 +- 10 files changed, 887 insertions(+), 84 deletions(-) create mode 100644 src/lib/svgSplit.ts diff --git a/package.json b/package.json index de37f3a..ab2782d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruwio", "private": true, - "version": "0.16.1", + "version": "0.17.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 6226321..d8b9afe 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -24,6 +24,7 @@ import { PhPencilSimpleLine, PhPlus, PhPolygon, + PhScissors, PhSpeakerSlash, PhSquare, PhTextT, @@ -2683,6 +2684,26 @@ function convertSelectedToClipFromMenu() { trackMenu.value = null } +/** True only for a context-menu tied to an SVG-backed image layer. + * Gates the "Split SVG" entry so the menu doesn't show a dead action + * on raster layers. */ +const canSplitSvgFromMenu = computed(() => { + const m = trackMenu.value + if (!m || m.kind !== 'layer') return false + const layer = store.scene.layers.find((l) => l.id === m.trackId) + if (!layer) return false + const asset = store.scene.assets.find((a) => a.id === layer.assetId) + if (!asset || asset.kind !== 'image') return false + return asset.src.startsWith('data:image/svg+xml') +}) + +function splitSvgFromMenu() { + const m = trackMenu.value + if (!m || m.kind !== 'layer') return + store.splitLayerSvg(m.trackId) + trackMenu.value = null +} + function deleteTrack() { if (!trackMenu.value) return if (trackMenu.value.kind === 'layer') { @@ -3976,6 +3997,15 @@ onBeforeUnmount(() => { {{ t('timeline.convertToClip', { n: store.selectedLayerIds.length }) }} +