diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue index e70bd541ca3..1b67bade7af 100644 --- a/app/gui2/src/components/GraphEditor.vue +++ b/app/gui2/src/components/GraphEditor.vue @@ -149,6 +149,7 @@ const graphBindingsHandler = graphBindings.handler({ for (const node of nodeSelection.selected) { graphStore.deleteNode(node) } + nodeSelection.selected.clear() }) }, zoomToSelected() { @@ -158,7 +159,7 @@ const graphBindingsHandler = graphBindings.handler({ let right = -Infinity let bottom = -Infinity const nodesToCenter = - nodeSelection.selected.size === 0 ? graphStore.nodeRects.keys() : nodeSelection.selected + nodeSelection.selected.size === 0 ? graphStore.currentNodeIds : nodeSelection.selected for (const id of nodesToCenter) { const rect = graphStore.nodeRects.get(id) if (!rect) continue diff --git a/app/gui2/src/components/GraphEditor/__tests__/dragging.test.ts b/app/gui2/src/components/GraphEditor/__tests__/dragging.test.ts index 0132d71e470..2ea662446e1 100644 --- a/app/gui2/src/components/GraphEditor/__tests__/dragging.test.ts +++ b/app/gui2/src/components/GraphEditor/__tests__/dragging.test.ts @@ -96,8 +96,8 @@ test.each` } const grid = new SnapGrid(computed(() => rects)) const xSnapped = new Rect(new Vec2(snappedRectPosition, 0.0), new Vec2(10.0, 10.0)) - expect(grid.snap(xSnapped, 15.0)[0]).toBeCloseTo(expectedSnap) + expect(grid.snap(xSnapped, 16.0).x).toBeCloseTo(expectedSnap) const ySnapped = new Rect(new Vec2(0.0, snappedRectPosition), new Vec2(10.0, 10.0)) - expect(grid.snap(ySnapped, 15.0)[1]).toBeCloseTo(expectedSnap) + expect(grid.snap(ySnapped, 16.0).y).toBeCloseTo(expectedSnap) }, ) diff --git a/app/gui2/src/components/GraphEditor/dragging.ts b/app/gui2/src/components/GraphEditor/dragging.ts index b0867ec3ea5..0ad8b5ae7b7 100644 --- a/app/gui2/src/components/GraphEditor/dragging.ts +++ b/app/gui2/src/components/GraphEditor/dragging.ts @@ -4,13 +4,19 @@ import { useApproach } from '@/util/animation' import { partitionPoint } from '@/util/array' import type { Opt } from '@/util/opt' import { Rect } from '@/util/rect' +import theme from '@/util/theme.json' import { Vec2 } from '@/util/vec2' import { iteratorFilter } from 'lib0/iterator' -import { abs } from 'lib0/math' import type { ExprId } from 'shared/yjsModel' import { computed, markRaw, ref, watchEffect, type ComputedRef, type WatchStopHandle } from 'vue' -const DRAG_SNAP_THRESHOLD = 15 +const DRAG_SNAP_THRESHOLD = 16 +const VERTICAL_GAP = theme.node.vertical_gap + +interface PartialVec2 { + x: number | null + y: number | null +} export class SnapGrid { leftAxes: ComputedRef @@ -26,29 +32,35 @@ export class SnapGrid { this.rightAxes = computed(() => Array.from(rects.value, (rect) => rect.right).sort((a, b) => a - b), ) - this.topAxes = computed(() => Array.from(rects.value, (rect) => rect.top).sort((a, b) => a - b)) + this.topAxes = computed(() => + Array.from(rects.value, (rect) => rect.top) + .concat(Array.from(rects.value, (rect) => rect.bottom + VERTICAL_GAP)) + .sort((a, b) => a - b), + ) this.bottomAxes = computed(() => - Array.from(rects.value, (rect) => rect.bottom).sort((a, b) => a - b), + Array.from(rects.value, (rect) => rect.bottom) + .concat(Array.from(rects.value, (rect) => rect.top - VERTICAL_GAP)) + .sort((a, b) => a - b), ) } snappedMany(rects: Rect[], threshold: number): Vec2 { - const minSnap = rects.reduce<[x: number | null, y: number | null]>( + const minSnap = rects.reduce( (minSnap, rect) => { - const [xSnap, ySnap] = this.snap(rect, threshold) - return [SnapGrid.minSnap(minSnap[0], xSnap), SnapGrid.minSnap(minSnap[1], ySnap)] + const snap = this.snap(rect, threshold) + return { x: SnapGrid.minSnap(minSnap.x, snap.x), y: SnapGrid.minSnap(minSnap.y, snap.y) } }, - [null, null], + { x: null, y: null }, ) - return new Vec2(minSnap[0] ?? 0.0, minSnap[1] ?? 0.0) + return new Vec2(minSnap.x ?? 0.0, minSnap.y ?? 0.0) } - snap(rect: Rect, threshold: number): [x: number | null, y: number | null] { + snap(rect: Rect, threshold: number): PartialVec2 { const leftSnap = SnapGrid.boundSnap(rect.left, this.leftAxes.value, threshold) const rightSnap = SnapGrid.boundSnap(rect.right, this.rightAxes.value, threshold) const topSnap = SnapGrid.boundSnap(rect.top, this.topAxes.value, threshold) const bottomSnap = SnapGrid.boundSnap(rect.bottom, this.bottomAxes.value, threshold) - return [SnapGrid.minSnap(leftSnap, rightSnap), SnapGrid.minSnap(topSnap, bottomSnap)] + return { x: SnapGrid.minSnap(leftSnap, rightSnap), y: SnapGrid.minSnap(topSnap, bottomSnap) } } private static boundSnap(value: number, axes: number[], threshold: number): number | null { @@ -58,11 +70,11 @@ export class SnapGrid { const notLowerNearest = axes[firstNotLower] const snapToHigher = notLowerNearest != null ? notLowerNearest - value : null const snap = SnapGrid.minSnap(snapToLower, snapToHigher) - return snap != null && abs(snap) <= threshold ? snap : null + return snap != null && Math.abs(snap) <= threshold ? snap : null } private static minSnap(a: Opt, b: Opt): number | null { - if (a != null && b != null) return abs(a) < abs(b) ? a : b + if (a != null && b != null) return Math.abs(a) < Math.abs(b) ? a : b else return a ?? b ?? null } } @@ -135,10 +147,10 @@ export function useDragging() { const newSnappedOffsetTarget = snappedOffsetTarget.value // Skip animation if target offset does not change significantly, to avoid shivering // when node is snapped. - if (abs(newSnappedOffsetTarget.x - oldSnappedOffset.x) < 2.0) { + if (Math.abs(newSnappedOffsetTarget.x - oldSnappedOffset.x) < 2.0) { snapX.skip() } - if (abs(newSnappedOffsetTarget.y - oldSnappedOffset.y) < 2.0) { + if (Math.abs(newSnappedOffsetTarget.y - oldSnappedOffset.y) < 2.0) { snapY.skip() } } @@ -151,10 +163,10 @@ export function useDragging() { createSnapGrid() { const nonDraggedRects = computed(() => { const nonDraggedNodes = iteratorFilter( - graphStore.nodeRects.entries(), - ([id]) => !this.draggedNodes.has(id), + graphStore.currentNodeIds.values(), + (id) => !this.draggedNodes.has(id), ) - return Array.from(nonDraggedNodes, ([, rect]) => rect) + return Array.from(nonDraggedNodes, (id) => graphStore.nodeRects.get(id)!) }) return new SnapGrid(nonDraggedRects) } diff --git a/app/gui2/src/stores/graph/graphDatabase.ts b/app/gui2/src/stores/graph/graphDatabase.ts index 43067ec0385..44fa5da344e 100644 --- a/app/gui2/src/stores/graph/graphDatabase.ts +++ b/app/gui2/src/stores/graph/graphDatabase.ts @@ -290,9 +290,10 @@ export class GraphDb { } const functionAst = functionAst_.astExtended - if (!functionAst) return - if (!functionAst.isTree(RawAst.Tree.Type.Function)) return - this.bindings.readFunctionAst(functionAst) + if (functionAst?.isTree(RawAst.Tree.Type.Function)) { + this.bindings.readFunctionAst(functionAst) + } + return currentNodeIds } assignUpdatedMetadata(node: Node, meta: NodeMetadata) { diff --git a/app/gui2/src/stores/graph/index.ts b/app/gui2/src/stores/graph/index.ts index 43635f68cef..428aa4ba2f0 100644 --- a/app/gui2/src/stores/graph/index.ts +++ b/app/gui2/src/stores/graph/index.ts @@ -54,6 +54,7 @@ export const useGraphStore = defineStore('graph', () => { const editedNodeInfo = ref() const imports = ref<{ import: Import; span: ContentRange }[]>([]) const methodAst = ref() + const currentNodeIds = ref(new Set()) const unconnectedEdge = ref() @@ -109,7 +110,7 @@ export const useGraphStore = defineStore('graph', () => { methodAst.value = getExecutedMethodAst(newRoot, proj.executionContext.getStackTop(), db) if (methodAst.value) { - db.readFunctionAst(methodAst.value, (id) => meta.get(id)) + currentNodeIds.value = db.readFunctionAst(methodAst.value, (id) => meta.get(id)) } }) } @@ -215,6 +216,8 @@ export const useGraphStore = defineStore('graph', () => { const node = db.nodeIdToNode.get(id) if (!node) return proj.module?.deleteExpression(node.outerExprId) + nodeRects.delete(id) + node.pattern?.visitRecursive((ast) => exprRects.delete(ast.astId)) } function setNodeContent(id: ExprId, content: string) { @@ -331,6 +334,7 @@ export const useGraphStore = defineStore('graph', () => { editedNodeInfo, unconnectedEdge, edges, + currentNodeIds, nodeRects, vizRects, exprRects, diff --git a/app/gui2/src/util/navigator.ts b/app/gui2/src/util/navigator.ts index 5fa7b4c9fb6..dc886b628ff 100644 --- a/app/gui2/src/util/navigator.ts +++ b/app/gui2/src/util/navigator.ts @@ -84,7 +84,11 @@ export function useNavigator(viewportNode: Ref) { viewportNode.value.clientWidth / rect.width, ), ) - targetCenter.value = new Vec2(rect.left + rect.width / 2, rect.top + rect.height / 2) + const centerX = + !Number.isFinite(rect.left) && !Number.isFinite(rect.width) ? 0 : rect.left + rect.width / 2 + const centerY = + !Number.isFinite(rect.top) && !Number.isFinite(rect.height) ? 0 : rect.top + rect.height / 2 + targetCenter.value = new Vec2(centerX, centerY) } let zoomPivot = Vec2.Zero