Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Self-argument arrows: New edge rendering mode #9099

Merged
merged 3 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/gui2/e2e/edgeInteractions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, Page, test } from '@playwright/test'
import { expect, test, type Page } from '@playwright/test'
import * as actions from './actions'
import { edgesToNodeWithBinding, graphNodeByBinding } from './locate'

Expand All @@ -22,7 +22,7 @@ test('Disconnect an edge from a port', async ({ page }) => {
await initGraph(page)
await expect(await edgesToNodeWithBinding(page, 'sum')).toHaveCount(2 * EDGE_PARTS)

const targetEdge = page.locator('svg.behindNodes g:nth-child(2) path.visible')
const targetEdge = page.locator('svg.behindNodes g:nth-child(2) path.edge.visible')

// Hover over edge to the right of node with binding `ten`.
await targetEdge.click({
Expand All @@ -41,7 +41,7 @@ test('Connect an node to a port via dragging the edge', async ({ page }) => {
await initGraph(page)

await expect(await edgesToNodeWithBinding(page, 'sum')).toHaveCount(2 * EDGE_PARTS)
const targetEdge = page.locator('svg.behindNodes g:nth-child(2) path.visible')
const targetEdge = page.locator('svg.behindNodes g:nth-child(2) path.edge.visible')
// Hover over edge to the left of node with binding `ten`.
await targetEdge.click({
position: { x: 450, y: 5.0 },
Expand Down
153 changes: 85 additions & 68 deletions app/gui2/src/components/GraphEditor/GraphEdge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,17 @@ const targetNode = computed(
)
const targetNodeRect = computed(() => targetNode.value && graph.nodeRects.get(targetNode.value))

const targetRect = computed<Rect | undefined>(() => {
const targetPos = computed<Vec2 | undefined>(() => {
const expr = targetExpr.value
if (expr != null && targetNode.value != null && targetNodeRect.value != null) {
const targetRectRelative = graph.getPortRelativeRect(expr)
if (targetRectRelative == null) return
return targetRectRelative.offsetBy(targetNodeRect.value.pos)
const yAdjustment = targetIsSelfArgument.value
? -(selfArgumentArrowHeight + selfArgumentArrowYOffset)
: 0
return targetNodeRect.value.pos.add(new Vec2(targetRectRelative.center().x, yAdjustment))
} else if (navigator?.sceneMousePos != null) {
return new Rect(navigator.sceneMousePos, Vec2.Zero)
return navigator.sceneMousePos
} else {
return undefined
}
Expand Down Expand Up @@ -102,14 +105,9 @@ interface Inputs {
/** The width and height of the node that originates the edge, if any.
* The edge may begin anywhere around the bottom half of the node. */
sourceSize: Vec2
/** The width and height of the port that the edge is attached to, if any. */
targetSize: Vec2
/** The coordinates of the node input port that is the edge's destination, relative to the source
* position. The edge enters the port from above. */
targetOffset: Vec2
/** The distance between the target port top edge and the target node top edge. It is undefined
* when there is no clear target node set, e.g. when the edge is being dragged. */
targetPortTopDistanceInNode: number | undefined
}

interface JunctionPoints {
Expand Down Expand Up @@ -167,18 +165,9 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
// 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 - theme.node.corner_radius, 0)
const attachment =
inputs.targetPortTopDistanceInNode != null
? {
target: inputs.targetOffset.add(new Vec2(0, inputs.targetSize.y * -0.5)),
length: inputs.targetPortTopDistanceInNode,
}
: undefined

const targetWellBelowSource =
inputs.targetOffset.y - (inputs.targetPortTopDistanceInNode ?? 0) >=
theme.edge.min_approach_height
const targetBelowSource = inputs.targetOffset.y > theme.node.height / 2.0
const attachmentTarget = inputs.targetOffset
const targetWellBelowSource = inputs.targetOffset.y >= theme.edge.min_approach_height
const targetBelowSource = inputs.targetOffset.y > 0
const targetBeyondSource = Math.abs(inputs.targetOffset.x) > sourceMaxXOffset
const horizontalRoomFor3Corners =
targetBeyondSource &&
Expand Down Expand Up @@ -218,16 +207,8 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
sourceDY = -innerTheme.source_node_overlap + halfSourceSize.y
}
const source = new Vec2(sourceX, sourceDY)
// The target attachment will extend as far toward the edge of the node as it can without
// rising above the source.
let attachmentHeight =
inputs.targetPortTopDistanceInNode != null
? Math.min(inputs.targetPortTopDistanceInNode, Math.abs(inputs.targetOffset.y))
: 0
let attachmentY = inputs.targetOffset.y - attachmentHeight - inputs.targetSize.y / 2.0
let targetAttachment = new Vec2(inputs.targetOffset.x, attachmentY)
return {
points: [source, targetAttachment],
points: [source, inputs.targetOffset],
maxRadius,
}
} else {
Expand Down Expand Up @@ -267,16 +248,13 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
heightAdjustment = 0
}
if (j0x == null || j1x == null || heightAdjustment == null) return null
const attachmentHeight = inputs.targetPortTopDistanceInNode ?? 0
const top = Math.min(
inputs.targetOffset.y - theme.edge.min_approach_height - attachmentHeight + heightAdjustment,
inputs.targetOffset.y - theme.edge.min_approach_height + heightAdjustment,
0,
)
const source = new Vec2(sourceX, 0)
const j0 = new Vec2(j0x, top / 2)
const j1 = new Vec2(j1x, top)
// The corners meet the target attachment at the top of the node.
const attachmentTarget = attachment?.target ?? inputs.targetOffset
return {
points: [source, j0, j1, attachmentTarget],
maxRadius: radiusMax,
Expand Down Expand Up @@ -347,15 +325,12 @@ function render(sourcePos: Vec2, elements: Element[]): string {
}

const currentJunctionPoints = computed(() => {
const target = targetRect.value
const targetNode = targetNodeRect.value
const target = targetPos.value
const source = sourceRect.value
if (target == null || source == null) return null
const inputs: Inputs = {
targetOffset: target.center().sub(source.center()),
sourceSize: source.size,
targetSize: target.size,
targetPortTopDistanceInNode: targetNode != null ? target.top - targetNode.top : undefined,
targetOffset: target.sub(source.center()),
}
return junctionPoints(inputs)
})
Expand All @@ -375,9 +350,7 @@ const activePath = computed(() => {
else return undefined
})

function lengthTo(pos: Vec2): number | undefined {
const path = base.value
if (path == null) return undefined
function lengthTo(path: SVGPathElement, pos: Vec2): number {
const totalLength = path.getTotalLength()
let best: number | undefined
let bestDist: number | undefined
Expand All @@ -400,56 +373,89 @@ function lengthTo(pos: Vec2): number | undefined {
return best
}

const mouseLocationOnEdge = computed(() => {
if (navigator?.sceneMousePos == null) return
if (base.value == null) return
const sourceToMouse = lengthTo(base.value, navigator.sceneMousePos)
const sourceToTarget = base.value.getTotalLength()
const mouseToTarget = sourceToTarget - sourceToMouse
return { sourceToMouse, sourceToTarget, mouseToTarget }
})

const hovered = ref(false)
const activeStyle = computed(() => {
if (!hovered.value) return {}
if (props.edge.source == null || props.edge.target == null) return {}
if (base.value == null) return {}
if (navigator?.sceneMousePos == null) return {}
const length = base.value.getTotalLength()
let offset = lengthTo(navigator.sceneMousePos)
if (offset == null) return {}
offset = length - offset
if (offset < length / 2) {
offset += length
}
const distances = mouseLocationOnEdge.value
if (distances == null) return {}
const offset =
distances.sourceToMouse < distances.mouseToTarget
? distances.mouseToTarget
: -distances.sourceToMouse
return {
...baseStyle.value,
strokeDasharray: length,
strokeDasharray: distances.sourceToTarget,
strokeDashoffset: offset,
}
})

const targetEndIsDimmed = computed(() => {
if (!hovered.value) return false
const distances = mouseLocationOnEdge.value
if (!distances) return false
return distances.sourceToMouse < distances.mouseToTarget
})

const baseStyle = computed(() => ({ '--node-base-color': edgeColor.value ?? 'tan' }))

function click() {
if (base.value == null) return
if (navigator?.sceneMousePos == null) return
const length = base.value.getTotalLength()
let offset = lengthTo(navigator?.sceneMousePos)
if (offset == null) return
if (offset < length / 2) graph.disconnectTarget(props.edge)
const distances = mouseLocationOnEdge.value
if (distances == null) return
if (distances.sourceToMouse < distances.mouseToTarget) graph.disconnectTarget(props.edge)
else graph.disconnectSource(props.edge)
}

function arrowPosition(): Vec2 | undefined {
function svgTranslate(offset: Vec2): string {
return `translate(${offset.x},${offset.y})`
}

const backwardEdgeArrowTransform = computed<string | undefined>(() => {
if (props.edge.source == null || props.edge.target == null) return
const points = currentJunctionPoints.value?.points
if (points == null || points.length < 3) return
const target = targetRect.value
const target = targetPos.value
const source = sourceRect.value
if (target == null || source == null) return
if (target.pos.y > source.pos.y - theme.edge.three_corner.backward_edge_arrow_threshold) return
if (target.y > source.pos.y - theme.edge.three_corner.backward_edge_arrow_threshold) return
if (points[1] == null) return
return source.center().add(points[1])
}
return svgTranslate(source.center().add(points[1]))
})

const arrowTransform = computed(() => {
const pos = arrowPosition()
if (pos != null) return `translate(${pos.x},${pos.y})`
else return undefined
const targetIsSelfArgument = computed(() => false)

const selfArgumentArrowHeight = 9
const selfArgumentArrowYOffset = 0
const selfArgumentArrowTransform = computed<string | undefined>(() => {
const selfArgumentArrowTopOffset = 4
const selfArgumentArrowWidth = 12
if (!targetIsSelfArgument.value) return
const target = targetPos.value
if (target == null) return
const pos = target.sub(new Vec2(selfArgumentArrowWidth / 2, selfArgumentArrowTopOffset))
return svgTranslate(pos)
})

const selfArgumentArrowPath = [
'M10.9635 1.5547',
'L6.83205 7.75193',
'C6.43623 8.34566 5.56377 8.34566 5.16795 7.75192',
'L1.03647 1.5547',
'C0.593431 0.890146 1.06982 0 1.86852 0',
'L10.1315 0',
'C10.9302 0 11.4066 0.890147 10.9635 1.5547',
'Z',
].join('')

const connected = computed(() => isConnected(props.edge))
</script>

Expand Down Expand Up @@ -509,14 +515,21 @@ const connected = computed(() => isConnected(props.edge))
:data-target-node-id="targetNode"
/>
<polygon
v-if="arrowTransform"
:transform="arrowTransform"
v-if="backwardEdgeArrowTransform"
:transform="backwardEdgeArrowTransform"
points="0,-9.375 -9.375,9.375 9.375,9.375"
class="arrow visible"
:style="baseStyle"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
/>
<path
v-if="selfArgumentArrowTransform"
:transform="selfArgumentArrowTransform"
:d="selfArgumentArrowPath"
:class="{ arrow: true, visible: true, dimmed: targetEndIsDimmed }"
:style="baseStyle"
/>
</g>
</template>
</template>
Expand Down Expand Up @@ -551,4 +564,8 @@ const connected = computed(() => isConnected(props.edge))
/* stroke: rgba(255, 255, 255, 0.4); */
stroke: color-mix(in oklab, var(--edge-color) 60%, white 40%);
}

.arrow.visible.dimmed {
fill: color-mix(in oklab, var(--edge-color) 60%, white 40%);
}
</style>
Loading