mirror of
https://github.com/enso-org/enso.git
synced 2024-12-21 21:21:32 +03:00
Color edges according to source node. (#11810)
Fixes #11536 <img width="238" alt="image" src="https://github.com/user-attachments/assets/e253b188-a6ce-468f-864c-9e814a8a2584"> Also fixed incorrect edge offset when dragging an target-connected edge. <img width="140" alt="image" src="https://github.com/user-attachments/assets/d75ac860-c614-4f40-9cbc-b955e94f5a58"> (cursor painted in, because it wasn't captured by the screenshot 😅) # Important Notes Split away edge layout code away from the component to separate file. That code is likely to be significantly changed soon, but no significant modifications were made right now.
This commit is contained in:
parent
3e23e72bed
commit
7e47a65b72
@ -16,6 +16,7 @@ distribution/lib/Standard/*/*/manifest.yaml
|
|||||||
distribution/lib/Standard/*/*/polyglot
|
distribution/lib/Standard/*/*/polyglot
|
||||||
distribution/lib/Standard/*/*/THIRD-PARTY
|
distribution/lib/Standard/*/*/THIRD-PARTY
|
||||||
distribution/docs-js
|
distribution/docs-js
|
||||||
|
docs/**/*.md
|
||||||
|
|
||||||
built-distribution/
|
built-distribution/
|
||||||
THIRD-PARTY
|
THIRD-PARTY
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
be opened on OS X][11755].
|
be opened on OS X][11755].
|
||||||
- [Fix some UI elements drawing on top of visualization toolbar dropdown
|
- [Fix some UI elements drawing on top of visualization toolbar dropdown
|
||||||
menus][11768].
|
menus][11768].
|
||||||
|
- [Edges are now colored based on their source component.][11810]
|
||||||
- [Highlight missing required arguments][11803].
|
- [Highlight missing required arguments][11803].
|
||||||
- [Arrows in some drop-down buttons are now clearly visible][11800]
|
- [Arrows in some drop-down buttons are now clearly visible][11800]
|
||||||
|
|
||||||
@ -90,6 +91,7 @@
|
|||||||
[11753]: https://github.com/enso-org/enso/pull/11753
|
[11753]: https://github.com/enso-org/enso/pull/11753
|
||||||
[11761]: https://github.com/enso-org/enso/pull/11761
|
[11761]: https://github.com/enso-org/enso/pull/11761
|
||||||
[11768]: https://github.com/enso-org/enso/pull/11768
|
[11768]: https://github.com/enso-org/enso/pull/11768
|
||||||
|
[11810]: https://github.com/enso-org/enso/pull/11810
|
||||||
[11803]: https://github.com/enso-org/enso/pull/11803
|
[11803]: https://github.com/enso-org/enso/pull/11803
|
||||||
[11800]: https://github.com/enso-org/enso/pull/11800
|
[11800]: https://github.com/enso-org/enso/pull/11800
|
||||||
|
|
||||||
|
@ -240,10 +240,6 @@ const nodeColor = computed(() => {
|
|||||||
}
|
}
|
||||||
return 'var(--node-color-no-type)'
|
return 'var(--node-color-no-type)'
|
||||||
})
|
})
|
||||||
watchEffect(() => {
|
|
||||||
if (!graphStore.cbEditedEdge) return
|
|
||||||
graphStore.cbEditedEdge.color = nodeColor.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedSuggestionIcon = computed(() => {
|
const selectedSuggestionIcon = computed(() => {
|
||||||
return selectedSuggestion.value ? suggestionEntryToIcon(selectedSuggestion.value) : undefined
|
return selectedSuggestion.value ? suggestionEntryToIcon(selectedSuggestion.value) : undefined
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { junctionPoints, pathElements, toSvgPath } from '@/components/GraphEditor/GraphEdge/layout'
|
||||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||||
import { injectGraphSelection } from '@/providers/graphSelection'
|
import { injectGraphSelection } from '@/providers/graphSelection'
|
||||||
import type { Edge } from '@/stores/graph'
|
import type { Edge } from '@/stores/graph'
|
||||||
@ -7,14 +8,13 @@ import { assert } from '@/util/assert'
|
|||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import theme from '@/util/theme'
|
import theme from '@/util/theme'
|
||||||
import { clamp } from '@vueuse/core'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const selection = injectGraphSelection(true)
|
const selection = injectGraphSelection(true)
|
||||||
const navigator = injectGraphNavigator(true)
|
const navigator = injectGraphNavigator(true)
|
||||||
const graph = useGraphStore()
|
const graph = useGraphStore()
|
||||||
|
|
||||||
const props = defineProps<{
|
const { edge, maskSource, animateFromSourceHover } = defineProps<{
|
||||||
edge: Edge
|
edge: Edge
|
||||||
maskSource?: boolean
|
maskSource?: boolean
|
||||||
animateFromSourceHover?: boolean
|
animateFromSourceHover?: boolean
|
||||||
@ -27,32 +27,30 @@ const VISIBLE_PORT_MASK_PADDING = 6
|
|||||||
|
|
||||||
const base = ref<SVGPathElement>()
|
const base = ref<SVGPathElement>()
|
||||||
|
|
||||||
const mouseAnchor = computed(() => 'anchor' in props.edge && props.edge.anchor.type === 'mouse')
|
const mouseAnchor = computed(() => 'anchor' in edge && edge.anchor.type === 'mouse')
|
||||||
const mouseAnchorPos = computed(() => (mouseAnchor.value ? navigator?.sceneMousePos : undefined))
|
const mouseAnchorPos = computed(() => (mouseAnchor.value ? navigator?.sceneMousePos : undefined))
|
||||||
const hoveredNode = computed(() => (mouseAnchor.value ? selection?.hoveredNode : undefined))
|
const hoveredNode = computed(() => (mouseAnchor.value ? selection?.hoveredNode : undefined))
|
||||||
const hoveredPort = computed(() => (mouseAnchor.value ? selection?.hoveredPort : undefined))
|
const hoveredPort = computed(() => (mouseAnchor.value ? selection?.hoveredPort : undefined))
|
||||||
|
|
||||||
const isSuggestion = computed(() => 'suggestion' in props.edge && props.edge.suggestion)
|
const isSuggestion = computed(() => 'suggestion' in edge && edge.suggestion)
|
||||||
|
|
||||||
const connectedSourceNode = computed(
|
const connectedSourceNode = computed(() => edge.source && graph.getSourceNodeId(edge.source))
|
||||||
() => props.edge.source && graph.getSourceNodeId(props.edge.source),
|
|
||||||
)
|
|
||||||
|
|
||||||
const sourceNode = computed(() => {
|
const sourceNode = computed(() => {
|
||||||
if (connectedSourceNode.value) {
|
if (connectedSourceNode.value) {
|
||||||
return connectedSourceNode.value
|
return connectedSourceNode.value
|
||||||
} else if (hoveredNode.value != null && props.edge.target) {
|
} else if (hoveredNode.value != null && edge.target) {
|
||||||
// When the source is not set (i.e. edge is dragged), use the currently hovered over expression
|
// When the source is not set (i.e. edge is dragged), use the currently hovered over expression
|
||||||
// as the source, as long as it is an output node or the same node as the target.
|
// as the source, as long as it is an output node or the same node as the target.
|
||||||
const nodeType = graph.db.nodeIdToNode.get(hoveredNode.value)?.type
|
const nodeType = graph.db.nodeIdToNode.get(hoveredNode.value)?.type
|
||||||
const rawTargetNode = graph.getPortNodeId(props.edge.target)
|
const rawTargetNode = graph.getPortNodeId(edge.target)
|
||||||
if (nodeType !== 'output' && hoveredNode.value != rawTargetNode) return hoveredNode.value
|
if (nodeType !== 'output' && hoveredNode.value != rawTargetNode) return hoveredNode.value
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const targetExpr = computed(() => {
|
const targetExpr = computed(() => {
|
||||||
const setTarget = props.edge.target
|
const setTarget = edge.target
|
||||||
if (setTarget) {
|
if (setTarget) {
|
||||||
return setTarget
|
return setTarget
|
||||||
} else if (hoveredNode.value != null && hoveredNode.value !== connectedSourceNode.value) {
|
} else if (hoveredNode.value != null && hoveredNode.value !== connectedSourceNode.value) {
|
||||||
@ -77,8 +75,8 @@ const targetPos = computed<Vec2 | undefined>(() => {
|
|||||||
return targetNodeRect.value.pos.add(new Vec2(targetRectRelative.center().x, yAdjustment))
|
return targetNodeRect.value.pos.add(new Vec2(targetRectRelative.center().x, yAdjustment))
|
||||||
} else if (mouseAnchorPos.value != null) {
|
} else if (mouseAnchorPos.value != null) {
|
||||||
return mouseAnchorPos.value
|
return mouseAnchorPos.value
|
||||||
} else if ('anchor' in props.edge && props.edge.anchor.type === 'fixed') {
|
} else if ('anchor' in edge && edge.anchor.type === 'fixed') {
|
||||||
return props.edge.anchor.scenePos
|
return edge.anchor.scenePos
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@ -92,9 +90,9 @@ const sourceRect = computed<Rect | undefined>(() => {
|
|||||||
if (sourceNodeRect.value) {
|
if (sourceNodeRect.value) {
|
||||||
return sourceNodeRect.value
|
return sourceNodeRect.value
|
||||||
} else if (
|
} else if (
|
||||||
'anchor' in props.edge &&
|
'anchor' in edge &&
|
||||||
props.edge.anchor.type === 'mouse' &&
|
edge.anchor.type === 'mouse' &&
|
||||||
props.edge.target != null &&
|
edge.target != null &&
|
||||||
mouseAnchorPos.value != null
|
mouseAnchorPos.value != null
|
||||||
) {
|
) {
|
||||||
return new Rect(mouseAnchorPos.value, Vec2.Zero)
|
return new Rect(mouseAnchorPos.value, Vec2.Zero)
|
||||||
@ -122,7 +120,7 @@ type NodeMask = {
|
|||||||
|
|
||||||
const startsInPort = computed(() => currentJunctionPoints.value?.startsInPort)
|
const startsInPort = computed(() => currentJunctionPoints.value?.startsInPort)
|
||||||
const sourceMask = computed<NodeMask | undefined>(() => {
|
const sourceMask = computed<NodeMask | undefined>(() => {
|
||||||
if (!props.maskSource && !startsInPort.value) return
|
if (!maskSource && !startsInPort.value) return
|
||||||
const nodeRect = sourceNodeRect.value
|
const nodeRect = sourceNodeRect.value
|
||||||
if (!nodeRect) return
|
if (!nodeRect) return
|
||||||
const animProgress =
|
const animProgress =
|
||||||
@ -130,268 +128,28 @@ const sourceMask = computed<NodeMask | undefined>(() => {
|
|||||||
(sourceNode.value && graph.nodeHoverAnimations.get(sourceNode.value)) ?? 0
|
(sourceNode.value && graph.nodeHoverAnimations.get(sourceNode.value)) ?? 0
|
||||||
: 0
|
: 0
|
||||||
const padding = animProgress * VISIBLE_PORT_MASK_PADDING
|
const padding = animProgress * VISIBLE_PORT_MASK_PADDING
|
||||||
if (!props.maskSource && padding === 0) return
|
if (!maskSource && padding === 0) return
|
||||||
const rect = nodeRect.expand(padding)
|
const rect = nodeRect.expand(padding)
|
||||||
const radius = 16 + padding
|
const radius = 16 + padding
|
||||||
const id = `mask_for_edge_to-${props.edge.target ?? 'unconnected'}`
|
const id = `mask_for_edge_to-${edge.target ?? 'unconnected'}`
|
||||||
return { id, rect, radius }
|
return { id, rect, radius }
|
||||||
})
|
})
|
||||||
|
|
||||||
const edgeColor = computed(() =>
|
const edgeColor = computed(() =>
|
||||||
'color' in props.edge ? props.edge.color
|
'color' in edge ? edge.color
|
||||||
: targetNode.value ? graph.db.getNodeColorStyle(targetNode.value)
|
|
||||||
: sourceNode.value ? graph.db.getNodeColorStyle(sourceNode.value)
|
: sourceNode.value ? graph.db.getNodeColorStyle(sourceNode.value)
|
||||||
: undefined,
|
: undefined,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** The inputs to the edge state computation. */
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JunctionPoints {
|
|
||||||
points: Vec2[]
|
|
||||||
maxRadius: number
|
|
||||||
startsInPort: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function circleIntersection(x: number, r1: number, r2: number): number {
|
|
||||||
const xNorm = clamp(x, -r2, r1)
|
|
||||||
return Math.sqrt(r1 * r1 + r2 * r2 - xNorm * xNorm)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edge layout calculation.
|
|
||||||
*
|
|
||||||
* # Corners
|
|
||||||
*
|
|
||||||
* ```text
|
|
||||||
* ────╮
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The fundamental unit of edge layout is the [`Corner`]. A corner is a line segment attached to a
|
|
||||||
* 90° arc. The length of the straight segment, the radius of the arc, and the orientation of the
|
|
||||||
* shape may vary. Any shape of edge is built from corners.
|
|
||||||
*
|
|
||||||
* The shape of a corner can be fully-specified by two points: The horizontal end, and the vertical
|
|
||||||
* end.
|
|
||||||
*
|
|
||||||
* In special cases, a corner may be *trivial*: It may have a radius of zero, in which case either
|
|
||||||
* the horizontal or vertical end will not be in the usual orientation. The layout algorithm only
|
|
||||||
* produces trivial corners when the source is directly in line with the target, or in some cases
|
|
||||||
* when subdividing a corner (see [Partial edges] below).
|
|
||||||
*
|
|
||||||
* # Junction points
|
|
||||||
*
|
|
||||||
* ```text
|
|
||||||
* 3
|
|
||||||
* 1 /
|
|
||||||
* \ ╭─────╮
|
|
||||||
* ────╯\ \
|
|
||||||
* 2 4
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The layout algorithm doesn't directly place corners. The layout algorithm places a sequence of
|
|
||||||
* junction points--coordinates where two horizontal corner ends or two vertical corner ends meet
|
|
||||||
* (or just one corner end, at an end of an edge). A series of junction points, always alternating
|
|
||||||
* horizontal/vertical, has a one-to-one relationship with a sequence of corners.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the start and end positions of each 1-corner section composing an edge to the
|
|
||||||
* given offset. Return the points and the maximum radius that should be used to draw the corners
|
|
||||||
* connecting them.
|
|
||||||
*/
|
|
||||||
function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
|
||||||
const 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, 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 &&
|
|
||||||
Math.abs(inputs.targetOffset.x) - sourceMaxXOffset >=
|
|
||||||
3.0 * (theme.edge.radius - theme.edge.three_corner.max_squeeze)
|
|
||||||
const horizontalRoomFor3CornersNoSqueeze =
|
|
||||||
targetBeyondSource &&
|
|
||||||
Math.abs(inputs.targetOffset.x) - sourceMaxXOffset >=
|
|
||||||
3.0 * theme.edge.radius + theme.edge.three_corner.radius_max
|
|
||||||
|
|
||||||
if (targetWellBelowSource || (targetBelowSource && !horizontalRoomFor3Corners)) {
|
|
||||||
const innerTheme = theme.edge.one_corner
|
|
||||||
// The edge can originate anywhere along the length of the node.
|
|
||||||
const sourceX = clamp(inputs.targetOffset.x, -sourceMaxXOffset, sourceMaxXOffset)
|
|
||||||
const distanceX = Math.max(Math.abs(inputs.targetOffset.x) - halfSourceSize.x, 0)
|
|
||||||
const radiusX = innerTheme.radius_x_base + distanceX * innerTheme.radius_x_factor
|
|
||||||
// The minimum length of straight line there should be at the target end of the edge. This
|
|
||||||
// is a fixed value, except it is reduced when the target is horizontally very close to the
|
|
||||||
// edge of the source, so that very short edges are less sharp.
|
|
||||||
const yAdjustment = Math.min(
|
|
||||||
Math.abs(inputs.targetOffset.x) - halfSourceSize.x + innerTheme.radius_y_adjustment / 2.0,
|
|
||||||
innerTheme.radius_y_adjustment,
|
|
||||||
)
|
|
||||||
const radiusY = Math.max(Math.abs(inputs.targetOffset.y) - yAdjustment, 0.0)
|
|
||||||
const maxRadius = Math.min(radiusX, radiusY)
|
|
||||||
// The radius the edge would have, if the arc portion were as large as possible.
|
|
||||||
const offsetX = Math.abs(inputs.targetOffset.x - sourceX)
|
|
||||||
const naturalRadius = Math.min(
|
|
||||||
Math.abs(inputs.targetOffset.x - sourceX),
|
|
||||||
Math.abs(inputs.targetOffset.y),
|
|
||||||
)
|
|
||||||
let sourceDY = 0
|
|
||||||
let startsInPort = true
|
|
||||||
if (naturalRadius > innerTheme.minimum_tangent_exit_radius) {
|
|
||||||
// Offset the beginning of the edge so that it is normal to the curve of the source node
|
|
||||||
// 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 - theme.node.corner_radius
|
|
||||||
const circleOffset = arcOriginX - sourceArcOrigin
|
|
||||||
const intersection = circleIntersection(circleOffset, theme.node.corner_radius, radius)
|
|
||||||
sourceDY = -Math.abs(radius - intersection)
|
|
||||||
} else if (halfSourceSize.y != 0) {
|
|
||||||
sourceDY = 0 - innerTheme.source_node_overlap
|
|
||||||
startsInPort = offsetX < innerTheme.minimum_tangent_exit_radius
|
|
||||||
}
|
|
||||||
const source = new Vec2(sourceX, sourceDY)
|
|
||||||
return {
|
|
||||||
points: [source, inputs.targetOffset],
|
|
||||||
maxRadius,
|
|
||||||
startsInPort,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const radiusMax = theme.edge.three_corner.radius_max
|
|
||||||
// The edge originates from either side of the node.
|
|
||||||
const signX = Math.sign(inputs.targetOffset.x)
|
|
||||||
const sourceX = Math.abs(sourceMaxXOffset) * signX
|
|
||||||
const distanceX = Math.abs(inputs.targetOffset.x - sourceX)
|
|
||||||
let j0x: number
|
|
||||||
let j1x: number
|
|
||||||
let heightAdjustment: number
|
|
||||||
if (horizontalRoomFor3Corners) {
|
|
||||||
// J1
|
|
||||||
// /
|
|
||||||
// ╭──────╮
|
|
||||||
// ╭─────╮ │ ▢
|
|
||||||
// ╰─────╯────╯\
|
|
||||||
// J0
|
|
||||||
// Junctions (J0, J1) are in between source and target.
|
|
||||||
const j0Dx = Math.min(2 * radiusMax, distanceX / 2)
|
|
||||||
const j1Dx = Math.min(radiusMax, (distanceX - j0Dx) / 2)
|
|
||||||
j0x = sourceX + Math.abs(j0Dx) * signX
|
|
||||||
j1x = j0x + Math.abs(j1Dx) * signX
|
|
||||||
heightAdjustment = radiusMax - j1Dx
|
|
||||||
} else {
|
|
||||||
// J1
|
|
||||||
// /
|
|
||||||
// ╭──────╮ J0
|
|
||||||
// ▢ │/
|
|
||||||
// ╭─────╮ │
|
|
||||||
// ╰─────╯────╯
|
|
||||||
// J0 > source; J0 > J1; J1 > target.
|
|
||||||
j1x = inputs.targetOffset.x + Math.abs(radiusMax) * signX
|
|
||||||
const j0BeyondSource = Math.abs(inputs.targetOffset.x) + radiusMax * 2
|
|
||||||
const j0BeyondTarget = Math.abs(sourceX) + radiusMax
|
|
||||||
j0x = Math.abs(Math.max(j0BeyondTarget, j0BeyondSource)) * signX
|
|
||||||
heightAdjustment = 0
|
|
||||||
}
|
|
||||||
if (j0x == null || j1x == null || heightAdjustment == null) return null
|
|
||||||
const top = Math.min(
|
|
||||||
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)
|
|
||||||
return {
|
|
||||||
points: [source, j0, j1, attachmentTarget],
|
|
||||||
maxRadius: radiusMax,
|
|
||||||
startsInPort: horizontalRoomFor3CornersNoSqueeze,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Line = { axis: 'h' | 'v'; length: number }
|
|
||||||
type Arc = { radius: number; signX: number; signY: number; sweep: 0 | 1 }
|
|
||||||
type Element = Arc | Line
|
|
||||||
|
|
||||||
function pathElements(junctions: JunctionPoints): { start: Vec2; elements: Element[] } {
|
|
||||||
const elements: Element[] = []
|
|
||||||
const pushLine = (line: Line) => {
|
|
||||||
if (line.length === 0) return
|
|
||||||
const e = elements.pop()
|
|
||||||
if (e != null) {
|
|
||||||
if ('axis' in e && e.axis == line.axis) {
|
|
||||||
e.length += line.length
|
|
||||||
elements.push(e)
|
|
||||||
} else {
|
|
||||||
elements.push(e)
|
|
||||||
elements.push(line)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
elements.push(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const start = junctions.points[0]
|
|
||||||
if (start == null) return { start: Vec2.Zero, elements: [] }
|
|
||||||
let prev = start
|
|
||||||
junctions.points.slice(1).forEach((j, i) => {
|
|
||||||
const d = j.sub(prev)
|
|
||||||
const radius = Math.min(junctions.maxRadius, Math.abs(d.x), Math.abs(d.y))
|
|
||||||
const signX = Math.sign(d.x)
|
|
||||||
const signY = Math.sign(d.y)
|
|
||||||
const dx = (Math.abs(d.x) - radius) * signX
|
|
||||||
const dy = (Math.abs(d.y) - radius) * signY
|
|
||||||
const h: Line = { axis: 'h', length: dx }
|
|
||||||
const v: Line = { axis: 'v', length: dy }
|
|
||||||
const sweep = (signX === signY) === (i % 2 === 0) ? 1 : 0
|
|
||||||
if (i % 2 == 0) {
|
|
||||||
pushLine(h)
|
|
||||||
elements.push({ radius, signX, signY, sweep })
|
|
||||||
pushLine(v)
|
|
||||||
} else {
|
|
||||||
pushLine(v)
|
|
||||||
elements.push({ radius, signX, signY, sweep })
|
|
||||||
pushLine(h)
|
|
||||||
}
|
|
||||||
prev = j
|
|
||||||
})
|
|
||||||
return { start, elements }
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(sourcePos: Vec2, elements: Element[]): string {
|
|
||||||
let out = `M ${sourcePos.x} ${sourcePos.y}`
|
|
||||||
for (const e of elements) {
|
|
||||||
if ('axis' in e) {
|
|
||||||
out += ` ${e.axis} ${e.length}`
|
|
||||||
} else {
|
|
||||||
const dx = e.radius * e.signX
|
|
||||||
const dy = e.radius * e.signY
|
|
||||||
out += ` a ${e.radius} ${e.radius} 0 0 ${e.sweep} ${dx} ${dy}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceOriginPoint = computed(() => {
|
const sourceOriginPoint = computed(() => {
|
||||||
const source = sourceRect.value
|
const source = sourceRect.value
|
||||||
if (source == null) return null
|
if (source == null) return null
|
||||||
const target = targetPos.value
|
const target = targetPos.value
|
||||||
const targetAbove = target != null ? target.y < source.bottom : false
|
const targetAbove = target != null ? target.y < source.bottom : false
|
||||||
const targetAside = target != null ? source.left > target.x || source.right < target.x : false
|
const targetAside = target != null ? source.left > target.x || source.right < target.x : false
|
||||||
const offset = targetAside || targetAbove ? theme.node.corner_radius : 0
|
const halfSourceHeight = (source.bottom - source.top) * 0.5
|
||||||
|
const offset =
|
||||||
|
targetAside || targetAbove ? Math.min(halfSourceHeight, theme.node.corner_radius) : 0
|
||||||
const sourceStartPosY = Math.max(source.top + offset, source.bottom - offset)
|
const sourceStartPosY = Math.max(source.top + offset, source.bottom - offset)
|
||||||
return new Vec2(source.center().x, sourceStartPosY)
|
return new Vec2(source.center().x, sourceStartPosY)
|
||||||
})
|
})
|
||||||
@ -420,12 +178,10 @@ const basePath = computed(() => {
|
|||||||
const { start, elements } = pathElements
|
const { start, elements } = pathElements
|
||||||
const origin = sourceOriginPoint.value
|
const origin = sourceOriginPoint.value
|
||||||
if (origin == null) return undefined
|
if (origin == null) return undefined
|
||||||
return render(origin.add(start), elements)
|
return toSvgPath(origin.add(start), elements)
|
||||||
})
|
})
|
||||||
|
|
||||||
const activePath = computed(
|
const activePath = computed(() => hovered.value && edge.source != null && edge.target != null)
|
||||||
() => hovered.value && props.edge.source != null && props.edge.target != null,
|
|
||||||
)
|
|
||||||
|
|
||||||
function lengthTo(path: SVGPathElement, pos: Vec2): number {
|
function lengthTo(path: SVGPathElement, pos: Vec2): number {
|
||||||
const totalLength = path.getTotalLength()
|
const totalLength = path.getTotalLength()
|
||||||
@ -463,7 +219,7 @@ const mouseLocationOnEdge = computed(() => {
|
|||||||
const hovered = ref(false)
|
const hovered = ref(false)
|
||||||
const activeStyle = computed(() => {
|
const activeStyle = computed(() => {
|
||||||
if (!hovered.value) return {}
|
if (!hovered.value) return {}
|
||||||
if (props.edge.source == null || props.edge.target == null) return {}
|
if (edge.source == null || edge.target == null) return {}
|
||||||
const distances = mouseLocationOnEdge.value
|
const distances = mouseLocationOnEdge.value
|
||||||
if (distances == null) return {}
|
if (distances == null) return {}
|
||||||
const offset =
|
const offset =
|
||||||
@ -477,6 +233,7 @@ const activeStyle = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const targetEndIsDimmed = computed(() => {
|
const targetEndIsDimmed = computed(() => {
|
||||||
|
if (isSuggestion.value) return true
|
||||||
if (!hovered.value) return false
|
if (!hovered.value) return false
|
||||||
const distances = mouseLocationOnEdge.value
|
const distances = mouseLocationOnEdge.value
|
||||||
if (!distances) return false
|
if (!distances) return false
|
||||||
@ -488,9 +245,9 @@ const baseStyle = computed(() => ({ '--node-base-color': edgeColor.value ?? 'tan
|
|||||||
function click(event: PointerEvent) {
|
function click(event: PointerEvent) {
|
||||||
const distances = mouseLocationOnEdge.value
|
const distances = mouseLocationOnEdge.value
|
||||||
if (distances == null) return
|
if (distances == null) return
|
||||||
if (!isConnected(props.edge)) return
|
if (!isConnected(edge)) return
|
||||||
if (distances.sourceToMouse < distances.mouseToTarget) graph.disconnectTarget(props.edge, event)
|
if (distances.sourceToMouse < distances.mouseToTarget) graph.disconnectTarget(edge, event)
|
||||||
else graph.disconnectSource(props.edge, event)
|
else graph.disconnectSource(edge, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
function svgTranslate(offset: Vec2): string {
|
function svgTranslate(offset: Vec2): string {
|
||||||
@ -498,7 +255,7 @@ function svgTranslate(offset: Vec2): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backwardEdgeArrowTransform = computed<string | undefined>(() => {
|
const backwardEdgeArrowTransform = computed<string | undefined>(() => {
|
||||||
if (props.edge.source == null || props.edge.target == null) return
|
if (edge.source == null || edge.target == null) return
|
||||||
const points = currentJunctionPoints.value?.points
|
const points = currentJunctionPoints.value?.points
|
||||||
if (points == null || points.length < 3) return
|
if (points == null || points.length < 3) return
|
||||||
const target = targetPos.value
|
const target = targetPos.value
|
||||||
@ -532,7 +289,7 @@ const arrowPath = [
|
|||||||
].join('')
|
].join('')
|
||||||
|
|
||||||
const sourceHoverAnimationStyle = computed(() => {
|
const sourceHoverAnimationStyle = computed(() => {
|
||||||
if (!props.animateFromSourceHover || !base.value || !sourceNode.value) return {}
|
if (!animateFromSourceHover || !base.value || !sourceNode.value) return {}
|
||||||
const progress = graph.nodeHoverAnimations.get(sourceNode.value) ?? 0
|
const progress = graph.nodeHoverAnimations.get(sourceNode.value) ?? 0
|
||||||
if (progress === 1) return {}
|
if (progress === 1) return {}
|
||||||
const currentLength = progress * base.value.getTotalLength()
|
const currentLength = progress * base.value.getTotalLength()
|
||||||
@ -540,6 +297,10 @@ const sourceHoverAnimationStyle = computed(() => {
|
|||||||
strokeDasharray: `${currentLength}px 1000000px`,
|
strokeDasharray: `${currentLength}px 1000000px`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const baseClass = computed(() => {
|
||||||
|
return { dimmed: activePath.value || isSuggestion.value }
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -575,13 +336,13 @@ const sourceHoverAnimationStyle = computed(() => {
|
|||||||
ref="base"
|
ref="base"
|
||||||
:d="basePath"
|
:d="basePath"
|
||||||
class="edge visible"
|
class="edge visible"
|
||||||
:class="{ dimmed: activePath || isSuggestion }"
|
:class="baseClass"
|
||||||
:style="{ ...baseStyle, ...sourceHoverAnimationStyle }"
|
:style="{ ...baseStyle, ...sourceHoverAnimationStyle }"
|
||||||
:data-source-node-id="sourceNode"
|
:data-source-node-id="sourceNode"
|
||||||
:data-target-node-id="targetNode"
|
:data-target-node-id="targetNode"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
v-if="isConnected(props.edge)"
|
v-if="isConnected(edge)"
|
||||||
:d="basePath"
|
:d="basePath"
|
||||||
class="edge io clickable"
|
class="edge io clickable"
|
||||||
:data-source-node-id="sourceNode"
|
:data-source-node-id="sourceNode"
|
||||||
@ -599,6 +360,14 @@ const sourceHoverAnimationStyle = computed(() => {
|
|||||||
:data-source-node-id="sourceNode"
|
:data-source-node-id="sourceNode"
|
||||||
:data-target-node-id="targetNode"
|
:data-target-node-id="targetNode"
|
||||||
/>
|
/>
|
||||||
|
<path
|
||||||
|
v-if="arrowTransform"
|
||||||
|
:transform="arrowTransform"
|
||||||
|
:d="arrowPath"
|
||||||
|
class="arrow visible"
|
||||||
|
:class="{ dimmed: targetEndIsDimmed }"
|
||||||
|
:style="baseStyle"
|
||||||
|
/>
|
||||||
<polygon
|
<polygon
|
||||||
v-if="backwardEdgeArrowTransform"
|
v-if="backwardEdgeArrowTransform"
|
||||||
:transform="backwardEdgeArrowTransform"
|
:transform="backwardEdgeArrowTransform"
|
||||||
@ -608,13 +377,6 @@ const sourceHoverAnimationStyle = computed(() => {
|
|||||||
:data-source-node-id="sourceNode"
|
:data-source-node-id="sourceNode"
|
||||||
:data-target-node-id="targetNode"
|
:data-target-node-id="targetNode"
|
||||||
/>
|
/>
|
||||||
<path
|
|
||||||
v-if="arrowTransform"
|
|
||||||
:transform="arrowTransform"
|
|
||||||
:d="arrowPath"
|
|
||||||
:class="{ arrow: true, visible: true, dimmed: targetEndIsDimmed }"
|
|
||||||
:style="baseStyle"
|
|
||||||
/>
|
|
||||||
</g>
|
</g>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
@ -0,0 +1,250 @@
|
|||||||
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
|
import theme from '@/util/theme'
|
||||||
|
import { clamp } from '@vueuse/core'
|
||||||
|
|
||||||
|
/** The inputs to the edge state computation. */
|
||||||
|
export 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JunctionPoints {
|
||||||
|
points: Vec2[]
|
||||||
|
maxRadius: number
|
||||||
|
startsInPort: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge layout calculation.
|
||||||
|
*
|
||||||
|
* # Corners
|
||||||
|
*
|
||||||
|
* ```text
|
||||||
|
* ────╮
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The fundamental unit of edge layout is the [`Corner`]. A corner is a line segment attached to a
|
||||||
|
* 90° arc. The length of the straight segment, the radius of the arc, and the orientation of the
|
||||||
|
* shape may vary. Any shape of edge is built from corners.
|
||||||
|
*
|
||||||
|
* The shape of a corner can be fully-specified by two points: The horizontal end, and the vertical
|
||||||
|
* end.
|
||||||
|
*
|
||||||
|
* In special cases, a corner may be *trivial*: It may have a radius of zero, in which case either
|
||||||
|
* the horizontal or vertical end will not be in the usual orientation. The layout algorithm only
|
||||||
|
* produces trivial corners when the source is directly in line with the target, or in some cases
|
||||||
|
* when subdividing a corner (see [Partial edges] below).
|
||||||
|
*
|
||||||
|
* # Junction points
|
||||||
|
*
|
||||||
|
* ```text
|
||||||
|
* 3
|
||||||
|
* 1 /
|
||||||
|
* \ ╭─────╮
|
||||||
|
* ────╯\ \
|
||||||
|
* 2 4
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The layout algorithm doesn't directly place corners. The layout algorithm places a sequence of
|
||||||
|
* junction points--coordinates where two horizontal corner ends or two vertical corner ends meet
|
||||||
|
* (or just one corner end, at an end of an edge). A series of junction points, always alternating
|
||||||
|
* horizontal/vertical, has a one-to-one relationship with a sequence of corners.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the start and end positions of each 1-corner section composing an edge to the
|
||||||
|
* given offset. Return the points and the maximum radius that should be used to draw the corners
|
||||||
|
* connecting them.
|
||||||
|
*/
|
||||||
|
export function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||||
|
const 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, 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 &&
|
||||||
|
Math.abs(inputs.targetOffset.x) - sourceMaxXOffset >=
|
||||||
|
3.0 * (theme.edge.radius - theme.edge.three_corner.max_squeeze)
|
||||||
|
const horizontalRoomFor3CornersNoSqueeze =
|
||||||
|
targetBeyondSource &&
|
||||||
|
Math.abs(inputs.targetOffset.x) - sourceMaxXOffset >=
|
||||||
|
3.0 * theme.edge.radius + theme.edge.three_corner.radius_max
|
||||||
|
|
||||||
|
if (targetWellBelowSource || (targetBelowSource && !horizontalRoomFor3Corners)) {
|
||||||
|
const innerTheme = theme.edge.one_corner
|
||||||
|
// The edge can originate anywhere along the length of the node.
|
||||||
|
const sourceX = clamp(inputs.targetOffset.x, -sourceMaxXOffset, sourceMaxXOffset)
|
||||||
|
const distanceX = Math.max(Math.abs(inputs.targetOffset.x) - halfSourceSize.x, 0)
|
||||||
|
const radiusX = innerTheme.radius_x_base + distanceX * innerTheme.radius_x_factor
|
||||||
|
// The minimum length of straight line there should be at the target end of the edge. This
|
||||||
|
// is a fixed value, except it is reduced when the target is horizontally very close to the
|
||||||
|
// edge of the source, so that very short edges are less sharp.
|
||||||
|
const yAdjustment = Math.min(
|
||||||
|
Math.abs(inputs.targetOffset.x) - halfSourceSize.x + innerTheme.radius_y_adjustment / 2.0,
|
||||||
|
innerTheme.radius_y_adjustment,
|
||||||
|
)
|
||||||
|
const radiusY = Math.max(Math.abs(inputs.targetOffset.y) - yAdjustment, 0.0)
|
||||||
|
const maxRadius = Math.min(radiusX, radiusY)
|
||||||
|
// The radius the edge would have, if the arc portion were as large as possible.
|
||||||
|
const offsetX = Math.abs(inputs.targetOffset.x - sourceX)
|
||||||
|
const naturalRadius = Math.min(
|
||||||
|
Math.abs(inputs.targetOffset.x - sourceX),
|
||||||
|
Math.abs(inputs.targetOffset.y),
|
||||||
|
)
|
||||||
|
let sourceDY = 0
|
||||||
|
let startsInPort = true
|
||||||
|
if (naturalRadius > innerTheme.minimum_tangent_exit_radius) {
|
||||||
|
// Offset the beginning of the edge so that it is normal to the curve of the source node
|
||||||
|
// 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 - theme.node.corner_radius
|
||||||
|
const circleOffset = arcOriginX - sourceArcOrigin
|
||||||
|
const intersection = circleIntersection(circleOffset, theme.node.corner_radius, radius)
|
||||||
|
sourceDY = -Math.abs(radius - intersection)
|
||||||
|
} else if (halfSourceSize.y != 0) {
|
||||||
|
sourceDY = 0 - innerTheme.source_node_overlap
|
||||||
|
startsInPort = offsetX < innerTheme.minimum_tangent_exit_radius
|
||||||
|
}
|
||||||
|
const source = new Vec2(sourceX, sourceDY)
|
||||||
|
return {
|
||||||
|
points: [source, inputs.targetOffset],
|
||||||
|
maxRadius,
|
||||||
|
startsInPort,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const radiusMax = theme.edge.three_corner.radius_max
|
||||||
|
// The edge originates from either side of the node.
|
||||||
|
const signX = Math.sign(inputs.targetOffset.x)
|
||||||
|
const sourceX = Math.abs(sourceMaxXOffset) * signX
|
||||||
|
const distanceX = Math.abs(inputs.targetOffset.x - sourceX)
|
||||||
|
let j0x: number
|
||||||
|
let j1x: number
|
||||||
|
let heightAdjustment: number
|
||||||
|
if (horizontalRoomFor3Corners) {
|
||||||
|
// J1
|
||||||
|
// /
|
||||||
|
// ╭──────╮
|
||||||
|
// ╭─────╮ │ ▢
|
||||||
|
// ╰─────╯────╯\
|
||||||
|
// J0
|
||||||
|
// Junctions (J0, J1) are in between source and target.
|
||||||
|
const j0Dx = Math.min(2 * radiusMax, distanceX / 2)
|
||||||
|
const j1Dx = Math.min(radiusMax, (distanceX - j0Dx) / 2)
|
||||||
|
j0x = sourceX + Math.abs(j0Dx) * signX
|
||||||
|
j1x = j0x + Math.abs(j1Dx) * signX
|
||||||
|
heightAdjustment = radiusMax - j1Dx
|
||||||
|
} else {
|
||||||
|
// J1
|
||||||
|
// /
|
||||||
|
// ╭──────╮ J0
|
||||||
|
// ▢ │/
|
||||||
|
// ╭─────╮ │
|
||||||
|
// ╰─────╯────╯
|
||||||
|
// J0 > source; J0 > J1; J1 > target.
|
||||||
|
j1x = inputs.targetOffset.x + Math.abs(radiusMax) * signX
|
||||||
|
const j0BeyondSource = Math.abs(inputs.targetOffset.x) + radiusMax * 2
|
||||||
|
const j0BeyondTarget = Math.abs(sourceX) + radiusMax
|
||||||
|
j0x = Math.abs(Math.max(j0BeyondTarget, j0BeyondSource)) * signX
|
||||||
|
heightAdjustment = 0
|
||||||
|
}
|
||||||
|
if (j0x == null || j1x == null || heightAdjustment == null) return null
|
||||||
|
const top = Math.min(
|
||||||
|
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)
|
||||||
|
return {
|
||||||
|
points: [source, j0, j1, attachmentTarget],
|
||||||
|
maxRadius: radiusMax,
|
||||||
|
startsInPort: horizontalRoomFor3CornersNoSqueeze,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Line = { axis: 'h' | 'v'; length: number }
|
||||||
|
type Arc = { radius: number; signX: number; signY: number; sweep: 0 | 1 }
|
||||||
|
type Element = Arc | Line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert calculated path junction points to an array of rounded horizontal/vertical path elements.
|
||||||
|
*/
|
||||||
|
export function pathElements(junctions: JunctionPoints): { start: Vec2; elements: Element[] } {
|
||||||
|
const elements: Element[] = []
|
||||||
|
const pushLine = (line: Line) => {
|
||||||
|
if (line.length === 0) return
|
||||||
|
const e = elements.pop()
|
||||||
|
if (e != null) {
|
||||||
|
if ('axis' in e && e.axis == line.axis) {
|
||||||
|
e.length += line.length
|
||||||
|
elements.push(e)
|
||||||
|
} else {
|
||||||
|
elements.push(e)
|
||||||
|
elements.push(line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements.push(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const start = junctions.points[0]
|
||||||
|
if (start == null) return { start: Vec2.Zero, elements: [] }
|
||||||
|
let prev = start
|
||||||
|
junctions.points.slice(1).forEach((j, i) => {
|
||||||
|
const d = j.sub(prev)
|
||||||
|
const radius = Math.min(junctions.maxRadius, Math.abs(d.x), Math.abs(d.y))
|
||||||
|
const signX = Math.sign(d.x)
|
||||||
|
const signY = Math.sign(d.y)
|
||||||
|
const dx = (Math.abs(d.x) - radius) * signX
|
||||||
|
const dy = (Math.abs(d.y) - radius) * signY
|
||||||
|
const h: Line = { axis: 'h', length: dx }
|
||||||
|
const v: Line = { axis: 'v', length: dy }
|
||||||
|
const sweep = (signX === signY) === (i % 2 === 0) ? 1 : 0
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
pushLine(h)
|
||||||
|
elements.push({ radius, signX, signY, sweep })
|
||||||
|
pushLine(v)
|
||||||
|
} else {
|
||||||
|
pushLine(v)
|
||||||
|
elements.push({ radius, signX, signY, sweep })
|
||||||
|
pushLine(h)
|
||||||
|
}
|
||||||
|
prev = j
|
||||||
|
})
|
||||||
|
return { start, elements }
|
||||||
|
}
|
||||||
|
|
||||||
|
function circleIntersection(x: number, r1: number, r2: number): number {
|
||||||
|
const xNorm = clamp(x, -r2, r1)
|
||||||
|
return Math.sqrt(r1 * r1 + r2 * r2 - xNorm * xNorm)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a set of generated path elements to svg path syntax representation.
|
||||||
|
*/
|
||||||
|
export function toSvgPath(sourcePos: Vec2, elements: Element[]): string {
|
||||||
|
let out = `M ${sourcePos.x} ${sourcePos.y}`
|
||||||
|
for (const e of elements) {
|
||||||
|
if ('axis' in e) {
|
||||||
|
out += ` ${e.axis} ${e.length}`
|
||||||
|
} else {
|
||||||
|
const dx = e.radius * e.signX
|
||||||
|
const dy = e.radius * e.signY
|
||||||
|
out += ` a ${e.radius} ${e.radius} 0 0 ${e.sweep} ${dx} ${dy}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
@ -24,8 +24,6 @@ export interface UnconnectedSource extends AnyUnconnectedEdge {
|
|||||||
export interface UnconnectedTarget extends AnyUnconnectedEdge {
|
export interface UnconnectedTarget extends AnyUnconnectedEdge {
|
||||||
source: AstId
|
source: AstId
|
||||||
target: undefined
|
target: undefined
|
||||||
/** If true, the target end should be drawn as with a self-argument arrow. */
|
|
||||||
targetIsSelfArgument?: boolean
|
|
||||||
/** If true, the edge will be rendered in its dimmed color. */
|
/** If true, the edge will be rendered in its dimmed color. */
|
||||||
suggestion?: boolean
|
suggestion?: boolean
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user