Add new magnet axes (#8506)

- Closes #8460
- Add new magnet axes 32px below the bottom of the previous node, and 32px above the top of the next node

- Fix a bug where magnet alignment and "zoom to all" take into account nodes that no longer exist
- Fix a bug where "zoom to all" breaks completely right after deleting a node

# Important Notes
- Snapping with bounded cross axes were discussed during refinement, but are out of scope of this PR.
This commit is contained in:
somebody1234 2023-12-12 03:55:37 +10:00 committed by GitHub
parent b89d1b692a
commit 0974d5ff1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 48 additions and 26 deletions

View File

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

View File

@ -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)
},
)

View File

@ -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<number[]>
@ -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<PartialVec2>(
(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<number>, b: Opt<number>): 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)
}

View File

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

View File

@ -54,6 +54,7 @@ export const useGraphStore = defineStore('graph', () => {
const editedNodeInfo = ref<NodeEditInfo>()
const imports = ref<{ import: Import; span: ContentRange }[]>([])
const methodAst = ref<Ast.Function>()
const currentNodeIds = ref(new Set<ExprId>())
const unconnectedEdge = ref<UnconnectedEdge>()
@ -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,

View File

@ -84,7 +84,11 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
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