mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 18:34:03 +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) {
|
for (const node of nodeSelection.selected) {
|
||||||
graphStore.deleteNode(node)
|
graphStore.deleteNode(node)
|
||||||
}
|
}
|
||||||
|
nodeSelection.selected.clear()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
zoomToSelected() {
|
zoomToSelected() {
|
||||||
@ -158,7 +159,7 @@ const graphBindingsHandler = graphBindings.handler({
|
|||||||
let right = -Infinity
|
let right = -Infinity
|
||||||
let bottom = -Infinity
|
let bottom = -Infinity
|
||||||
const nodesToCenter =
|
const nodesToCenter =
|
||||||
nodeSelection.selected.size === 0 ? graphStore.nodeRects.keys() : nodeSelection.selected
|
nodeSelection.selected.size === 0 ? graphStore.currentNodeIds : nodeSelection.selected
|
||||||
for (const id of nodesToCenter) {
|
for (const id of nodesToCenter) {
|
||||||
const rect = graphStore.nodeRects.get(id)
|
const rect = graphStore.nodeRects.get(id)
|
||||||
if (!rect) continue
|
if (!rect) continue
|
||||||
|
@ -96,8 +96,8 @@ test.each`
|
|||||||
}
|
}
|
||||||
const grid = new SnapGrid(computed(() => rects))
|
const grid = new SnapGrid(computed(() => rects))
|
||||||
const xSnapped = new Rect(new Vec2(snappedRectPosition, 0.0), new Vec2(10.0, 10.0))
|
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))
|
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 { partitionPoint } from '@/util/array'
|
||||||
import type { Opt } from '@/util/opt'
|
import type { Opt } from '@/util/opt'
|
||||||
import { Rect } from '@/util/rect'
|
import { Rect } from '@/util/rect'
|
||||||
|
import theme from '@/util/theme.json'
|
||||||
import { Vec2 } from '@/util/vec2'
|
import { Vec2 } from '@/util/vec2'
|
||||||
import { iteratorFilter } from 'lib0/iterator'
|
import { iteratorFilter } from 'lib0/iterator'
|
||||||
import { abs } from 'lib0/math'
|
|
||||||
import type { ExprId } from 'shared/yjsModel'
|
import type { ExprId } from 'shared/yjsModel'
|
||||||
import { computed, markRaw, ref, watchEffect, type ComputedRef, type WatchStopHandle } from 'vue'
|
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 {
|
export class SnapGrid {
|
||||||
leftAxes: ComputedRef<number[]>
|
leftAxes: ComputedRef<number[]>
|
||||||
@ -26,29 +32,35 @@ export class SnapGrid {
|
|||||||
this.rightAxes = computed(() =>
|
this.rightAxes = computed(() =>
|
||||||
Array.from(rects.value, (rect) => rect.right).sort((a, b) => a - b),
|
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(() =>
|
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 {
|
snappedMany(rects: Rect[], threshold: number): Vec2 {
|
||||||
const minSnap = rects.reduce<[x: number | null, y: number | null]>(
|
const minSnap = rects.reduce<PartialVec2>(
|
||||||
(minSnap, rect) => {
|
(minSnap, rect) => {
|
||||||
const [xSnap, ySnap] = this.snap(rect, threshold)
|
const snap = this.snap(rect, threshold)
|
||||||
return [SnapGrid.minSnap(minSnap[0], xSnap), SnapGrid.minSnap(minSnap[1], ySnap)]
|
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 leftSnap = SnapGrid.boundSnap(rect.left, this.leftAxes.value, threshold)
|
||||||
const rightSnap = SnapGrid.boundSnap(rect.right, this.rightAxes.value, threshold)
|
const rightSnap = SnapGrid.boundSnap(rect.right, this.rightAxes.value, threshold)
|
||||||
const topSnap = SnapGrid.boundSnap(rect.top, this.topAxes.value, threshold)
|
const topSnap = SnapGrid.boundSnap(rect.top, this.topAxes.value, threshold)
|
||||||
const bottomSnap = SnapGrid.boundSnap(rect.bottom, this.bottomAxes.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 {
|
private static boundSnap(value: number, axes: number[], threshold: number): number | null {
|
||||||
@ -58,11 +70,11 @@ export class SnapGrid {
|
|||||||
const notLowerNearest = axes[firstNotLower]
|
const notLowerNearest = axes[firstNotLower]
|
||||||
const snapToHigher = notLowerNearest != null ? notLowerNearest - value : null
|
const snapToHigher = notLowerNearest != null ? notLowerNearest - value : null
|
||||||
const snap = SnapGrid.minSnap(snapToLower, snapToHigher)
|
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 {
|
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
|
else return a ?? b ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,10 +147,10 @@ export function useDragging() {
|
|||||||
const newSnappedOffsetTarget = snappedOffsetTarget.value
|
const newSnappedOffsetTarget = snappedOffsetTarget.value
|
||||||
// Skip animation if target offset does not change significantly, to avoid shivering
|
// Skip animation if target offset does not change significantly, to avoid shivering
|
||||||
// when node is snapped.
|
// when node is snapped.
|
||||||
if (abs(newSnappedOffsetTarget.x - oldSnappedOffset.x) < 2.0) {
|
if (Math.abs(newSnappedOffsetTarget.x - oldSnappedOffset.x) < 2.0) {
|
||||||
snapX.skip()
|
snapX.skip()
|
||||||
}
|
}
|
||||||
if (abs(newSnappedOffsetTarget.y - oldSnappedOffset.y) < 2.0) {
|
if (Math.abs(newSnappedOffsetTarget.y - oldSnappedOffset.y) < 2.0) {
|
||||||
snapY.skip()
|
snapY.skip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,10 +163,10 @@ export function useDragging() {
|
|||||||
createSnapGrid() {
|
createSnapGrid() {
|
||||||
const nonDraggedRects = computed(() => {
|
const nonDraggedRects = computed(() => {
|
||||||
const nonDraggedNodes = iteratorFilter(
|
const nonDraggedNodes = iteratorFilter(
|
||||||
graphStore.nodeRects.entries(),
|
graphStore.currentNodeIds.values(),
|
||||||
([id]) => !this.draggedNodes.has(id),
|
(id) => !this.draggedNodes.has(id),
|
||||||
)
|
)
|
||||||
return Array.from(nonDraggedNodes, ([, rect]) => rect)
|
return Array.from(nonDraggedNodes, (id) => graphStore.nodeRects.get(id)!)
|
||||||
})
|
})
|
||||||
return new SnapGrid(nonDraggedRects)
|
return new SnapGrid(nonDraggedRects)
|
||||||
}
|
}
|
||||||
|
@ -290,10 +290,11 @@ export class GraphDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const functionAst = functionAst_.astExtended
|
const functionAst = functionAst_.astExtended
|
||||||
if (!functionAst) return
|
if (functionAst?.isTree(RawAst.Tree.Type.Function)) {
|
||||||
if (!functionAst.isTree(RawAst.Tree.Type.Function)) return
|
|
||||||
this.bindings.readFunctionAst(functionAst)
|
this.bindings.readFunctionAst(functionAst)
|
||||||
}
|
}
|
||||||
|
return currentNodeIds
|
||||||
|
}
|
||||||
|
|
||||||
assignUpdatedMetadata(node: Node, meta: NodeMetadata) {
|
assignUpdatedMetadata(node: Node, meta: NodeMetadata) {
|
||||||
const newPosition = new Vec2(meta.x, -meta.y)
|
const newPosition = new Vec2(meta.x, -meta.y)
|
||||||
|
@ -54,6 +54,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
const editedNodeInfo = ref<NodeEditInfo>()
|
const editedNodeInfo = ref<NodeEditInfo>()
|
||||||
const imports = ref<{ import: Import; span: ContentRange }[]>([])
|
const imports = ref<{ import: Import; span: ContentRange }[]>([])
|
||||||
const methodAst = ref<Ast.Function>()
|
const methodAst = ref<Ast.Function>()
|
||||||
|
const currentNodeIds = ref(new Set<ExprId>())
|
||||||
|
|
||||||
const unconnectedEdge = ref<UnconnectedEdge>()
|
const unconnectedEdge = ref<UnconnectedEdge>()
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
|
|
||||||
methodAst.value = getExecutedMethodAst(newRoot, proj.executionContext.getStackTop(), db)
|
methodAst.value = getExecutedMethodAst(newRoot, proj.executionContext.getStackTop(), db)
|
||||||
if (methodAst.value) {
|
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)
|
const node = db.nodeIdToNode.get(id)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
proj.module?.deleteExpression(node.outerExprId)
|
proj.module?.deleteExpression(node.outerExprId)
|
||||||
|
nodeRects.delete(id)
|
||||||
|
node.pattern?.visitRecursive((ast) => exprRects.delete(ast.astId))
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNodeContent(id: ExprId, content: string) {
|
function setNodeContent(id: ExprId, content: string) {
|
||||||
@ -331,6 +334,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
editedNodeInfo,
|
editedNodeInfo,
|
||||||
unconnectedEdge,
|
unconnectedEdge,
|
||||||
edges,
|
edges,
|
||||||
|
currentNodeIds,
|
||||||
nodeRects,
|
nodeRects,
|
||||||
vizRects,
|
vizRects,
|
||||||
exprRects,
|
exprRects,
|
||||||
|
@ -84,7 +84,11 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
|||||||
viewportNode.value.clientWidth / rect.width,
|
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
|
let zoomPivot = Vec2.Zero
|
||||||
|
Loading…
Reference in New Issue
Block a user