Add support for rendering VueFeatures in boards

This commit is contained in:
thepaperpilot 2024-03-03 19:59:26 -06:00
parent 1cbe97251c
commit c64ac82a25
4 changed files with 225 additions and 167 deletions

View file

@ -1,73 +0,0 @@
/**
* @module
* @hidden
*/
import { main } from "data/projEntry";
import { createCumulativeConversion } from "features/conversion";
import { jsx } from "features/feature";
import { createHotkey } from "features/hotkey";
import { createReset } from "features/reset";
import MainDisplay from "features/resources/MainDisplay.vue";
import { createResource } from "features/resources/resource";
import { addTooltip } from "features/tooltips/tooltip";
import { createResourceTooltip } from "features/trees/tree";
import { BaseLayer, createLayer } from "game/layers";
import type { DecimalSource } from "util/bignum";
import { render } from "util/vue";
import { createLayerTreeNode, createResetButton } from "../common";
const id = "p";
const layer = createLayer(id, function (this: BaseLayer) {
const name = "Prestige";
const color = "#4BDC13";
const points = createResource<DecimalSource>(0, "prestige points");
const conversion = createCumulativeConversion(() => ({
formula: x => x.div(10).sqrt(),
baseResource: main.points,
gainResource: points
}));
const reset = createReset(() => ({
thingsToReset: (): Record<string, unknown>[] => [layer]
}));
const treeNode = createLayerTreeNode(() => ({
layerID: id,
color,
reset
}));
const tooltip = addTooltip(treeNode, {
display: createResourceTooltip(points),
pinnable: true
});
const resetButton = createResetButton(() => ({
conversion,
tree: main.tree,
treeNode
}));
const hotkey = createHotkey(() => ({
description: "Reset for prestige points",
key: "p",
onPress: resetButton.onClick
}));
return {
name,
color,
points,
tooltip,
display: jsx(() => (
<>
<MainDisplay resource={points} color={color} />
{render(resetButton)}
</>
)),
treeNode,
hotkey
};
});
export default layer;

View file

