mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 02:21:54 +03:00
Layout nodes without position (#8326)
- Closes #8071 # Important Notes There is currently no way to predict the width a node, taking into account the width of widgets. This should probably be done in another task.
This commit is contained in:
parent
062992bb8b
commit
375e610660
@ -21,7 +21,8 @@ const radius = size.y / 2
|
||||
|
||||
const getScreenBounds = vi.fn(() => defaultScreenBounds)
|
||||
const getNodeRects = vi.fn(() => iterable.empty())
|
||||
const getGap = vi.fn(() => 24)
|
||||
const getHorizontalGap = vi.fn(() => 24)
|
||||
const getVerticalGap = vi.fn(() => 24)
|
||||
const getSelectedNodeRects = vi.fn(() => iterable.empty())
|
||||
const getMousePosition = vi.fn(() => Vec2.Zero)
|
||||
// Center is at (1100, 700)
|
||||
@ -57,36 +58,36 @@ describe('Non dictated placement', () => {
|
||||
|
||||
test.each([
|
||||
// === Miscellaneous tests ===
|
||||
{ desc: 'Empty graph', nodes: [], pos: new Vec2(1050, 690) },
|
||||
{ desc: 'Empty graph', nodes: [], pos: new Vec2(1090, 690) },
|
||||
|
||||
// === Single node tests ===
|
||||
{ desc: 'Single node', nodes: [rectAt(1050, 690)], pos: new Vec2(1050, 734) },
|
||||
{ desc: 'Single node', nodes: [rectAt(1050, 690)], pos: new Vec2(1090, 734) },
|
||||
//
|
||||
{
|
||||
desc: 'Single node (far enough left that it does not overlap)',
|
||||
nodes: [rectAt(950, 690)],
|
||||
pos: new Vec2(1050, 690),
|
||||
nodes: [rectAt(990, 690)],
|
||||
pos: new Vec2(1090, 690),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (far enough right that it does not overlap)',
|
||||
nodes: [rectAt(1150, 690)],
|
||||
pos: new Vec2(1050, 690),
|
||||
nodes: [rectAt(1190, 690)],
|
||||
pos: new Vec2(1090, 690),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (overlaps on the left by 1px)',
|
||||
nodes: [rectAt(951, 690)],
|
||||
pos: new Vec2(1050, 734),
|
||||
nodes: [rectAt(991, 690)],
|
||||
pos: new Vec2(1090, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (overlaps on the right by 1px)',
|
||||
nodes: [rectAt(1149, 690)],
|
||||
pos: new Vec2(1050, 734),
|
||||
nodes: [rectAt(1189, 690)],
|
||||
pos: new Vec2(1090, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (BIG gap)',
|
||||
nodes: [rectAt(1050, 690)],
|
||||
gap: 1000,
|
||||
pos: new Vec2(1050, 1710),
|
||||
pos: new Vec2(1090, 1710),
|
||||
pan: new Vec2(0, 1020),
|
||||
},
|
||||
|
||||
@ -94,12 +95,12 @@ describe('Non dictated placement', () => {
|
||||
{
|
||||
desc: 'Multiple nodes',
|
||||
nodes: map(range(0, 1001, 20), rectAtX(1050)),
|
||||
pos: new Vec2(1050, 1044),
|
||||
pos: new Vec2(1090, 1044),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with gap',
|
||||
nodes: map(range(1000, -1, -20), rectAtX(1050)),
|
||||
pos: new Vec2(1050, 1044),
|
||||
pos: new Vec2(1090, 1044),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with gap 2',
|
||||
@ -107,17 +108,17 @@ describe('Non dictated placement', () => {
|
||||
map(range(500, 901, 20), rectAtX(1050)),
|
||||
map(range(1000, 1501, 20), rectAtX(1050)),
|
||||
),
|
||||
pos: new Vec2(1050, 944),
|
||||
pos: new Vec2(1090, 944),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with gap (just big enough)',
|
||||
nodes: map(range(690, 1500, 88), rectAtX(1050)),
|
||||
pos: new Vec2(1050, 734),
|
||||
pos: new Vec2(1090, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with gap (slightly too small)',
|
||||
nodes: map(range(500, 849, 87), rectAtX(1050)),
|
||||
pos: new Vec2(1050, 892),
|
||||
pos: new Vec2(1090, 892),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with smallest gap',
|
||||
@ -125,7 +126,7 @@ describe('Non dictated placement', () => {
|
||||
map(range(500, 901, 20), rectAtX(1050)),
|
||||
map(range(988, 1489, 20), rectAtX(1050)),
|
||||
),
|
||||
pos: new Vec2(1050, 944),
|
||||
pos: new Vec2(1090, 944),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with smallest gap (reverse)',
|
||||
@ -133,7 +134,7 @@ describe('Non dictated placement', () => {
|
||||
map(range(1488, 987, -20), rectAtX(1050)),
|
||||
map(range(900, 499, -20), rectAtX(1050)),
|
||||
),
|
||||
pos: new Vec2(1050, 944),
|
||||
pos: new Vec2(1090, 944),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with gap that is too small',
|
||||
@ -143,7 +144,7 @@ describe('Non dictated placement', () => {
|
||||
),
|
||||
// This gap is 1px smaller than the previous test - so, 1px too small.
|
||||
// This position is offscreen (y >= 1000), so we pan so that the new node is centered (1531 - 690).
|
||||
pos: new Vec2(1050, 1531),
|
||||
pos: new Vec2(1090, 1531),
|
||||
pan: new Vec2(0, 841),
|
||||
},
|
||||
{
|
||||
@ -152,12 +153,15 @@ describe('Non dictated placement', () => {
|
||||
map(range(900, 499, -20), rectAtX(1050)),
|
||||
map(range(1487, 986, -20), rectAtX(1050)),
|
||||
),
|
||||
pos: new Vec2(1050, 1531),
|
||||
pos: new Vec2(1090, 1531),
|
||||
pan: new Vec2(0, 841),
|
||||
},
|
||||
])('$desc', ({ nodes, pos, gap, pan }) => {
|
||||
expect(
|
||||
nonDictatedPlacement(nodeSize, nonDictatedEnvironment(nodes), gap ? { gap } : {}),
|
||||
nonDictatedPlacement(nodeSize, nonDictatedEnvironment(nodes), {
|
||||
horizontalGap: gap ?? 24,
|
||||
verticalGap: gap ?? 24,
|
||||
}),
|
||||
).toEqual({ position: pos, pan })
|
||||
expect(getSelectedNodeRects, 'Should not depend on `selectedNodeRects`').not.toHaveBeenCalled()
|
||||
expect(getMousePosition, 'Should not depend on `mousePosition`').not.toHaveBeenCalled()
|
||||
@ -213,36 +217,36 @@ describe('Previous node dictated placement', () => {
|
||||
|
||||
test.each([
|
||||
// === Single node tests ===
|
||||
{ desc: 'Single node', nodes: [], pos: new Vec2(1050, 734) },
|
||||
{ desc: 'Single node', nodes: [], pos: new Vec2(1090, 734) },
|
||||
{
|
||||
desc: 'Single node (far enough up that it does not overlap)',
|
||||
nodes: [rectAt(1150, 714)],
|
||||
pos: new Vec2(1050, 734),
|
||||
nodes: [rectAt(1189, 714)],
|
||||
pos: new Vec2(1090, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (far enough down that it does not overlap)',
|
||||
nodes: [rectAt(1150, 754)],
|
||||
pos: new Vec2(1050, 734),
|
||||
nodes: [rectAt(1189, 754)],
|
||||
pos: new Vec2(1090, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (far enough left that it does not overlap)',
|
||||
nodes: [rectAt(926, 734)],
|
||||
pos: new Vec2(1050, 734),
|
||||
nodes: [rectAt(966, 734)],
|
||||
pos: new Vec2(1090, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (overlapping on the left by 1px)',
|
||||
nodes: [rectAt(927, 734)],
|
||||
pos: new Vec2(1051, 734),
|
||||
nodes: [rectAt(967, 734)],
|
||||
pos: new Vec2(1091, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (blocking initial position)',
|
||||
nodes: [rectAt(1050, 734)],
|
||||
pos: new Vec2(1174, 734),
|
||||
nodes: [rectAt(1090, 734)],
|
||||
pos: new Vec2(1214, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (far enough right that it does not overlap)',
|
||||
nodes: [rectAt(1174, 690)],
|
||||
pos: new Vec2(1050, 734),
|
||||
pos: new Vec2(1090, 734),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (overlapping on the right by 1px)',
|
||||
@ -263,14 +267,14 @@ describe('Previous node dictated placement', () => {
|
||||
desc: 'Single node (BIG gap)',
|
||||
nodes: [],
|
||||
gap: 1000,
|
||||
pos: new Vec2(1050, 1710),
|
||||
pos: new Vec2(1090, 1710),
|
||||
pan: new Vec2(0, 1020),
|
||||
},
|
||||
{
|
||||
desc: 'Single node (BIG gap, overlapping on the left by 1px)',
|
||||
nodes: [rectAt(927, 1710)],
|
||||
nodes: [rectAt(967, 1710)],
|
||||
gap: 1000,
|
||||
pos: new Vec2(2027, 1710),
|
||||
pos: new Vec2(2067, 1710),
|
||||
pan: new Vec2(977, 1020),
|
||||
},
|
||||
|
||||
@ -279,13 +283,13 @@ describe('Previous node dictated placement', () => {
|
||||
desc: 'Multiple nodes',
|
||||
nodes: map(range(1000, 2001, 100), rectAtY(734)),
|
||||
pos: new Vec2(2124, 734),
|
||||
pan: new Vec2(1074, 44),
|
||||
pan: new Vec2(1034, 44),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes (reverse)',
|
||||
nodes: map(range(2000, 999, -100), rectAtY(734)),
|
||||
pos: new Vec2(2124, 734),
|
||||
pan: new Vec2(1074, 44),
|
||||
pan: new Vec2(1034, 44),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with gap',
|
||||
@ -328,7 +332,7 @@ describe('Previous node dictated placement', () => {
|
||||
map(range(1647, 1948, 100), rectAtY(734)),
|
||||
),
|
||||
pos: new Vec2(2071, 734),
|
||||
pan: new Vec2(1021, 44),
|
||||
pan: new Vec2(981, 44),
|
||||
},
|
||||
{
|
||||
desc: 'Multiple nodes with gap that is too small (each range reversed)',
|
||||
@ -337,14 +341,14 @@ describe('Previous node dictated placement', () => {
|
||||
map(range(1947, 1646, -100), rectAtY(734)),
|
||||
),
|
||||
pos: new Vec2(2071, 734),
|
||||
pan: new Vec2(1021, 44),
|
||||
pan: new Vec2(981, 44),
|
||||
},
|
||||
])('$desc', ({ nodes, gap, pos, pan }) => {
|
||||
expect(
|
||||
previousNodeDictatedPlacement(
|
||||
nodeSize,
|
||||
previousNodeDictatedEnvironment([...nodes, rectAt(1050, 690)]),
|
||||
gap != null ? { gap } : {},
|
||||
previousNodeDictatedEnvironment([...nodes, rectAt(1090, 690)]),
|
||||
{ horizontalGap: gap ?? 24, verticalGap: gap ?? 24 },
|
||||
),
|
||||
).toEqual({ position: pos, pan })
|
||||
expect(getMousePosition, 'Should not depend on `mousePosition`').not.toHaveBeenCalled()
|
||||
@ -422,8 +426,11 @@ describe('Mouse dictated placement', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
get gap() {
|
||||
return getGap()
|
||||
get horizontalGap() {
|
||||
return getHorizontalGap()
|
||||
},
|
||||
get verticalGap() {
|
||||
return getVerticalGap()
|
||||
},
|
||||
},
|
||||
),
|
||||
@ -435,7 +442,8 @@ describe('Mouse dictated placement', () => {
|
||||
expect(getScreenBounds, 'Should not depend on `screenBounds`').not.toHaveBeenCalled()
|
||||
expect(getNodeRects, 'Should not depend on `nodeRects`').not.toHaveBeenCalled()
|
||||
expect(getSelectedNodeRects, 'Should not depend on `selectedNodeRects`').not.toHaveBeenCalled()
|
||||
expect(getGap, 'Should not depend on `gap`').not.toHaveBeenCalled()
|
||||
expect(getHorizontalGap, 'Should not depend on `horizontalGap`').not.toHaveBeenCalled()
|
||||
expect(getVerticalGap, 'Should not depend on `verticalGap`').not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { bail } from '@/util/assert'
|
||||
import { Rect } from '@/util/rect'
|
||||
import theme from '@/util/theme.json'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
|
||||
export interface Environment {
|
||||
@ -10,7 +11,8 @@ export interface Environment {
|
||||
}
|
||||
|
||||
export interface PlacementOptions {
|
||||
gap?: number
|
||||
horizontalGap?: number
|
||||
verticalGap?: number
|
||||
}
|
||||
|
||||
export interface Placement {
|
||||
@ -18,9 +20,6 @@ export interface Placement {
|
||||
pan?: Vec2
|
||||
}
|
||||
|
||||
// The default gap is the height of a single node.
|
||||
const defaultGap = 24
|
||||
|
||||
/** The new node should appear at the center of the screen if there is enough space for the new node.
|
||||
* Otherwise, it should be moved down to the closest free space.
|
||||
*
|
||||
@ -35,18 +34,18 @@ const defaultGap = 24
|
||||
export function nonDictatedPlacement(
|
||||
nodeSize: Vec2,
|
||||
{ screenBounds, nodeRects }: Environment,
|
||||
{ gap = defaultGap }: PlacementOptions = {},
|
||||
{ verticalGap = theme.node.vertical_gap }: PlacementOptions = {},
|
||||
): Placement {
|
||||
const initialPosition = screenBounds.center().sub(nodeSize.scale(0.5))
|
||||
const initialPosition = screenBounds.center().sub(new Vec2(nodeSize.y / 2, nodeSize.y / 2))
|
||||
const initialRect = new Rect(initialPosition, nodeSize)
|
||||
let top = initialPosition.y
|
||||
const height = nodeSize.y
|
||||
const bottom = () => top + height
|
||||
const nodeRectsSorted = Array.from(nodeRects).sort((a, b) => a.top - b.top)
|
||||
for (const rect of nodeRectsSorted) {
|
||||
if (initialRect.intersectsX(rect) && rect.bottom + gap > top) {
|
||||
if (rect.top - bottom() < gap) {
|
||||
top = rect.bottom + gap
|
||||
if (initialRect.intersectsX(rect) && rect.bottom + verticalGap > top) {
|
||||
if (rect.top - bottom() < verticalGap) {
|
||||
top = rect.bottom + verticalGap
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,7 +62,7 @@ export function nonDictatedPlacement(
|
||||
* In case the place is offscreen, the camera should be panned accordingly.
|
||||
*
|
||||
* Specifically, this code, in order:
|
||||
* - uses the left side of the first selected node and as the initial x-position
|
||||
* - uses the left side of the first selected node as the initial x-position
|
||||
* - uses the lowest (highest y-position) of all selected nodes, plus the specified gap,
|
||||
* as the initial y-position
|
||||
* - searches for all horizontal spans to the right of the initial position,
|
||||
@ -79,13 +78,16 @@ export function nonDictatedPlacement(
|
||||
export function previousNodeDictatedPlacement(
|
||||
nodeSize: Vec2,
|
||||
{ screenBounds, selectedNodeRects, nodeRects }: Environment,
|
||||
{ gap = defaultGap }: PlacementOptions = {},
|
||||
{
|
||||
horizontalGap = theme.node.horizontal_gap,
|
||||
verticalGap = theme.node.vertical_gap,
|
||||
}: PlacementOptions = {},
|
||||
): Placement {
|
||||
let initialLeft: number | undefined
|
||||
let top = -Infinity
|
||||
for (const rect of selectedNodeRects) {
|
||||
initialLeft ??= rect.left
|
||||
const newTop = rect.bottom + gap
|
||||
const newTop = rect.bottom + verticalGap
|
||||
if (newTop > top) top = newTop
|
||||
}
|
||||
if (initialLeft == null)
|
||||
@ -97,16 +99,16 @@ export function previousNodeDictatedPlacement(
|
||||
const initialRect = new Rect(initialPosition, nodeSize)
|
||||
const sortedNodeRects = Array.from(nodeRects).sort((a, b) => a.left - b.left)
|
||||
for (const rect of sortedNodeRects) {
|
||||
if (initialRect.intersectsY(rect) && rect.right + gap > left) {
|
||||
if (rect.left - right() < gap) {
|
||||
left = rect.right + gap
|
||||
if (initialRect.intersectsY(rect) && rect.right + horizontalGap > left) {
|
||||
if (rect.left - right() < horizontalGap) {
|
||||
left = rect.right + horizontalGap
|
||||
}
|
||||
}
|
||||
}
|
||||
const finalPosition = new Vec2(left, top)
|
||||
if (new Rect(finalPosition, nodeSize).within(screenBounds)) return { position: finalPosition }
|
||||
else {
|
||||
const screenCenter = screenBounds.center().sub(nodeSize.scale(0.5))
|
||||
const screenCenter = screenBounds.center().sub(new Vec2(nodeSize.y / 2, nodeSize.y / 2))
|
||||
return { position: finalPosition, pan: finalPosition.sub(screenCenter) }
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,8 @@ function targetComponentBrowserPosition() {
|
||||
} else if (hasNodeSelected) {
|
||||
const gapBetweenNodes = 48.0
|
||||
return previousNodeDictatedPlacement(nodeSize, placementEnvironment.value, {
|
||||
gap: gapBetweenNodes,
|
||||
horizontalGap: gapBetweenNodes,
|
||||
verticalGap: gapBetweenNodes,
|
||||
}).position
|
||||
} else {
|
||||
return mouseDictatedPlacement(nodeSize, placementEnvironment.value).position
|
||||
|
@ -4,6 +4,7 @@ import { injectGraphSelection } from '@/providers/graphSelection.ts'
|
||||
import { useGraphStore, type Edge } from '@/stores/graph'
|
||||
import { assert } from '@/util/assert'
|
||||
import { Rect } from '@/util/rect'
|
||||
import theme from '@/util/theme.json'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
import { clamp } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
@ -94,8 +95,6 @@ type JunctionPoints = {
|
||||
|
||||
/** Minimum height above the target the edge must approach it from. */
|
||||
const MIN_APPROACH_HEIGHT = 32
|
||||
const NODE_HEIGHT = 32 // TODO (crate::component::node::HEIGHT)
|
||||
const NODE_CORNER_RADIUS = 16 // TODO (crate::component::node::CORNER_RADIUS)
|
||||
/** The preferred arc radius. */
|
||||
const RADIUS_BASE = 20
|
||||
|
||||
@ -173,7 +172,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
let halfSourceSize = inputs.sourceSize?.scale(0.5) ?? Vec2.Zero
|
||||
// The maximum x-distance from the source (our local coordinate origin) for the point where the
|
||||
// edge will begin.
|
||||
const sourceMaxXOffset = Math.max(halfSourceSize.x - NODE_CORNER_RADIUS, 0)
|
||||
const sourceMaxXOffset = Math.max(halfSourceSize.x - theme.node.corner_radius, 0)
|
||||
const attachment =
|
||||
inputs.targetPortTopDistanceInNode != null
|
||||
? {
|
||||
@ -184,7 +183,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
|
||||
const targetWellBelowSource =
|
||||
inputs.targetOffset.y - (inputs.targetPortTopDistanceInNode ?? 0) >= MIN_APPROACH_HEIGHT
|
||||
const targetBelowSource = inputs.targetOffset.y > NODE_HEIGHT / 2.0
|
||||
const targetBelowSource = inputs.targetOffset.y > theme.node.height / 2.0
|
||||
const targetBeyondSource = Math.abs(inputs.targetOffset.x) > sourceMaxXOffset
|
||||
const horizontalRoomFor3Corners =
|
||||
targetBeyondSource &&
|
||||
@ -222,9 +221,9 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
// at the point that it exits the node.
|
||||
const radius = Math.min(naturalRadius, maxRadius)
|
||||
const arcOriginX = Math.abs(inputs.targetOffset.x) - radius
|
||||
const sourceArcOrigin = halfSourceSize.x - NODE_CORNER_RADIUS
|
||||
const sourceArcOrigin = halfSourceSize.x - theme.node.corner_radius
|
||||
const circleOffset = arcOriginX - sourceArcOrigin
|
||||
const intersection = circleIntersection(circleOffset, NODE_CORNER_RADIUS, radius)
|
||||
const intersection = circleIntersection(circleOffset, theme.node.corner_radius, radius)
|
||||
sourceDY = -Math.abs(radius - intersection)
|
||||
} else if (halfSourceSize.y != 0) {
|
||||
sourceDY = -SOURCE_NODE_OVERLAP + halfSourceSize.y
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { nonDictatedPlacement } from '@/components/ComponentBrowser/placement'
|
||||
import { SuggestionDb, groupColorStyle, type Group } from '@/stores/suggestionDatabase'
|
||||
import { tryGetIndex } from '@/util/array'
|
||||
import { Ast, AstExtended } from '@/util/ast'
|
||||
import { colorFromString } from '@/util/colors'
|
||||
import { ComputedValueRegistry, type ExpressionInfo } from '@/util/computedValueRegistry'
|
||||
import { ReactiveDb, ReactiveIndex, ReactiveMapping } from '@/util/database/reactiveDb'
|
||||
import { getTextWidth } from '@/util/measurement'
|
||||
import type { Opt } from '@/util/opt'
|
||||
import { qnJoin, tryQualifiedName } from '@/util/qualifiedName'
|
||||
import { Rect } from '@/util/rect'
|
||||
import theme from '@/util/theme.json'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
import * as set from 'lib0/set'
|
||||
import {
|
||||
@ -103,11 +107,20 @@ export class GraphDb {
|
||||
this.nodes.moveToLast(id)
|
||||
}
|
||||
|
||||
getNodeWidth(node: Node) {
|
||||
// FIXME [sb]: This should take into account the width of all widgets.
|
||||
// This will require a recursive traversal of the `Node`'s children.
|
||||
return getTextWidth(node.rootSpan.repr(), '11.5px', '"M PLUS 1", sans-serif') * 1.2
|
||||
}
|
||||
|
||||
readFunctionAst(
|
||||
functionAst: AstExtended<Ast.Tree.Function>,
|
||||
getMeta: (id: ExprId) => NodeMetadata | undefined,
|
||||
) {
|
||||
const currentNodeIds = new Set<ExprId>()
|
||||
const nodeRectMap = new Map<ExprId, Rect>()
|
||||
let numberOfUnpositionedNodes = 0
|
||||
let maxUnpositionedNodeWidth = 0
|
||||
if (functionAst) {
|
||||
for (const nodeAst of functionAst.visit(getFunctionNodeExpressions)) {
|
||||
const newNode = nodeFromAst(nodeAst)
|
||||
@ -117,7 +130,6 @@ export class GraphDb {
|
||||
currentNodeIds.add(nodeId)
|
||||
if (node == null) {
|
||||
this.nodes.set(nodeId, newNode)
|
||||
if (nodeMeta) this.assignUpdatedMetadata(newNode, nodeMeta)
|
||||
} else {
|
||||
if (node.binding !== newNode.binding) {
|
||||
node.binding = newNode.binding
|
||||
@ -128,7 +140,24 @@ export class GraphDb {
|
||||
if (indexedDB.cmp(node.rootSpan.contentHash(), newNode.rootSpan.contentHash()) !== 0) {
|
||||
node.rootSpan = newNode.rootSpan
|
||||
}
|
||||
if (nodeMeta) this.assignUpdatedMetadata(node, nodeMeta)
|
||||
}
|
||||
if (!nodeMeta) {
|
||||
numberOfUnpositionedNodes += 1
|
||||
maxUnpositionedNodeWidth = Math.max(
|
||||
maxUnpositionedNodeWidth,
|
||||
this.getNodeWidth(node ?? newNode),
|
||||
)
|
||||
} else {
|
||||
this.assignUpdatedMetadata(node ?? newNode, nodeMeta)
|
||||
nodeRectMap.set(
|
||||
nodeId,
|
||||
Rect.FromBounds(
|
||||
nodeMeta.x,
|
||||
nodeMeta.y,
|
||||
nodeMeta.x + this.getNodeWidth(node ?? newNode),
|
||||
nodeMeta.y + theme.node.height,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,6 +167,36 @@ export class GraphDb {
|
||||
this.nodes.delete(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
const nodeRects = [...nodeRectMap.values()]
|
||||
const rectsHeight =
|
||||
numberOfUnpositionedNodes * (theme.node.height + theme.node.vertical_gap) -
|
||||
theme.node.vertical_gap
|
||||
const { position: rectsPosition } = nonDictatedPlacement(
|
||||
new Vec2(maxUnpositionedNodeWidth, rectsHeight),
|
||||
{
|
||||
nodeRects,
|
||||
// The rest of the properties should not matter.
|
||||
selectedNodeRects: [],
|
||||
screenBounds: Rect.Zero,
|
||||
mousePosition: Vec2.Zero,
|
||||
},
|
||||
)
|
||||
let nodeIndex = 0
|
||||
for (const nodeId of this.allNodeIds()) {
|
||||
const meta = getMeta(nodeId)
|
||||
if (meta) continue
|
||||
const node = this.nodes.get(nodeId)!
|
||||
const size = new Vec2(this.getNodeWidth(node), theme.node.height)
|
||||
const position = new Vec2(
|
||||
rectsPosition.x,
|
||||
rectsPosition.y + (theme.node.height + theme.node.vertical_gap) * nodeIndex,
|
||||
)
|
||||
nodeRects.push(new Rect(position, size))
|
||||
node.position = new Vec2(position.x, position.y)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
assignUpdatedMetadata(node: Node, meta: NodeMetadata) {
|
||||
|
8
app/gui2/src/util/theme.json
Normal file
8
app/gui2/src/util/theme.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"node": {
|
||||
"height": 32,
|
||||
"corner_radius": 16,
|
||||
"vertical_gap": 32,
|
||||
"horizontal_gap": 32
|
||||
}
|
||||
}
|
@ -1,6 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "shared/**/*", "shared/**/*.vue"],
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"shared/**/*",
|
||||
"shared/**/*.vue",
|
||||
"src/util/theme.json"
|
||||
],
|
||||
"exclude": ["src/**/__tests__/*", "shared/**/__tests__/*", "public/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
|
Loading…
Reference in New Issue
Block a user