Files
RUWIO/plan-svg-draw.txt
T
th d03fbe4ecb 1.2.0: SVG asset editor + end keyframe type + UX bundle
This release bundles three independently-useful pieces of work that
overlap heavily in shared files (types, store, i18n, App, CanvasStage,
tests), so they ship together.

SVG asset editor
- New asset kind 'svg' (SvgAsset { canvas, layers }), authored from
  inside the existing shape primitives (rect / circle / text / polygon /
  freeform). "+" button under the Images sidebar header; opens a
  sub-editor that swaps the library sidebar for a dedicated layer
  panel, hides the timeline, and uses the same canvas-resize flow
  ("Edit canvas size" via right-click → no Document settings entry
  while in scope). Children carry a single t=0 keyframe each.
- Multi-select rotation in SVG scope only: a temporary group pivot
  appears at the selection centroid (draggable), rotation around it
  shifts each layer's x/y/rotation. Pivot follows the centroid drift
  on Konva-transformer scale/move. Outside SVG, transformer
  rotateEnabled stays off (group-rotate write-back is unreliable
  per prior feedback).
- Authored SVGs are placed as layers in the main scene and rendered
  via drawMaskedGroup at t=0; thumbnails surface in the sidebar via
  renderSvgThumbnail. Drop / click / drag / dbl-click-to-edit work
  via a transparent svg-hit catcher rect routed in the stage handler.
