Fixed board feature
This commit is contained in:
parent
066afc5dfe
commit
6edf16515b
3 changed files with 161 additions and 145 deletions
|
@ -15,8 +15,12 @@
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "^4.5.4",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
"@pixi/app": "~6.3.2",
|
"@pixi/app": "~6.3.2",
|
||||||
|
"@pixi/constants": "~6.3.2",
|
||||||
"@pixi/core": "~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",
|
"@pixi/ticker": "~6.3.2",
|
||||||
"@vitejs/plugin-vue": "^2.3.3",
|
"@vitejs/plugin-vue": "^2.3.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||||
|
@ -31,7 +35,7 @@
|
||||||
"vite-tsconfig-paths": "^3.5.0",
|
"vite-tsconfig-paths": "^3.5.0",
|
||||||
"vue": "^3.2.26",
|
"vue": "^3.2.26",
|
||||||
"vue-next-select": "^2.10.2",
|
"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-textarea-autosize": "^1.1.1",
|
||||||
"vue-toastification": "^2.0.0-rc.1",
|
"vue-toastification": "^2.0.0-rc.1",
|
||||||
"vue-transition-expand": "^0.1.0",
|
"vue-transition-expand": "^0.1.0",
|
||||||
|
|
|
@ -61,17 +61,15 @@ import type {
|
||||||
import { getNodeProperty } from "features/boards/board";
|
import { getNodeProperty } from "features/boards/board";
|
||||||
import type { StyleValue } from "features/feature";
|
import type { StyleValue } from "features/feature";
|
||||||
import { Visibility } from "features/feature";
|
import { Visibility } from "features/feature";
|
||||||
import { PersistentState } from "game/persistence";
|
|
||||||
import type { ProcessedComputable } from "util/computed";
|
import type { ProcessedComputable } from "util/computed";
|
||||||
import { computed, ref, Ref, toRefs, unref } from "vue";
|
import { computed, ref, Ref, toRefs, unref } from "vue";
|
||||||
import panZoom from "vue-panzoom";
|
|
||||||
import BoardLinkVue from "./BoardLink.vue";
|
import BoardLinkVue from "./BoardLink.vue";
|
||||||
import BoardNodeVue from "./BoardNode.vue";
|
import BoardNodeVue from "./BoardNode.vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
nodes: Ref<BoardNode[]>;
|
nodes: Ref<BoardNode[]>;
|
||||||
types: Record<string, GenericNodeType>;
|
types: Record<string, GenericNodeType>;
|
||||||
[PersistentState]: Ref<BoardData>;
|
state: Ref<BoardData>;
|
||||||
visibility: ProcessedComputable<Visibility>;
|
visibility: ProcessedComputable<Visibility>;
|
||||||
width?: ProcessedComputable<string>;
|
width?: ProcessedComputable<string>;
|
||||||
height?: ProcessedComputable<string>;
|
height?: ProcessedComputable<string>;
|
||||||
|
@ -80,6 +78,7 @@ const _props = defineProps<{
|
||||||
links: Ref<BoardNodeLink[] | null>;
|
links: Ref<BoardNodeLink[] | null>;
|
||||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||||
selectedNode: Ref<BoardNode | null>;
|
selectedNode: Ref<BoardNode | null>;
|
||||||
|
mousePosition: Ref<{ x: number; y: number } | null>;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
const props = toRefs(_props);
|
||||||
|
|
||||||
|
@ -170,13 +169,13 @@ function mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, dra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeID != null) {
|
if (nodeID != null) {
|
||||||
props[PersistentState].value.selectedNode = null;
|
props.state.value.selectedNode = null;
|
||||||
props[PersistentState].value.selectedAction = null;
|
props.state.value.selectedAction = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drag(e: MouseEvent | TouchEvent) {
|
function drag(e: MouseEvent | TouchEvent) {
|
||||||
const zoom = stage.value.$panZoomInstance.getTransform().scale;
|
const { x, y, scale } = stage.value.panZoomInstance.getTransform();
|
||||||
|
|
||||||
let clientX, clientY;
|
let clientX, clientY;
|
||||||
if ("touches" in e) {
|
if ("touches" in e) {
|
||||||
|
@ -185,6 +184,7 @@ function drag(e: MouseEvent | TouchEvent) {
|
||||||
clientY = e.touches[0].clientY;
|
clientY = e.touches[0].clientY;
|
||||||
} else {
|
} else {
|
||||||
endDragging(dragging.value);
|
endDragging(dragging.value);
|
||||||
|
props.mousePosition.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -192,9 +192,14 @@ function drag(e: MouseEvent | TouchEvent) {
|
||||||
clientY = e.clientY;
|
clientY = e.clientY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
props.mousePosition.value = {
|
||||||
|
x: (clientX - x) / scale,
|
||||||
|
y: (clientY - y) / scale
|
||||||
|
};
|
||||||
|
|
||||||
dragged.value = {
|
dragged.value = {
|
||||||
x: dragged.value.x + (clientX - lastMousePosition.value.x) / zoom,
|
x: dragged.value.x + (clientX - lastMousePosition.value.x) / scale,
|
||||||
y: dragged.value.y + (clientY - lastMousePosition.value.y) / zoom
|
y: dragged.value.y + (clientY - lastMousePosition.value.y) / scale
|
||||||
};
|
};
|
||||||
lastMousePosition.value = {
|
lastMousePosition.value = {
|
||||||
x: clientX,
|
x: clientX,
|
||||||
|
@ -229,8 +234,8 @@ function endDragging(nodeID: number | null) {
|
||||||
|
|
||||||
dragging.value = null;
|
dragging.value = null;
|
||||||
} else if (!hasDragged.value) {
|
} else if (!hasDragged.value) {
|
||||||
props[PersistentState].value.selectedNode = null;
|
props.state.value.selectedNode = null;
|
||||||
props[PersistentState].value.selectedAction = null;
|
props.state.value.selectedAction = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -239,7 +244,11 @@ function endDragging(nodeID: number | null) {
|
||||||
.vue-pan-zoom-scene {
|
.vue-pan-zoom-scene {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: move;
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-pan-zoom-scene:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.g1 {
|
.g1 {
|
||||||
|
|
|
@ -21,9 +21,12 @@ import type {
|
||||||
} from "util/computed";
|
} from "util/computed";
|
||||||
import { processComputable } from "util/computed";
|
import { processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
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";
|
import type { Link } from "../links/links";
|
||||||
|
|
||||||
|
globalBus.on("setupVue", app => panZoom.install(app));
|
||||||
|
|
||||||
export const BoardType = Symbol("Board");
|
export const BoardType = Symbol("Board");
|
||||||
|
|
||||||
export type NodeComputable<T> = Computable<T> | ((node: BoardNode) => T);
|
export type NodeComputable<T> = Computable<T> | ((node: BoardNode) => T);
|
||||||
|
@ -166,12 +169,14 @@ export interface BoardOptions {
|
||||||
types: Record<string, NodeTypeOptions>;
|
types: Record<string, NodeTypeOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseBoard extends Persistent<BoardData> {
|
export interface BaseBoard {
|
||||||
id: string;
|
id: string;
|
||||||
|
state: Persistent<BoardData>;
|
||||||
links: Ref<BoardNodeLink[] | null>;
|
links: Ref<BoardNodeLink[] | null>;
|
||||||
nodes: Ref<BoardNode[]>;
|
nodes: Ref<BoardNode[]>;
|
||||||
selectedNode: Ref<BoardNode | null>;
|
selectedNode: Ref<BoardNode | null>;
|
||||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||||
|
mousePosition: Ref<{ x: number; y: number } | null>;
|
||||||
type: typeof BoardType;
|
type: typeof BoardType;
|
||||||
[Component]: typeof BoardComponent;
|
[Component]: typeof BoardComponent;
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
|
@ -199,140 +204,138 @@ export type GenericBoard = Replace<
|
||||||
export function createBoard<T extends BoardOptions>(
|
export function createBoard<T extends BoardOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseBoard, GenericBoard>
|
optionsFunc: OptionsFunc<T, BaseBoard, GenericBoard>
|
||||||
): Board<T> {
|
): Board<T> {
|
||||||
return createLazyProxy(
|
return createLazyProxy(() => {
|
||||||
persistent => {
|
const board = optionsFunc();
|
||||||
const board = Object.assign(persistent, optionsFunc());
|
board.id = getUniqueID("board-");
|
||||||
board.id = getUniqueID("board-");
|
board.type = BoardType;
|
||||||
board.type = BoardType;
|
board[Component] = BoardComponent;
|
||||||
board[Component] = BoardComponent;
|
|
||||||
|
|
||||||
board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
|
board.state = persistent<BoardData>({
|
||||||
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<BaseNodeType> = 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<T>;
|
|
||||||
return processedBoard;
|
|
||||||
},
|
|
||||||
persistent<BoardData>({
|
|
||||||
nodes: [],
|
nodes: [],
|
||||||
selectedNode: null,
|
selectedNode: null,
|
||||||
selectedAction: 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<BaseNodeType> = 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<T>;
|
||||||
|
return processedBoard;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {
|
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {
|
||||||
|
|
Loading…
Reference in a new issue