openpencil/src/components/EditorCanvas.vue
Danila Poyarkov f44f06f721 Refactor: extract business logic from components, clean composables
Tier 1 — Core API fixes:
- updateNodeWithUndo now calls requestRender internally, eliminating
  15+ redundant requestRender() calls across 6 property panel components
- New editor/variables.ts: 7 undo-able variable CRUD operations
  (renameCollection, addCollection, removeCollection, addVariable,
  removeVariable, renameVariable, updateVariableValue)
- New editor/alignment.ts: alignNodes, flipNodes, rotateNodes —
  moves bounding box geometry out of PositionSection.vue

Tier 2 — Composable cleanup:
- Merged use-node-props + use-multi-props into single useNodeProps()
  with MIXED sentinel, eliminating duplicate computed refs
- Added toolCursor() utility — replaces if/else chain in EditorCanvas
- Renamed use-toast.ts → toast.ts (singleton module, not a composable)

Tier 3 — use-canvas-input.ts split (1497 → 817 lines):
- input/types.ts (88) — DragState variants, HandlePosition, TOOL_TO_NODE
- input/geometry.ts (170) — hit testing, handle positions, rotation cursor
- input/pan-zoom.ts (266) — wheel, touch pinch, Safari gestures
- input/resize.ts (121) — constrainToAspectRatio, applyResize
- input/auto-layout.ts (103) — insert indicator computation

All 955 tests pass, 0 lint errors, 1.48% duplication.
2026-03-16 20:35:17 +03:00

95 lines
3 KiB
Vue

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useCanvas } from '@/composables/use-canvas'
import { useCanvasDrop } from '@/composables/use-canvas-drop'
import { useCanvasInput } from '@/composables/use-canvas-input'
import { useCollabInjected } from '@/composables/use-collab'
import { useTextEdit } from '@/composables/use-text-edit'
import { useEditorStore } from '@/stores/editor'
import { toolCursor } from '@open-pencil/vue'
import CanvasContextMenu from './CanvasContextMenu.vue'
const store = useEditorStore()
const collab = useCollabInjected()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas(
canvasRef,
store
)
const { cursorOverride } = useCanvasInput(
canvasRef,
store,
hitTestSectionTitle,
hitTestComponentLabel,
hitTestFrameTitle,
(cx, cy) => collab?.updateCursor(cx, cy, store.state.currentPageId)
)
useTextEdit(canvasRef, store)
const { isDraggingOver } = useCanvasDrop(canvasRef, store)
watch(
() => [...store.state.selectedIds],
(ids) => collab?.updateSelection(ids)
)
const cursor = computed(() => toolCursor(store.state.activeTool, cursorOverride.value))
</script>
<template>
<CanvasContextMenu>
<div
data-test-id="canvas-area"
class="canvas-area relative min-h-0 min-w-0 flex-1 overflow-hidden"
>
<canvas
ref="canvasRef"
data-test-id="canvas-element"
:style="{ cursor }"
class="block size-full touch-none"
/>
<Transition
enter-active-class="transition-opacity duration-150"
enter-from-class="opacity-0"
leave-active-class="transition-opacity duration-150"
leave-to-class="opacity-0"
>
<div
v-if="isDraggingOver"
class="pointer-events-none absolute inset-0 z-40 border-2 border-dashed border-accent/60 bg-accent/5"
/>
</Transition>
<Transition leave-active-class="transition-opacity duration-300" leave-to-class="opacity-0">
<div
v-if="store.state.loading"
data-test-id="canvas-loading"
class="absolute inset-0 z-50 flex items-center justify-center bg-canvas"
>
<svg
class="size-8 text-white opacity-40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="m15.232 5.232 3.536 3.536m-2.036-5.036a2.5 2.5 0 0 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732Z"
/>
</svg>
<div
class="absolute bottom-1/2 left-1/2 h-0.5 w-25 -translate-x-1/2 translate-y-10 overflow-hidden rounded-full bg-white/8"
>
<div
class="h-full w-2/5 animate-[slide_1s_ease-in-out_infinite] rounded-full bg-white/25"
/>
</div>
</div>
</Transition>
</div>
</CanvasContextMenu>
</template>