Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{
"recommendations": ["Vue.volar", "streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint"]
"recommendations": [
"Vue.volar",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"aaron-bond.better-comments"
]
}
5 changes: 4 additions & 1 deletion packages/graph/src/base/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ export const MOVE_NODE_OPTIONS_DEFAULTS: MoveNodeOptions = {
broadcast: true,
};

export const BULK_MOVE_NODE_OPTIONS_DEFAULTS: MoveNodeOptions = {
export type BulkMoveNodeOptions = BroadcastOption & AnimateOption;

export const BULK_MOVE_NODE_OPTIONS_DEFAULTS: BulkMoveNodeOptions = {
broadcast: true,
animate: false,
};

export type EditEdgeLabelOptions = BroadcastOption & HistoryOption;
Expand Down
13 changes: 8 additions & 5 deletions packages/graph/src/base/useGraphCRUD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getConnectedEdges } from '../helpers';
import { nodeLetterLabelGetter } from '../labels';
import type { GraphSettings } from '../settings';
import type { GEdge, GNode } from '../types';
import { AnimationKeyframe } from './../../../shapes/src/animation/interpolation/types';
import type { GraphAnimations } from './animations';
import {
ADD_EDGE_DEFAULTS,
Expand All @@ -27,6 +28,7 @@ import {
import type {
AddEdgeOptions,
AddNodeOptions,
BulkMoveNodeOptions,
EditEdgeLabelOptions,
MoveNodeOptions,
RemoveEdgeOptions,
Expand Down Expand Up @@ -166,6 +168,7 @@ export const useGraphCRUD = ({
focus: false,
broadcast: false,
history: false,
animate: fullOptions.animate,
});
if (!newNode) continue;
createdNodes.push(newNode);
Expand Down Expand Up @@ -292,22 +295,22 @@ export const useGraphCRUD = ({

const bulkMoveNode = (
nodeMovements: GNodeMoveInstruction[],
options: Partial<MoveNodeOptions> = {},
options: Partial<BulkMoveNodeOptions> = {},
) => {
const fullOptions = {
...BULK_MOVE_NODE_OPTIONS_DEFAULTS,
...options,
};

const finalizeFrame = autoAnimate.captureFrame(() =>
draw(getCtx(magicCanvas.canvas)),
);
const animate = fullOptions.animate
? autoAnimate.captureFrame(() => draw(getCtx(magicCanvas.canvas)))
: null;

for (const { nodeId, coords } of nodeMovements) {
moveNode(nodeId, coords, fullOptions);
}

finalizeFrame();
animate?.();
};

const editEdgeLabel = (
Expand Down
33 changes: 18 additions & 15 deletions packages/graph/src/schematics/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ export const getEdgeSchematic = (
): Omit<SchemaItem, 'priority'> | undefined => {
const { displayEdgeLabels, isGraphDirected } = graph.settings.value;

const [from, to] = getConnectedNodes(edge.id, graph);
const edgesAlongPath = getEdgesAlongPath(from.id, to.id, graph);
const [fromNode, toNode] = getConnectedNodes(edge.id, graph);
const edgesAlongPath = getEdgesAlongPath(fromNode.id, toNode.id, graph);

const multipleEdgesInPath = edgesAlongPath.length > 1;
const isSelfDirected = to === from;
const isSelfDirected = toNode.id === fromNode.id;

const fromNodeBorderWidth = graph.getTheme('nodeBorderWidth', from);
const toNodeBorderWidth = graph.getTheme('nodeBorderWidth', to);
const fromNodeBorderWidth = graph.getTheme('nodeBorderWidth', fromNode);
const toNodeBorderWidth = graph.getTheme('nodeBorderWidth', toNode);

const fromNodeSize = graph.getTheme('nodeSize', from);
const toNodeSize = graph.getTheme('nodeSize', to);
const fromNodeSize = graph.getTheme('nodeSize', fromNode);
const toNodeSize = graph.getTheme('nodeSize', toNode);

const angle = Math.atan2(to.y - from.y, to.x - from.x);
const angle = Math.atan2(toNode.y - fromNode.y, toNode.x - fromNode.x);

const arrowHeadSpacingAwayFromNode =
toNodeBorderWidth / 2 + WHITESPACE_BETWEEN_ARROW_TIP_AND_NODE;
Expand All @@ -43,10 +43,10 @@ export const getEdgeSchematic = (
y: (toNodeSize + arrowHeadSpacingAwayFromNode) * Math.sin(angle),
};

const edgeStart = { x: from.x, y: from.y };
const edgeStart = { x: fromNode.x, y: fromNode.y };
const edgeEnd = {
x: to.x - (isGraphDirected ? arrowDrawOffset.x : 0),
y: to.y - (isGraphDirected ? arrowDrawOffset.y : 0),
x: toNode.x - (isGraphDirected ? arrowDrawOffset.x : 0),
y: toNode.y - (isGraphDirected ? arrowDrawOffset.y : 0),
};

const edgeWidth = graph.getTheme('edgeWidth', edge);
Expand Down Expand Up @@ -74,10 +74,13 @@ export const getEdgeSchematic = (
* from causing angle issues when no other edges are present
*/
graph.edges.value
.filter((e) => (e.from === from.id || e.to === to.id) && e.from !== e.to)
.filter(
(e) =>
(e.from === fromNode.id || e.to === toNode.id) && e.from !== e.to,
)
.map((e) => {
const [fromNode, toNode] = getConnectedNodes(e.id, graph);
return from.id === fromNode.id ? toNode : fromNode;
return fromNode.id === fromNode.id ? toNode : fromNode;
})
.filter(
(point, index, self) =>
Expand Down Expand Up @@ -117,7 +120,7 @@ export const getEdgeSchematic = (
const shape = graph.shapes.uturn({
id: edge.id,
spacing: edgeWidth * 1.2,
at: { x: from.x, y: from.y },
at: { x: fromNode.x, y: fromNode.y },
upDistance,
downDistance,
rotation: largestAngularSpace,
Expand All @@ -136,7 +139,7 @@ export const getEdgeSchematic = (
const sumOfToAndFromNodeSize =
fromNodeSize + fromNodeBorderWidth / 2 + toNodeSize + toNodeBorderWidth / 2;
const distanceSquaredBetweenNodes =
(from.x - to.x) ** 2 + (from.y - to.y) ** 2;
(fromNode.x - toNode.x) ** 2 + (fromNode.y - toNode.y) ** 2;
const areNodesTouching =
sumOfToAndFromNodeSize ** 2 > distanceSquaredBetweenNodes;

Expand Down
62 changes: 41 additions & 21 deletions packages/products/src/abstract-syntax-trees/MainView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { GEdge, GNode } from '@magic/graph/types';
import { getCtx } from '@magic/utils/ctx';
import { debounce } from '@magic/utils/debounce';
import * as ts from 'typescript';

Expand All @@ -12,27 +13,38 @@
import CodeEditor from './code-editor/CodeEditor.vue';
import { AST_GRAPH_SETTINGS } from './settings';

type ASTNode = ts.Node;
type ASTEdge = { fromNode: ts.Node; toNode: ts.Node };

const graphWithCanvas = useGraphWithCanvas(AST_GRAPH_SETTINGS);

/**
* coverts ts node object into a unique serializable form
* @example getNodeUniqueId(node) // "example.ts:VariableStatement:0-21"
*/
const getNodeUniqueId = (node: ts.Node) => {
const getASTNodeId = (node: ts.Node) => {
const kindName = ts.SyntaxKind[node.kind];
return `${kindName}:${node.pos}-${node.end}`;
};

const getASTEdgeId = ({
fromNode,
toNode,
}: {
fromNode: ts.Node;
toNode: ts.Node;
}) => {
const fromNodeId = getASTNodeId(fromNode);
const toNodeId = getASTNodeId(toNode);
return `[${fromNodeId}]->[${toNodeId}]`;
};

const getAllNodesAndEdges = (node: ts.Node) => {
const nodeIds: string[] = [];
const edges: { from: string; to: string }[] = [];
const processNode = (node: ts.Node) => {
const parentNodeId = node.parent
? getNodeUniqueId(node.parent)
: undefined;
const nodeId = getNodeUniqueId(node);
if (parentNodeId) edges.push({ from: parentNodeId, to: nodeId });
nodeIds.push(nodeId);
const nodeIds: ASTNode[] = [];
const edges: ASTEdge[] = [];
const processNode = (node: ASTNode) => {
if (node.parent) edges.push({ fromNode: node.parent, toNode: node });
nodeIds.push(node);
node.forEachChild(processNode);
};
processNode(node);
Expand Down Expand Up @@ -62,18 +74,19 @@
const graphNodesAndEdges = computed(() => {
const { nodes, edges } = astNodesAndEdges.value;
const graphNodes = nodes.map(
(nodeId): GNode => ({
id: nodeId,
label: nodeId,
(astNode): GNode => ({
id: getASTNodeId(astNode),
label: getASTNodeId(astNode),
x: 0,
y: 0,
}),
);

const graphEdges = edges.map(
(e): GEdge => ({
...e,
id: `${e.from}-${e.to}`,
(astEdge): GEdge => ({
to: getASTNodeId(astEdge.toNode),
from: getASTNodeId(astEdge.fromNode),
id: getASTEdgeId(astEdge),
label: '',
}),
);
Expand All @@ -85,23 +98,30 @@
};
});

const loadAst = () => {
graphWithCanvas.graph.load(graphNodesAndEdges.value);
const loadAst = async () => {
const { graph, canvas } = graphWithCanvas;
const draw = () => {
const ctx = getCtx(canvas.canvas.value);
graph.draw(ctx);
};
const animate = graph.autoAnimate.captureFrame(draw);
graph.load(graphNodesAndEdges.value);
shapeGraph(graphNodesAndEdges.value.rootNode);
animate();
};

onMounted(loadAst);
const debouncedLoadAst = debounce(loadAst, 2000);
const debouncedLoadAst = debounce(loadAst, 500);

watch(code, debouncedLoadAst);
</script>

<template>
<GraphProduct v-bind="graphWithCanvas">
<template #top-center>
<GButton @click="shapeGraph(graphNodesAndEdges.rootNode)">
<!-- <GButton @click="shapeGraph(graphNodesAndEdges.rootNode)">
Shape
</GButton>
</GButton> -->
</template>
<template #center-left>
<CodeEditor v-model="code" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const treeArrayToGraph = (
})
.filter(Boolean) as GNodeMoveInstruction[];

graph.bulkMoveNode(movementObj);
graph.bulkMoveNode(movementObj, { animate: true });

for (const edge of newTreeEdges) {
graph.addEdge(edge, { animate: true });
Expand Down
4 changes: 3 additions & 1 deletion packages/products/src/sandbox/ui/IslandToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@
return canRedo.value;
});

const treePositionerControls = useTreeGraphPositionerSync(graph.value);
const treePositionerControls = useTreeGraphPositionerSync(graph.value, {
animate: true,
});
</script>

<template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ export type UseTreeGraphPositionerOptions = {
* @default (rootNode) => ({ x: rootNode.x, y: rootNode.y })
*/
rootNodeCoordinates: Coordinate | ((rootNode: GNode) => Coordinate);
/**
* if `true`, nodes and edges animate into place
* @default false
*/
animate: boolean;
};

export const TREE_FORMATION_OPTIONS_DEFAULTS = {
xOffset: 250,
yOffset: 200,
shape: 'standard',
animate: false,
rootNodeCoordinates: (rootNode) => ({ x: rootNode.x, y: rootNode.y }),
} as const satisfies UseTreeGraphPositionerOptions;

Expand Down Expand Up @@ -80,7 +86,7 @@ export const useTreeGraphPositioner = (
if (!newPositions) return;

reshapingActive.value = true;
await graph.bulkMoveNode(newPositions);
await graph.bulkMoveNode(newPositions, { animate: treeOptions.animate });
reshapingActive.value = false;
};

Expand All @@ -90,3 +96,7 @@ export const useTreeGraphPositioner = (
options: optionsRef,
};
};

export type TreeGraphPositionerControls = ReturnType<
typeof useTreeGraphPositioner
>;
3 changes: 3 additions & 0 deletions packages/shapes/src/animation/autoAnimate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export const useAutoAnimate = (
* finalize(); // triggers animation between captured states
*/
captureFrame: (flushDraw: () => void) => {
// clear any stale snapshots left over from previous abandoned frames
snapshotMap.clear();

const takeSnapshot = (state: 'before' | 'after') => {
capturedSchemas = [];
activelyCapturingSchemas = true;
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/ctx/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { MaybeRef } from 'vue';

/**
* pulls ctx from a canvas or canvas ref (vue.js), throws if not found
* gets `ctx` from a `<canvas />` or canvas ref (vue.js)
*
* @returns {CanvasRenderingContext2D}
* @example const ctx = getCtx(canvasRef);
* // ctx is defined and ready to use
* @throws {Error} if canvas or 2d context not found
* @throws {Error} if canvas element isn't in the DOM or `canvas.getContext` returns `null`
*/
export const getCtx = (
canvasInput: MaybeRef<HTMLCanvasElement | null | undefined>,
Expand Down
Loading