- Freeform default stroke is red (#e23b3b) so the open path is
  visible immediately. addDrawingPoint resolves the layer through
  activeLayers so freeform draws correctly inside SVG and clip
  scopes too.

End keyframe type
- Keyframe.kind: 'normal' | 'end'. End kfs carry no props, mark the
  visibility cap. layerActiveRange / isLayerActiveAt / sampleKeyframeSet
  ignore them for interpolation and use them only as the visible-from
  / visible-until bounds.
- One-time migration in normalizeProject synthesizes an end kf at the
  time of the last normal kf for layers that relied on the legacy
  "last kf = disappears" semantics; layers using hold:true to extend
  visibility past the end skip the migration. Idempotent.
- Timeline UI: new "+end" toolbar button (multi-select fan-out, toggle),
  "Convert to end" / "Back to normal" entries in the kf context menu
  (easing / still-frame / duplicate hidden for end kfs), end kfs
  render as a flat axis-aligned square (vs. the diamond-rotated normal
  kf). Render filter drops end kfs that collide with a normal kf at
  the same time so legacy migrated projects don't read as four-point
  stars.

UX polish
- TAB-collapse stage anchoring: the canvas content stays put when the
  asset / properties asides toggle (CanvasStage's ResizeObserver
  compensates viewX/Y by the container's bounding-rect drift).
- Topbar clipboard button copies the current frame to the system
  clipboard as a PNG via navigator.clipboard.write([ClipboardItem]).
- chromeClick predicate adds .timeline to the editing-panels bail so
  the synthetic click after a Timeline box-select drag no longer
  wipes the just-committed kf selection.
- Right-click on a polygon vertex stops propagation so the vertex
  context menu wins instead of the generic stage menu opening on top.
2026-04-25 21:45:58 +02:00

69 lines
5.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan — SVG asset authoring view
## Concept
A new asset kind `svg` (sibling to `button`) with its own sub-editor scope. Created from a "+" button under the Images section. The sub-editor reuses the canvas + shape primitives but has no time axis: each sub-layer is a single static frame.
## 1. Data model — `src/types/scene.ts`
- Extend `AssetKind` union with `'svg'`.
- Add `SvgAsset { id; kind: 'svg'; name; canvas: { width; height; background? }; layers: Layer[] }`.
- Add to the `Asset` discriminated union.
- Layers reuse the existing `Layer` type — single keyframe at `t=0` is all the renderer/sampler needs.
## 2. Store scope — `src/stores/scene.ts`
- New state: `activeSvgAssetId: string | null`.
- Actions (mirroring the button pair):
- `addSvgAsset(name, canvas)` → creates asset, returns it.
- `enterSvgAssetEdit(assetId)` → sets `activeSvgAssetId`, clears layer / clip / button selections, parks playhead at 0.
- `exitSvgAssetEdit()` → nulls scope + selection.
- The existing `addLayer(assetId)` and `addAsset(...)` already write into the active scope (button / clip / main); extend the same scope-resolver so `activeSvgAssetId` is recognised. That means `addSampleRect / addSampleCircle / addSampleText / addSamplePolygon / startFreeformDrawing` all work unchanged inside the SVG scope.
- Block keyframe-creating paths inside SVG scope (`upsertKeyframe`, the "+ keyframe" button, arrow-nudge keyframe writes) — silently bake into the single frame at `t=0` instead of inserting a new one. Cheaper and cleaner than scrubbing them out post-hoc.
## 3. AssetSidebar — `src/components/AssetSidebar.vue`
- Add a `section-action` "+" button to the Images section header (same pattern as Clips/Buttons section).
- Handler: prompt for name (and maybe canvas size — default `300×300`) → `addSvgAsset(...)` → `enterSvgAssetEdit(asset.id)`.
- SVG assets show up in the Images section list as their own row (icon + name + click-to-edit + drag-to-stage to place a layer that references the asset). Pattern matches Buttons section.
## 4. Sub-editor wiring — `src/App.vue`
- Scope footer: add a third branch alongside `activeClipId` / `activeButtonAssetId`. SvgAsset name + back button → `exitSvgAssetEdit()`.
- **Layout swap** (the "left side replaces library" bit): when `activeSvgAssetId` is truthy:
- The `.sidebar` slot renders a new `SvgLayerPanel` instead of `AssetSidebar`.
- The `.timeline` footer area is collapsed / hidden (toggle a `panels-no-timeline` class on `.app` that flips `--timeline-height` to 0 and skips rendering Timeline). Same machinery the TAB-panels feature uses, so the visual transition is consistent.
- Header transport (play/pause/rewind/playhead) stays but should be disabled in SVG scope — there's nothing to play. Easiest: gate them on `!activeSvgAssetId`.
## 5. New component — `src/components/SvgLayerPanel.vue`
A purpose-built layer list. Cannot extract from Timeline — its track-name column is too coupled to the time axis. Surface area:
- One row per layer in `asset.layers`, top = front.
- Per row: type icon, inline-rename name, eye toggle (`editorHidden`), lock toggle, delete.
- Drag-to-reorder calling `moveLayer`.
- Top toolbar with the same shape-creation buttons currently in AssetSidebar (rect / circle / text / polygon / freeform). Reuse the existing `addSample*` handlers — they already obey active scope once step 2 lands.
- Selection state: existing `selectedLayerIds` works as-is.
## 6. CanvasStage — `src/components/CanvasStage.vue`
- Already drives off `activeButtonAssetId` for the button stage. Add a parallel branch for `activeSvgAssetId` that resolves the source layers + canvas size from the SVG asset.
- Skip the playhead animation: sample layers at `t=0` regardless of `store.playhead`. (Or: lock playhead to 0 in SVG scope so existing sampling Just Works.)
- Disable / hide UI not relevant to authoring statics: keyframe overlays, in/out brackets, scrub guidance.
## 7. Rendering the SVG asset back in the main scene — `src/lib/render.ts`
When a layer references an `SvgAsset`, draw its sub-layers via the existing layer/group draw path at `t=0`, scaled into the asset's `canvas` rect (same idea as `drawButtonFrame`). New helper `drawSvgFrame(ctx, asset, scene, images)` — small, reuses `drawMaskedGroup` like the button code does.
## 8. Persistence
No work. `SvgAsset.layers` is just `Layer[]`; goes through `projectForSave` → JSON → `.ruw` round-trip transparently. The only gate is the `AssetKind` union — once that's extended, save/load + zip/unzip + scene-clone paths all continue to work unchanged.
## 9. i18n
Strings: section-action button label/tooltip, scope footer label, layer-panel toolbar tooltips, the new-svg prompt copy. EN + DE.
## 10. Tests (devtools/tests.ts)
- Store: `addSvgAsset` shape, `enterSvgAssetEdit` flips scope + clears selection, `exitSvgAssetEdit` reverses it, `addLayer` inside SVG scope appends to `asset.layers`.
- Sidebar: "+" button under Images creates an asset and enters edit mode.
- App: while `activeSvgAssetId`, sidebar shows the layer panel and the timeline area is hidden.
- Render: a scene containing a layer referencing an SVG asset draws the asset's child shapes (snapshot via canvas pixel sample, like the existing button rendering tests).
- Round-trip: save + reload preserves `kind: 'svg'` and the nested `layers`.
## 11. Decisions (confirmed)
- **Canvas size on creation**: inherit from current scene; user can resize later via the existing resize button.
- **Background**: transparent.
- **Placement after creation**: drop user straight into the sub-editor.
- **Existing keyframe data**: strip on entry — SVG sub-layers carry a single t=0 keyframe.
Implementation order: 1–2 (types + store) → 3 + 4 (sidebar entry + scope swap) → 5 (layer panel) → 6 (canvas stage scope) → 7 (main-scene render) → 8 (i18n) → 9 (tests).