mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 13:02:07 +03:00
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:
parent
b89d1b692a
commit
0974d5ff1e
@ -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
|
||||
|
@ -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)
|
||||
},
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user