@ -4,6 +4,7 @@ import SVGNode from "features/boards/SVGNode.vue";
import SquareProgress from "features/boards/SquareProgress.vue"; import SquareProgress from "features/boards/SquareProgress.vue";
import { import {
NodePosition, NodePosition,
makeDraggable,
placeInAvailableSpace, placeInAvailableSpace,
setupActions, setupActions,
setupDraggableNode, setupDraggableNode,
@ -11,24 +12,29 @@ import {
setupUniqueIds setupUniqueIds
} from "features/boards/board"; } from "features/boards/board";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
import { createResource } from "features/resources/resource";
import { createUpgrade } from "features/upgrades/upgrade";
import type { BaseLayer, GenericLayer } from "game/layers"; import type { BaseLayer, GenericLayer } from "game/layers";
import { createLayer } from "game/layers"; import { createLayer } from "game/layers";
import { persistent } from "game/persistence"; import { Persistent, persistent } from "game/persistence";
import type { Player } from "game/player"; import type { Player } from "game/player";
import { createCostRequirement } from "game/requirements";
import { render } from "util/vue";
import { ComponentPublicInstance, computed, ref, watch } from "vue"; import { ComponentPublicInstance, computed, ref, watch } from "vue";
import prestige from "./layers/prestige"; import "./common.css";
type ANode = NodePosition & { id: number; links: number[]; type: "anode" };
type BNode = NodePosition & { id: number; links: number[]; type: "bnode" };
type NodeTypes = ANode | BNode;
/** /**
* @hidden * @hidden
*/ */
export const main = createLayer("main", function (this: BaseLayer) { export const main = createLayer("main", function (this: BaseLayer) {
type ANode = NodePosition & { id: number; links: number[]; type: "anode" };
type BNode = NodePosition & { id: number; links: number[]; type: "bnode" };
type CNode = typeof cNode & { position: Persistent<NodePosition> };
type NodeTypes = ANode | BNode;
const board = ref<ComponentPublicInstance<typeof Board>>(); const board = ref<ComponentPublicInstance<typeof Board>>();
const { select, deselect, selected } = setupSelectable<NodeTypes>(); const { select, deselect, selected } = setupSelectable<number>();
const { const {
select: selectAction, select: selectAction,
deselect: deselectAction, deselect: deselectAction,
@ -50,10 +56,15 @@ export const main = createLayer("main", function (this: BaseLayer) {
receivingNodes, receivingNodes,
receivingNode, receivingNode,
dragDelta dragDelta
} = setupDraggableNode<NodeTypes /* | typeof cNode*/>({ } = setupDraggableNode<number | "cnode">({
board, board,
isDraggable: function (node) { getPosition(id) {
return nodes.value.includes(node); return nodesById.value[id] ?? (cNode as CNode).position.value;
},
setPosition(id, position) {
const node = nodesById.value[id] ?? (cNode as CNode).position.value;
node.x = position.x;
node.y = position.y;
} }
}); });
@ -64,26 +75,26 @@ export const main = createLayer("main", function (this: BaseLayer) {
// c node also exists but is a single Upgrade element that cannot be selected, but can be dragged // c node also exists but is a single Upgrade element that cannot be selected, but can be dragged
// d nodes are a performance test - 1000 simple nodes that have no interactions // d nodes are a performance test - 1000 simple nodes that have no interactions
// Make all nodes animate in (decorator? `fadeIn(feature)?) // Make all nodes animate in (decorator? `fadeIn(feature)?)
const nodes = persistent<NodeTypes[]>([{ id: 0, x: 0, y: 0, links: [], type: "anode" }]); const nodes = persistent<(ANode | BNode)[]>([{ id: 0, x: 0, y: 0, links: [], type: "anode" }]);
const nodesById = computed<Record<string, NodeTypes>>(() => const nodesById = computed<Record<string, NodeTypes>>(() =>
nodes.value.reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {}) nodes.value.reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {})
); );
function mouseDownNode(e: MouseEvent | TouchEvent, node: NodeTypes) { function mouseDownNode(e: MouseEvent | TouchEvent, node: NodeTypes) {
if (nodeBeingDragged.value == null) { if (nodeBeingDragged.value == null) {
startDrag(e, node); startDrag(e, node.id);
} }
deselect(); deselect();
} }
function mouseUpNode(e: MouseEvent | TouchEvent, node: NodeTypes) { function mouseUpNode(e: MouseEvent | TouchEvent, node: NodeTypes) {
if (!hasDragged.value) { if (!hasDragged.value) {
endDrag(); endDrag();
select(node); if (typeof node.id === "number") {
select(node.id);
}
e.stopPropagation(); e.stopPropagation();
} }
} }
function getTranslateString(node: NodePosition, overrideSelected?: boolean) { function getTranslateString(node: NodePosition, isDragging: boolean) {
const isSelected = overrideSelected == null ? selected.value === node : overrideSelected;
const isDragging = !isSelected && nodeBeingDragged.value === node;
let x = node.x; let x = node.x;
let y = node.y; let y = node.y;
if (isDragging) { if (isDragging) {
@ -95,13 +106,13 @@ export const main = createLayer("main", function (this: BaseLayer) {
function getRotateString(rotation: number) { function getRotateString(rotation: number) {
return ` rotate(${rotation}deg) `; return ` rotate(${rotation}deg) `;
} }
function getScaleString(node: NodePosition, overrideSelected?: boolean) { function getScaleString(nodeOrBool: NodeTypes | boolean) {
const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; const isSelected =
typeof nodeOrBool === "boolean" ? nodeOrBool : selected.value === nodeOrBool.id;
return isSelected ? " scale(1.2)" : ""; return isSelected ? " scale(1.2)" : "";
} }
function getOpacityString(node: NodePosition, overrideSelected?: boolean) { function getOpacityString(node: NodeTypes) {
const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; const isDragging = selected.value !== node.id && nodeBeingDragged.value === node.id;
const isDragging = !isSelected && nodeBeingDragged.value === node;
if (isDragging) { if (isDragging) {
return "; opacity: 0.5;"; return "; opacity: 0.5;";
} }
@ -111,16 +122,19 @@ export const main = createLayer("main", function (this: BaseLayer) {
const renderANode = function (node: ANode) { const renderANode = function (node: ANode) {
return ( return (
<SVGNode <SVGNode
style={`transform: ${getTranslateString(node)}${getOpacityString(node)}`} style={`transform: ${getTranslateString(
node,
selected.value === node.id && nodeBeingDragged.value === node.id
)}${getOpacityString(node)}`}
onMouseDown={e => mouseDownNode(e, node)} onMouseDown={e => mouseDownNode(e, node)}
onMouseUp={e => mouseUpNode(e, node)} onMouseUp={e => mouseUpNode(e, node)}
> >
<g style={`transform: ${getScaleString(node)}; transition-duration: 0s`}> <g style={`transform: ${getScaleString(node)}`}>
{receivingNodes.value.includes(node) && ( {receivingNodes.value.includes(node.id) && (
<circle <circle
r="58" r="58"
fill="var(--background)" fill="var(--background)"
stroke={receivingNode.value === node ? "#0F0" : "#0F03"} stroke={receivingNode.value === node.id ? "#0F0" : "#0F03"}
stroke-width="2" stroke-width="2"
/> />
)} )}
@ -132,7 +146,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
stroke-width="4" stroke-width="4"
/> />
</g> </g>
{selected.value === node && selectedAction.value === 0 && ( {selected.value === node.id && selectedAction.value === 0 && (
<text y="140" fill="var(--foreground)" class="node-text"> <text y="140" fill="var(--foreground)" class="node-text">
Spawn B Node Spawn B Node
</text> </text>
@ -144,8 +158,8 @@ export const main = createLayer("main", function (this: BaseLayer) {
); );
}; };
const aActions = setupActions({ const aActions = setupActions({
node: selected, node: () => nodesById.value[selected.value ?? ""],
shouldShowActions: () => selected.value?.type === "anode", shouldShowActions: node => node.type === "anode",
actions(node) { actions(node) {
return [ return [
p => ( p => (
@ -153,10 +167,10 @@ export const main = createLayer("main", function (this: BaseLayer) {
style={`transform: ${getTranslateString( style={`transform: ${getTranslateString(
p, p,
selectedAction.value === 0 selectedAction.value === 0
)}${getScaleString(p, selectedAction.value === 0)}`} )}${getScaleString(selectedAction.value === 0)}`}
onClick={() => { onClick={() => {
if (selectedAction.value === 0) { if (selectedAction.value === 0) {
spawnBNode(node); spawnBNode(node as ANode);
} else { } else {
selectAction(0); selectAction(0);
} }
@ -176,16 +190,15 @@ export const main = createLayer("main", function (this: BaseLayer) {
const renderBNode = function (node: BNode) { const renderBNode = function (node: BNode) {
return ( return (
<SVGNode <SVGNode
style={`transform: ${getTranslateString(node)}${getOpacityString(node)}`} style={`transform: ${getTranslateString(
node,
selected.value === node.id && nodeBeingDragged.value === node.id
)}${getOpacityString(node)}`}
onMouseDown={e => mouseDownNode(e, node)} onMouseDown={e => mouseDownNode(e, node)}
onMouseUp={e => mouseUpNode(e, node)} onMouseUp={e => mouseUpNode(e, node)}
> >
<g <g style={`transform: ${getScaleString(node)}${getRotateString(45)}`}>
style={`transform: ${getScaleString(node)}${getRotateString( {receivingNodes.value.includes(node.id) && (
45
)}; transition-duration: 0s`}
>
{receivingNodes.value.includes(node) && (
<rect <rect
width={50 * sqrtTwo + 16} width={50 * sqrtTwo + 16}
height={50 * sqrtTwo + 16} height={50 * sqrtTwo + 16}
@ -193,7 +206,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
(-50 * sqrtTwo + 16) / 2 (-50 * sqrtTwo + 16) / 2
})`} })`}
fill="var(--background)" fill="var(--background)"
stroke={receivingNode.value === node ? "#0F0" : "#0F03"} stroke={receivingNode.value === node.id ? "#0F0" : "#0F03"}
stroke-width="2" stroke-width="2"
/> />
)} )}
@ -213,7 +226,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
stroke-width="4" stroke-width="4"
/> />
</g> </g>
{selected.value === node && selectedAction.value === 0 && ( {selected.value === node.id && selectedAction.value === 0 && (
<text y="140" fill="var(--foreground)" class="node-text"> <text y="140" fill="var(--foreground)" class="node-text">
Spawn A Node Spawn A Node
</text> </text>
@ -225,8 +238,8 @@ export const main = createLayer("main", function (this: BaseLayer) {
); );
}; };
const bActions = setupActions({ const bActions = setupActions({
node: selected, node: () => nodesById.value[selected.value ?? ""],
shouldShowActions: () => selected.value?.type === "bnode", shouldShowActions: node => node.type === "bnode",
actions(node) { actions(node) {
return [ return [
p => ( p => (
@ -234,10 +247,10 @@ export const main = createLayer("main", function (this: BaseLayer) {
style={`transform: ${getTranslateString( style={`transform: ${getTranslateString(
p, p,
selectedAction.value === 0 selectedAction.value === 0
)}${getScaleString(p, selectedAction.value === 0)}`} )}${getScaleString(selectedAction.value === 0)}`}
onClick={() => { onClick={() => {
if (selectedAction.value === 0) { if (selectedAction.value === 0) {
spawnANode(node); spawnANode(node as BNode);
} else { } else {
selectAction(0); selectAction(0);
} }
@ -253,7 +266,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
}, },
distance: 100 distance: 100
}); });
function spawnANode(parent: NodeTypes) { function spawnANode(parent: ANode | BNode) {
const node: ANode = { const node: ANode = {
x: parent.x, x: parent.x,
y: parent.y, y: parent.y,
@ -264,7 +277,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
placeInAvailableSpace(node, nodes.value); placeInAvailableSpace(node, nodes.value);
nodes.value.push(node); nodes.value.push(node);
} }
function spawnBNode(parent: NodeTypes) { function spawnBNode(parent: ANode | BNode) {
const node: BNode = { const node: BNode = {
x: parent.x, x: parent.x,
y: parent.y, y: parent.y,
@ -276,14 +289,29 @@ export const main = createLayer("main", function (this: BaseLayer) {
nodes.value.push(node); nodes.value.push(node);
} }
// const cNode = createUpgrade(() => ({ const points = createResource(10);
// requirements: createCostRequirement(() => ({ cost: 10, resource: points })), const cNode = createUpgrade(() => ({
// style: { display: "<h1>C</h1>",
// x: "100px", // Purposefully not using noPersist
// y: "100px" requirements: createCostRequirement(() => ({ cost: 10, resource: points })),
// } style: {
// })); x: "100px",
// makeDraggable(cNode); // TODO make decorator y: "100px"
}
}));
makeDraggable(cNode, {
id: "cnode",
endDrag,
startDrag,
hasDragged,
nodeBeingDragged,
dragDelta,
onMouseUp() {
if (!hasDragged.value) {
cNode.purchase();
}
}
});
// const dNodes; // const dNodes;
@ -302,22 +330,22 @@ export const main = createLayer("main", function (this: BaseLayer) {
stroke="white" stroke="white"
stroke-width={4} stroke-width={4}
x1={ x1={
nodeBeingDragged.value === link.from nodeBeingDragged.value === link.from.id
? dragDelta.value.x + link.from.x ? dragDelta.value.x + link.from.x
: link.from.x : link.from.x
} }
y1={ y1={
nodeBeingDragged.value === link.from nodeBeingDragged.value === link.from.id
? dragDelta.value.y + link.from.y ? dragDelta.value.y + link.from.y
: link.from.y : link.from.y
} }
x2={ x2={
nodeBeingDragged.value === link.to nodeBeingDragged.value === link.to.id
? dragDelta.value.x + link.to.x ? dragDelta.value.x + link.to.x
: link.to.x : link.to.x
} }
y2={ y2={
nodeBeingDragged.value === link.to nodeBeingDragged.value === link.to.id
? dragDelta.value.y + link.to.y ? dragDelta.value.y + link.to.y
: link.to.y : link.to.y
} }
@ -328,22 +356,30 @@ export const main = createLayer("main", function (this: BaseLayer) {
const nextId = setupUniqueIds(() => nodes.value); const nextId = setupUniqueIds(() => nodes.value);
function filterNodes(n: NodeTypes) { function filterNodes(n: number | "cnode") {
return n !== nodeBeingDragged.value && n !== selected.value; return n !== nodeBeingDragged.value && n !== selected.value;
} }
function renderNode(node: NodeTypes | undefined) { function renderNodeById(id: number | "cnode" | undefined) {
if (node == undefined) { if (id == null) {
return undefined; return undefined;
} else if (node.type === "anode") { }
return renderNode(nodesById.value[id] ?? cNode);
}
function renderNode(node: NodeTypes | typeof cNode) {
if (node.type === "anode") {
return renderANode(node); return renderANode(node);
} else if (node.type === "bnode") { } else if (node.type === "bnode") {
return renderBNode(node); return renderBNode(node);
} else {
return render(node);
} }
} }
return { return {
name: "Tree", name: "Tree",
color: "var(--accent1)",
display: jsx(() => ( display: jsx(() => (
<> <>
<Board <Board
@ -354,18 +390,20 @@ export const main = createLayer("main", function (this: BaseLayer) {
ref={board} ref={board}
> >
<SVGNode>{links()}</SVGNode> <SVGNode>{links()}</SVGNode>
{nodes.value.filter(filterNodes).map(renderNode)} {nodes.value.filter(n => filterNodes(n.id)).map(renderNode)}
{filterNodes("cnode") && render(cNode)}
<SVGNode> <SVGNode>
{aActions()} {aActions()}
{bActions()} {bActions()}
</SVGNode> </SVGNode>
{renderNode(selected.value)} {renderNodeById(selected.value)}
{renderNode(nodeBeingDragged.value)} {renderNodeById(nodeBeingDragged.value)}
</Board> </Board>
</> </>
)), )),
boardNodes: nodes boardNodes: nodes,
// cNode cNode,
selected: persistent(selected)
}; };
}); });
@ -376,7 +414,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
export const getInitialLayers = ( export const getInitialLayers = (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
player: Partial<Player> player: Partial<Player>
): Array<GenericLayer> => [main, prestige]; ): Array<GenericLayer> => [main];
/** /**
* A computed ref whose value is true whenever the game is over. * A computed ref whose value is true whenever the game is over.

View file

@ -0,0 +1,37 @@
<template>
<div
:style="`transform: translate(calc(${unref(position).x}px - 50%), ${unref(position).y}px);`"
@mousedown="e => mouseDown(e)"
@touchstart.passive="e => mouseDown(e)"
@mouseup="e => mouseUp(e)"
@touchend.passive="e => mouseUp(e)"
>
<component v-if="comp" :is="comp" />
</div>
</template>
<script setup lang="tsx">
import { jsx } from "features/feature";
import { VueFeature, coerceComponent, renderJSX } from "util/vue";
import { Ref, shallowRef, unref } from "vue";
import { NodePosition } from "./board";
unref;
const props = defineProps<{
element: VueFeature;
mouseDown: (e: MouseEvent | TouchEvent) => void;
mouseUp: (e: MouseEvent | TouchEvent) => void;
position: Ref<NodePosition>;
}>();
const comp = shallowRef(coerceComponent(jsx(() => renderJSX(props.element))));
</script>
<style scoped>
div {
position: absolute;
top: 0;
left: 50%;
transition-duration: 0s;
}
</style>

View file

@ -1,12 +1,15 @@
import Board from "features/boards/Board.vue"; import Board from "features/boards/Board.vue";
import { jsx } from "features/feature"; import Draggable from "features/boards/Draggable.vue";
import { Component, GatherProps, GenericComponent, jsx } from "features/feature";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import { Persistent, persistent } from "game/persistence";
import type { PanZoom } from "panzoom"; import type { PanZoom } from "panzoom";
import { Direction, isFunction } from "util/common"; import { Direction, isFunction } from "util/common";
import type { Computable, ProcessedComputable } from "util/computed"; import type { Computable, ProcessedComputable } from "util/computed";
import { convertComputable } from "util/computed"; import { convertComputable } from "util/computed";
import { VueFeature } from "util/vue";
import type { ComponentPublicInstance, Ref } from "vue"; import type { ComponentPublicInstance, Ref } from "vue";
import { computed, ref, unref, watchEffect } from "vue"; import { computed, nextTick, ref, unref, watchEffect } from "vue";
import panZoom from "vue-panzoom"; import panZoom from "vue-panzoom";
globalBus.on("setupVue", app => panZoom.install(app)); globalBus.on("setupVue", app => panZoom.install(app));
@ -53,20 +56,20 @@ export function setupSelectable<T>() {
}; };
} }
export function setupDraggableNode<T extends NodePosition, S extends NodePosition = T>(options: { export function setupDraggableNode<T>(options: {
board: Ref<ComponentPublicInstance<typeof Board> | undefined>; board: Ref<ComponentPublicInstance<typeof Board> | undefined>;
receivingNodes?: NodeComputable<T, S[]>; getPosition: (node: T) => NodePosition;
dropAreaRadius?: NodeComputable<S, number>; setPosition: (node: T, position: NodePosition) => void;
isDraggable?: NodeComputable<T, boolean>; receivingNodes?: NodeComputable<T, T[]>;
onDrop?: (acceptingNode: S, draggingNode: T) => void; dropAreaRadius?: NodeComputable<T, number>;
onDrop?: (acceptingNode: T, draggingNode: T) => void;
}) { }) {
const nodeBeingDragged = ref<T>(); const nodeBeingDragged = ref<T>();
const receivingNode = ref<S>(); const receivingNode = ref<T>();
const hasDragged = ref(false); const hasDragged = ref(false);
const mousePosition = ref<NodePosition>(); const mousePosition = ref<NodePosition>();
const lastMousePosition = ref({ x: 0, y: 0 }); const lastMousePosition = ref({ x: 0, y: 0 });
const dragDelta = ref({ x: 0, y: 0 }); const dragDelta = ref({ x: 0, y: 0 });
const isDraggable = options.isDraggable ?? true;
const receivingNodes = computed(() => const receivingNodes = computed(() =>
nodeBeingDragged.value == null nodeBeingDragged.value == null
? [] ? []
@ -75,31 +78,26 @@ export function setupDraggableNode<T extends NodePosition, S extends NodePositio
); );
const dropAreaRadius = options.dropAreaRadius ?? 50; const dropAreaRadius = options.dropAreaRadius ?? 50;
watchEffect(() => {
if (nodeBeingDragged.value != null && !unwrapNodeRef(isDraggable, nodeBeingDragged.value)) {
result.endDrag();
}
});
watchEffect(() => { watchEffect(() => {
const node = nodeBeingDragged.value; const node = nodeBeingDragged.value;
if (node == null) { if (node == null) {
return null; return null;
} }
const originalPosition = options.getPosition(node);
const position = { const position = {
x: node.x + dragDelta.value.x, x: originalPosition.x + dragDelta.value.x,
y: node.y + dragDelta.value.y y: originalPosition.y + dragDelta.value.y
}; };
let smallestDistance = Number.MAX_VALUE; let smallestDistance = Number.MAX_VALUE;
receivingNode.value = unref(receivingNodes).reduce((smallest: S | undefined, curr: S) => { receivingNode.value = unref(receivingNodes).reduce((smallest: T | undefined, curr: T) => {
if ((curr as S | T) === node) { if ((curr as T) === node) {
return smallest; return smallest;
} }
const distanceSquared = const { x, y } = options.getPosition(curr);
Math.pow(position.x - curr.x, 2) + Math.pow(position.y - curr.y, 2); const distanceSquared = Math.pow(position.x - x, 2) + Math.pow(position.y - y, 2);
const size = unwrapNodeRef(dropAreaRadius, curr); const size = unwrapNodeRef(dropAreaRadius, curr);
if (distanceSquared > smallestDistance || distanceSquared > size * size) { if (distanceSquared > smallestDistance || distanceSquared > size * size) {
return smallest; return smallest;
@ -118,7 +116,7 @@ export function setupDraggableNode<T extends NodePosition, S extends NodePositio
lastMousePosition, lastMousePosition,
dragDelta, dragDelta,
receivingNodes, receivingNodes,
startDrag: function (e: MouseEvent | TouchEvent, node?: T) { startDrag: function (e: MouseEvent | TouchEvent, node: T) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -141,17 +139,17 @@ export function setupDraggableNode<T extends NodePosition, S extends NodePositio
dragDelta.value = { x: 0, y: 0 }; dragDelta.value = { x: 0, y: 0 };
hasDragged.value = false; hasDragged.value = false;
if (node != null && unwrapNodeRef(isDraggable, node)) {
nodeBeingDragged.value = node; nodeBeingDragged.value = node;
}
}, },
endDrag: function () { endDrag: function () {
if (nodeBeingDragged.value == null) { if (nodeBeingDragged.value == null) {
return; return;
} }
if (receivingNode.value == null) { if (receivingNode.value == null) {
nodeBeingDragged.value.x += Math.round(dragDelta.value.x / 25) * 25; const { x, y } = options.getPosition(nodeBeingDragged.value);
nodeBeingDragged.value.y += Math.round(dragDelta.value.y / 25) * 25; const newX = x + Math.round(dragDelta.value.x / 25) * 25;
const newY = y + Math.round(dragDelta.value.y / 25) * 25;
options.setPosition(nodeBeingDragged.value, { x: newX, y: newY });
} }
if (receivingNode.value != null) { if (receivingNode.value != null) {
@ -210,6 +208,64 @@ export function setupDraggableNode<T extends NodePosition, S extends NodePositio
return result; return result;
} }
export function makeDraggable<T extends VueFeature, S>(
element: T,
options: {
id: S;
nodeBeingDragged: Ref<S | undefined>;
hasDragged: Ref<boolean>;
dragDelta: Ref<NodePosition>;
startDrag: (e: MouseEvent | TouchEvent, id: S) => void;
endDrag: VoidFunction;
onMouseDown?: (e: MouseEvent | TouchEvent) => boolean | void;
onMouseUp?: (e: MouseEvent | TouchEvent) => boolean | void;
initialPosition?: NodePosition;
}
): asserts element is T & { position: Persistent<NodePosition> } {
const position = persistent(options.initialPosition ?? { x: 0, y: 0 });
(element as T & { position: Persistent<NodePosition> }).position = position;
const computedPosition = computed(() => {
if (options.nodeBeingDragged.value === options.id) {
return {
x: position.value.x + options.dragDelta.value.x,
y: position.value.y + options.dragDelta.value.y
};
}
return position.value;
});
function handleMouseDown(e: MouseEvent | TouchEvent) {
if (options.onMouseDown?.(e) === false) {
return;
}
if (options.nodeBeingDragged.value == null) {
options.startDrag(e, options.id);
}
}
function handleMouseUp(e: MouseEvent | TouchEvent) {
options.onMouseUp?.(e);
}
nextTick(() => {
const elementComponent = element[Component];
const elementGatherProps = element[GatherProps].bind(element);
element[Component] = Draggable as GenericComponent;
element[GatherProps] = function gatherTooltipProps(this: typeof options) {
return {
element: {
[Component]: elementComponent,
[GatherProps]: elementGatherProps
},
mouseDown: handleMouseDown,
mouseUp: handleMouseUp,
position: computedPosition
};
}.bind(options);
});
}
export function setupActions<T extends NodePosition>(options: { export function setupActions<T extends NodePosition>(options: {
node: Computable<T | undefined>; node: Computable<T | undefined>;
shouldShowActions?: NodeComputable<T, boolean>; shouldShowActions?: NodeComputable<T, boolean>;