diff --git a/components/CanvasEditor.vue b/components/CanvasEditor.vue index f23b738..68dcce7 100644 --- a/components/CanvasEditor.vue +++ b/components/CanvasEditor.vue @@ -4,6 +4,7 @@ import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue'; import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue'; import { SnapFinder, type SnapHint } from '#shared/physics.util'; import type { CanvasPreferences } from '~/types/general'; +import Id from '~/pages/user/[id].vue'; export type Element = { type: 'node' | 'edge', id: string }; interface ActionMap { @@ -68,7 +69,7 @@ const props = defineProps<{ const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5); const focusing = ref(), editing = ref(); -const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'), toolbarRef = useTemplateRef('toolbarRef'), viewportSize = useElementSize(canvasRef); +const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'), toolbarRef = useTemplateRef('toolbarRef'), viewportSize = useElementBounding(canvasRef); const nodes = useTemplateRef('nodes'), edges = useTemplateRef('edges'); const canvasSettings = useCookie('canvasPreference', { default: () => ({ gridSnap: true, neighborSnap: true }) }); const hints = ref([]); @@ -78,7 +79,8 @@ const viewport = computed(() => { return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, w: width, h: height }; }); -const fakeEdge = ref<{ original?: string, from?: Position, fromSide?: Direction, to?: Position, toSide?: Direction, path?: Path, style?: { stroke: string, fill: string }, hex?: string, dragFrom?: string, snapped?: { node: string, side: Direction } }>({}); +type DragOrigin = { type: 'edge', id: string, destination: 'from' | 'to', node: string } | { type: 'node', id: string }; +const fakeEdge = ref<{ from?: Position, fromSide?: Direction, to?: Position, toSide?: Direction, path?: Path, style?: { stroke: string, fill: string }, hex?: string, drag?: DragOrigin, snapped?: { node: string, side: Direction } }>({}); const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined); let snapFinder: SnapFinder; @@ -118,7 +120,6 @@ function addAction(event: T, actions: HistoryAction { let lastX = 0, lastY = 0, lastDistance = 0; - let box = canvasRef.value?.getBoundingClientRect()!; const dragMove = (e: MouseEvent) => { dispX.value = dispX.value - (lastX - e.clientX) / zoom.value; dispY.value = dispY.value - (lastY - e.clientY) / zoom.value; @@ -143,7 +144,6 @@ onMounted(() => { document.removeEventListener('gesturechange', cancelEvent); }); }) - window.addEventListener('resize', () => box = canvasRef.value?.getBoundingClientRect()!); canvasRef.value?.addEventListener('mousedown', (e) => { if(e.button === 1) { @@ -159,7 +159,7 @@ onMounted(() => { return; const diff = Math.exp(e.deltaY * -0.001); - const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2); + const centerX = (viewportSize.x.value + viewportSize.width.value / 2), centerY = (viewportSize.y.value + viewportSize.height.value / 2); const mousex = centerX - e.clientX, mousey = centerY - e.clientY; dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value); @@ -364,7 +364,7 @@ function dragEdgeTo(e: MouseEvent): void (fakeEdge.value.to as Position).x += e.movementX / zoom.value; (fakeEdge.value.to as Position).y += e.movementY / zoom.value; - const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.dragFrom!, fakeEdge.value.to!.x, fakeEdge.value.to!.y); + const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.id, fakeEdge.value.to!.x, fakeEdge.value.to!.y); fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined; fakeEdge.value.path = bezier((fakeEdge.value.from as Position), fakeEdge.value.fromSide!, result ?? (fakeEdge.value.to as Position), result?.direction ?? fakeEdge.value.toSide!); @@ -376,8 +376,8 @@ function dragEndEdgeTo(e: MouseEvent): void if(fakeEdge.value.snapped) { - const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.dragFrom)!; - const edge: CanvasEdge = { fromNode: fakeEdge.value.dragFrom!, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(16), color: node.color }; + const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.drag!.id)!; + const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(16), color: node.color }; canvas.value.edges?.push(edge); addAction('create', [{ from: undefined, to: edge, element: { id: edge.id, type: 'edge' } }]); @@ -393,12 +393,84 @@ function dragStartEdgeTo(id: string, e: MouseEvent, direction: Direction): void window.addEventListener('mousemove', dragEdgeTo, { passive: true }); window.addEventListener('mouseup', dragEndEdgeTo, { passive: true }); } +function dragEdgeSide(e: MouseEvent): void +{ + if(fakeEdge.value.drag?.type === 'node') + return; + + const destination = fakeEdge.value.drag!.destination; + const pos = fakeEdge.value[destination]!; + + pos.x += e.movementX / zoom.value; + pos.y += e.movementY / zoom.value; + + const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.node, pos.x, pos.y); + + fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined; + fakeEdge.value.path = bezier(destination === 'from' ? (result ?? pos) : fakeEdge.value.from!, destination === 'from' ? result?.direction ?? fakeEdge.value.fromSide! : fakeEdge.value.fromSide!, destination === 'to' ? (result ?? pos) : fakeEdge.value.to!, destination === 'to' ? result?.direction ?? fakeEdge.value.toSide! : fakeEdge.value.toSide!); +} +function dragEndEdgeSide(e: MouseEvent): void +{ + if(fakeEdge.value.drag?.type === 'node') + return; + + window.removeEventListener('mousemove', dragEdgeSide); + window.removeEventListener('mouseup', dragEndEdgeSide); + + if(fakeEdge.value.snapped) + { + const edge = canvas.value.edges!.find(e => e.id === fakeEdge.value.drag?.id)! + const old = { ... edge }; + + const destination = fakeEdge.value.drag!.destination; + + edge.fromNode = destination === 'to' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node; + edge.fromSide = destination === 'to' ? fakeEdge.value.fromSide! : fakeEdge.value.snapped.side; + + edge.toNode = destination === 'from' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node; + edge.toSide = destination === 'from' ? fakeEdge.value.toSide! : fakeEdge.value.snapped.side; + + addAction('property', [{ from: old, to: edge, element: { id: edge.id, type: 'edge' } }]); + } + + fakeEdge.value = {}; +} +function dragStartEdgeSide(id: string, e: MouseEvent, direction: 'from' | 'to'): void +{ + const edge = canvas.value.edges!.find(e => e.id === id)!; + fakeEdgeFromEdge(edge, direction); + + window.addEventListener('mousemove', dragEdgeSide, { passive: true }); + window.addEventListener('mouseup', dragEndEdgeSide, { passive: true }); +} +function fakeEdgeFromEdge(edge: CanvasEdge, direction: 'from' | 'to'): void +{ + fakeEdge.value.drag = { type: 'edge', id: edge.id, destination: direction, node: direction === 'to' ? edge.fromNode : edge.toNode }; + + const destinationNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.fromNode)! : canvas.value.nodes!.find(e => e.id === edge.toNode)!; + const otherNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.toNode)! : canvas.value.nodes!.find(e => e.id === edge.fromNode)!; + const destinationPos = posFromDir(getBbox(destinationNode), direction === 'from' ? edge.fromSide : edge.toSide); + const otherPos = posFromDir(getBbox(otherNode), direction === 'from' ? edge.toSide : edge.fromSide); + + fakeEdge.value.from = direction === 'from' ? destinationPos : otherPos; + fakeEdge.value.fromSide = edge.fromSide; + + fakeEdge.value.to = direction === 'to' ? destinationPos : otherPos; + fakeEdge.value.toSide = edge.toSide; + + fakeEdge.value.path = bezier(destinationPos, edge.fromSide, otherPos, edge.toSide); + fakeEdge.value.hex = edge.color?.hex; + + fakeEdge.value.style = edge?.color ? edge.color?.class ? + { fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}` } : + { fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } : + { stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` }; +} function fakeEdgeFromNode(node: CanvasNode, direction: Direction): void { const pos = posFromDir(getBbox(node), direction); - fakeEdge.value.original = undefined; - fakeEdge.value.dragFrom = node.id; + fakeEdge.value.drag = { type: 'node', id: node.id }; fakeEdge.value.from = { ... pos }; fakeEdge.value.fromSide = direction; @@ -678,7 +750,7 @@ useShortcuts({ 'transform-origin': 'center center', }" class="h-full">
-
+
@@ -34,7 +36,7 @@ const { edge, nodes } = defineProps<{ const emit = defineEmits<{ (e: 'select', id: Element): void, (e: 'edit', id: Element): void, - (e: 'move', id: string, from: string, to: string): void, + (e: 'drag', id: string, _e: MouseEvent, origin: 'from' | 'to'): void, (e: 'input', id: string, text?: string): void, }>(); @@ -64,6 +66,11 @@ function edit(e: Event) { e.stopImmediatePropagation(); emit('edit', { type: 'edge', id: edge.id }); } +function dragEdge(e: MouseEvent, origin: 'from' | 'to') { + e.stopImmediatePropagation(); + + emit('drag', edge.id, e, origin); +} function unselect() { if(editing.value) { diff --git a/components/canvas/CanvasNodeEditor.vue b/components/canvas/CanvasNodeEditor.vue index b703474..340a793 100644 --- a/components/canvas/CanvasNodeEditor.vue +++ b/components/canvas/CanvasNodeEditor.vue @@ -29,7 +29,7 @@
{{ node.label }}
- +
@@ -119,7 +119,7 @@ function resizeNode(e: MouseEvent, x: number, y: number, w: number, h: number) { window.addEventListener('mousemove', resizemove); window.addEventListener('mouseup', resizeend); } -function dragEdge(e: Event, direction: Direction) { +function dragEdge(e: MouseEvent, direction: Direction) { e.stopImmediatePropagation(); emit('edge', node.id, e, direction) diff --git a/db.sqlite-shm b/db.sqlite-shm index f279b12..eb86b06 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index 3b9a8fd..8d1584f 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/plugins/autofocus.ts b/plugins/autofocus.ts new file mode 100644 index 0000000..dd72a91 --- /dev/null +++ b/plugins/autofocus.ts @@ -0,0 +1,7 @@ +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.directive('autofocus', { + mounted(el, binding) { + el.focus(); + } + }) +}) \ No newline at end of file diff --git a/shared/physics.util.ts b/shared/physics.util.ts index e49da6f..3a313e8 100644 --- a/shared/physics.util.ts +++ b/shared/physics.util.ts @@ -125,10 +125,10 @@ class SpatialGrid { const endX = Math.ceil((node.x + node.width) / this.cellSize); const endY = Math.ceil((node.y + node.height) / this.cellSize); - const minX = viewport ? Math.max(this.minx, Math.floor(viewport.x / this.cellSize)) : this.minx; - const minY = viewport ? Math.max(this.miny, Math.floor(viewport.y / this.cellSize)) : this.miny; - const maxX = viewport ? Math.min(this.maxx, Math.ceil((viewport.x + viewport.w) / this.cellSize)) : this.maxx; - const maxY = viewport ? Math.min(this.maxy, Math.ceil((viewport.y + viewport.h) / this.cellSize)) : this.maxy; + const minX = Math.max(viewport ? Math.max(this.minx, Math.floor(viewport.x / this.cellSize)) : this.minx, startX - 8); + const minY = Math.max(viewport ? Math.max(this.miny, Math.floor(viewport.y / this.cellSize)) : this.miny, startY - 8); + const maxX = Math.min(viewport ? Math.min(this.maxx, Math.ceil((viewport.x + viewport.w) / this.cellSize)) : this.maxx, endX + 8); + const maxY = Math.min(viewport ? Math.min(this.maxy, Math.ceil((viewport.y + viewport.h) / this.cellSize)) : this.maxy, endY + 8); for (let dx = minX; dx <= maxX; dx++) { const gridX = this.cells.get(dx);