Fixed board feature

This commit is contained in:
thepaperpilot 2022-09-07 20:01:05 -05:00
parent 066afc5dfe
commit 6edf16515b
3 changed files with 161 additions and 145 deletions

View file

@ -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",

View file

@ -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 {

View file

@ -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 {