diff --git a/package.json b/package.json index e45232c..914c19c 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,12 @@ "@fontsource/material-icons": "^4.5.4", "@fontsource/roboto-mono": "^4.5.8", "@pixi/app": "~6.3.2", + "@pixi/constants": "~6.3.2", "@pixi/core": "~6.3.2", - "@pixi/particle-emitter": "^5.0.4", + "@pixi/display": "~6.3.2", + "@pixi/math": "~6.3.2", + "@pixi/particle-emitter": "^5.0.7", + "@pixi/sprite": "~6.3.2", "@pixi/ticker": "~6.3.2", "@vitejs/plugin-vue": "^2.3.3", "@vitejs/plugin-vue-jsx": "^1.3.10", @@ -31,7 +35,7 @@ "vite-tsconfig-paths": "^3.5.0", "vue": "^3.2.26", "vue-next-select": "^2.10.2", - "vue-panzoom": "^1.1.6", + "vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git", "vue-textarea-autosize": "^1.1.1", "vue-toastification": "^2.0.0-rc.1", "vue-transition-expand": "^0.1.0", diff --git a/src/features/boards/Board.vue b/src/features/boards/Board.vue index 90f4e79..c7e7d9d 100644 --- a/src/features/boards/Board.vue +++ b/src/features/boards/Board.vue @@ -61,17 +61,15 @@ import type { import { getNodeProperty } from "features/boards/board"; import type { StyleValue } from "features/feature"; import { Visibility } from "features/feature"; -import { PersistentState } from "game/persistence"; import type { ProcessedComputable } from "util/computed"; import { computed, ref, Ref, toRefs, unref } from "vue"; -import panZoom from "vue-panzoom"; import BoardLinkVue from "./BoardLink.vue"; import BoardNodeVue from "./BoardNode.vue"; const _props = defineProps<{ nodes: Ref; types: Record; - [PersistentState]: Ref; + state: Ref; visibility: ProcessedComputable; width?: ProcessedComputable; height?: ProcessedComputable; @@ -80,6 +78,7 @@ const _props = defineProps<{ links: Ref; selectedAction: Ref; selectedNode: Ref; + mousePosition: Ref<{ x: number; y: number } | null>; }>(); const props = toRefs(_props); @@ -170,13 +169,13 @@ function mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, dra } } if (nodeID != null) { - props[PersistentState].value.selectedNode = null; - props[PersistentState].value.selectedAction = null; + props.state.value.selectedNode = null; + props.state.value.selectedAction = null; } } function drag(e: MouseEvent | TouchEvent) { - const zoom = stage.value.$panZoomInstance.getTransform().scale; + const { x, y, scale } = stage.value.panZoomInstance.getTransform(); let clientX, clientY; if ("touches" in e) { @@ -185,6 +184,7 @@ function drag(e: MouseEvent | TouchEvent) { clientY = e.touches[0].clientY; } else { endDragging(dragging.value); + props.mousePosition.value = null; return; } } else { @@ -192,9 +192,14 @@ function drag(e: MouseEvent | TouchEvent) { clientY = e.clientY; } + props.mousePosition.value = { + x: (clientX - x) / scale, + y: (clientY - y) / scale + }; + dragged.value = { - x: dragged.value.x + (clientX - lastMousePosition.value.x) / zoom, - y: dragged.value.y + (clientY - lastMousePosition.value.y) / zoom + x: dragged.value.x + (clientX - lastMousePosition.value.x) / scale, + y: dragged.value.y + (clientY - lastMousePosition.value.y) / scale }; lastMousePosition.value = { x: clientX, @@ -229,8 +234,8 @@ function endDragging(nodeID: number | null) { dragging.value = null; } else if (!hasDragged.value) { - props[PersistentState].value.selectedNode = null; - props[PersistentState].value.selectedAction = null; + props.state.value.selectedNode = null; + props.state.value.selectedAction = null; } } @@ -239,7 +244,11 @@ function endDragging(nodeID: number | null) { .vue-pan-zoom-scene { width: 100%; height: 100%; - cursor: move; + cursor: grab; +} + +.vue-pan-zoom-scene:active { + cursor: grabbing; } .g1 { diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts index 4c2cef5..322037b 100644 --- a/src/features/boards/board.ts +++ b/src/features/boards/board.ts @@ -21,9 +21,12 @@ import type { } from "util/computed"; import { processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; -import { computed, Ref, unref } from "vue"; +import { computed, ref, Ref, unref } from "vue"; +import panZoom from "vue-panzoom"; import type { Link } from "../links/links"; +globalBus.on("setupVue", app => panZoom.install(app)); + export const BoardType = Symbol("Board"); export type NodeComputable = Computable | ((node: BoardNode) => T); @@ -166,12 +169,14 @@ export interface BoardOptions { types: Record; } -export interface BaseBoard extends Persistent { +export interface BaseBoard { id: string; + state: Persistent; links: Ref; nodes: Ref; selectedNode: Ref; selectedAction: Ref; + mousePosition: Ref<{ x: number; y: number } | null>; type: typeof BoardType; [Component]: typeof BoardComponent; [GatherProps]: () => Record; @@ -199,140 +204,138 @@ export type GenericBoard = Replace< export function createBoard( optionsFunc: OptionsFunc ): Board { - return createLazyProxy( - persistent => { - const board = Object.assign(persistent, optionsFunc()); - board.id = getUniqueID("board-"); - board.type = BoardType; - board[Component] = BoardComponent; + return createLazyProxy(() => { + const board = optionsFunc(); + board.id = getUniqueID("board-"); + board.type = BoardType; + board[Component] = BoardComponent; - board.nodes = computed(() => processedBoard[PersistentState].value.nodes); - board.selectedNode = computed( - () => - processedBoard.nodes.value.find( - node => node.id === board[PersistentState].value.selectedNode - ) || null - ); - board.selectedAction = computed(() => { - const selectedNode = processedBoard.selectedNode.value; - if (selectedNode == null) { - return null; - } - const type = processedBoard.types[selectedNode.type]; - if (type.actions == null) { - return null; - } - return ( - type.actions.find( - action => action.id === processedBoard[PersistentState].value.selectedAction - ) || null - ); - }); - board.links = computed(() => { - if (processedBoard.selectedAction.value == null) { - return null; - } - if ( - processedBoard.selectedAction.value.links && - processedBoard.selectedNode.value - ) { - return getNodeProperty( - processedBoard.selectedAction.value.links, - processedBoard.selectedNode.value - ); - } - return null; - }); - processComputable(board as T, "visibility"); - setDefault(board, "visibility", Visibility.Visible); - processComputable(board as T, "width"); - setDefault(board, "width", "100%"); - processComputable(board as T, "height"); - setDefault(board, "height", "400px"); - processComputable(board as T, "classes"); - processComputable(board as T, "style"); - - for (const type in board.types) { - const nodeType: NodeTypeOptions & Partial = board.types[type]; - - processComputable(nodeType as NodeTypeOptions, "title"); - processComputable(nodeType as NodeTypeOptions, "label"); - processComputable(nodeType as NodeTypeOptions, "size"); - setDefault(nodeType, "size", 50); - processComputable(nodeType as NodeTypeOptions, "draggable"); - setDefault(nodeType, "draggable", false); - processComputable(nodeType as NodeTypeOptions, "shape"); - setDefault(nodeType, "shape", Shape.Circle); - processComputable(nodeType as NodeTypeOptions, "canAccept"); - setDefault(nodeType, "canAccept", false); - processComputable(nodeType as NodeTypeOptions, "progress"); - processComputable(nodeType as NodeTypeOptions, "progressDisplay"); - setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill); - processComputable(nodeType as NodeTypeOptions, "progressColor"); - setDefault(nodeType, "progressColor", "none"); - processComputable(nodeType as NodeTypeOptions, "fillColor"); - processComputable(nodeType as NodeTypeOptions, "outlineColor"); - processComputable(nodeType as NodeTypeOptions, "titleColor"); - processComputable(nodeType as NodeTypeOptions, "actionDistance"); - setDefault(nodeType, "actionDistance", Math.PI / 6); - nodeType.nodes = computed(() => - board[PersistentState].value.nodes.filter(node => node.type === type) - ); - setDefault(nodeType, "onClick", function (node: BoardNode) { - board[PersistentState].value.selectedNode = node.id; - }); - - if (nodeType.actions) { - for (const action of nodeType.actions) { - processComputable(action, "visibility"); - setDefault(action, "visibility", Visibility.Visible); - processComputable(action, "icon"); - processComputable(action, "fillColor"); - processComputable(action, "tooltip"); - processComputable(action, "links"); - } - } - } - - board[GatherProps] = function (this: GenericBoard) { - const { - nodes, - types, - [PersistentState]: state, - visibility, - width, - height, - style, - classes, - links, - selectedAction, - selectedNode - } = this; - return { - nodes, - types, - [PersistentState]: state, - visibility, - width, - height, - style: unref(style), - classes, - links, - selectedAction, - selectedNode - }; - }; - - // This is necessary because board.types is different from T and Board - const processedBoard = board as unknown as Board; - return processedBoard; - }, - persistent({ + board.state = persistent({ nodes: [], selectedNode: null, selectedAction: null - }) - ); + }); + board.nodes = computed(() => processedBoard.state.value.nodes); + board.selectedNode = computed( + () => + processedBoard.nodes.value.find( + node => node.id === processedBoard.state.value.selectedNode + ) || null + ); + board.selectedAction = computed(() => { + const selectedNode = processedBoard.selectedNode.value; + if (selectedNode == null) { + return null; + } + const type = processedBoard.types[selectedNode.type]; + if (type.actions == null) { + return null; + } + return ( + type.actions.find( + action => action.id === processedBoard.state.value.selectedAction + ) || null + ); + }); + board.mousePosition = ref(null); + board.links = computed(() => { + if (processedBoard.selectedAction.value == null) { + return null; + } + if (processedBoard.selectedAction.value.links && processedBoard.selectedNode.value) { + return getNodeProperty( + processedBoard.selectedAction.value.links, + processedBoard.selectedNode.value + ); + } + return null; + }); + processComputable(board as T, "visibility"); + setDefault(board, "visibility", Visibility.Visible); + processComputable(board as T, "width"); + setDefault(board, "width", "100%"); + processComputable(board as T, "height"); + setDefault(board, "height", "400px"); + processComputable(board as T, "classes"); + processComputable(board as T, "style"); + + for (const type in board.types) { + const nodeType: NodeTypeOptions & Partial = board.types[type]; + + processComputable(nodeType as NodeTypeOptions, "title"); + processComputable(nodeType as NodeTypeOptions, "label"); + processComputable(nodeType as NodeTypeOptions, "size"); + setDefault(nodeType, "size", 50); + processComputable(nodeType as NodeTypeOptions, "draggable"); + setDefault(nodeType, "draggable", false); + processComputable(nodeType as NodeTypeOptions, "shape"); + setDefault(nodeType, "shape", Shape.Circle); + processComputable(nodeType as NodeTypeOptions, "canAccept"); + setDefault(nodeType, "canAccept", false); + processComputable(nodeType as NodeTypeOptions, "progress"); + processComputable(nodeType as NodeTypeOptions, "progressDisplay"); + setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill); + processComputable(nodeType as NodeTypeOptions, "progressColor"); + setDefault(nodeType, "progressColor", "none"); + processComputable(nodeType as NodeTypeOptions, "fillColor"); + processComputable(nodeType as NodeTypeOptions, "outlineColor"); + processComputable(nodeType as NodeTypeOptions, "titleColor"); + processComputable(nodeType as NodeTypeOptions, "actionDistance"); + setDefault(nodeType, "actionDistance", Math.PI / 6); + nodeType.nodes = computed(() => + processedBoard.state.value.nodes.filter(node => node.type === type) + ); + setDefault(nodeType, "onClick", function (node: BoardNode) { + processedBoard.state.value.selectedNode = node.id; + }); + + if (nodeType.actions) { + for (const action of nodeType.actions) { + processComputable(action, "visibility"); + setDefault(action, "visibility", Visibility.Visible); + processComputable(action, "icon"); + processComputable(action, "fillColor"); + processComputable(action, "tooltip"); + processComputable(action, "links"); + } + } + } + + board[GatherProps] = function (this: GenericBoard) { + const { + nodes, + types, + state, + visibility, + width, + height, + style, + classes, + links, + selectedAction, + selectedNode, + mousePosition + } = this; + return { + nodes, + types, + state, + visibility, + width, + height, + style: unref(style), + classes, + links, + selectedAction, + selectedNode, + mousePosition + }; + }; + + // This is necessary because board.types is different from T and Board + const processedBoard = board as unknown as Board; + return processedBoard; + }); } export function getNodeProperty(property: NodeComputable, node: BoardNode): T {