mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 23:51:31 +03:00
Implement node ports below visualizations (#10113)
fixes #9830, #10108 ![image](https://github.com/enso-org/enso/assets/919491/8ae50912-1dfb-4389-b51a-44022b334b89)
This commit is contained in:
parent
56b289ae79
commit
322662ee9d
@ -1824,7 +1824,7 @@ applyMixins(MutableNumericLiteral, [MutableAst])
|
||||
* GUI. We just need to represent them faithfully and create the simple cases. */
|
||||
type ArgumentDefinition<T extends TreeRefs = RawRefs> = (T['ast'] | T['token'])[]
|
||||
|
||||
interface FunctionFields {
|
||||
export interface FunctionFields {
|
||||
name: NodeChild<AstId>
|
||||
argumentDefinitions: ArgumentDefinition[]
|
||||
equals: NodeChild<SyncTokenId>
|
||||
|
@ -19,6 +19,11 @@ const props = defineProps<{
|
||||
maskSource?: boolean
|
||||
}>()
|
||||
|
||||
// The padding added around the masking rect for nodes with visible output port. The actual padding
|
||||
// is animated together with node's port opening. Required to correctly not draw the edge in space
|
||||
// between the port path and node.
|
||||
const VISIBLE_PORT_MASK_PADDING = 6
|
||||
|
||||
const base = ref<SVGPathElement>()
|
||||
|
||||
const mouseAnchor = computed(() => 'anchor' in props.edge && props.edge.anchor.type === 'mouse')
|
||||
@ -109,11 +114,18 @@ type NodeMask = {
|
||||
rect: Rect
|
||||
radius: number
|
||||
}
|
||||
|
||||
const sourceMask = computed<NodeMask | undefined>(() => {
|
||||
if (!props.maskSource) return
|
||||
const rect = sourceNodeRect.value
|
||||
if (!rect) return
|
||||
const radius = 16
|
||||
const startsInPort = currentJunctionPoints.value?.startsInPort
|
||||
if (!props.maskSource && !startsInPort) return
|
||||
const nodeRect = sourceNodeRect.value
|
||||
if (!nodeRect) return
|
||||
const animProgress =
|
||||
startsInPort ? (sourceNode.value && graph.nodeHoverAnimations.get(sourceNode.value)) ?? 0 : 0
|
||||
let padding = animProgress * VISIBLE_PORT_MASK_PADDING
|
||||
if (!props.maskSource && padding === 0) return
|
||||
const rect = nodeRect.expand(padding)
|
||||
const radius = 16 + padding
|
||||
const id = `mask_for_edge_to-${props.edge.target ?? 'unconnected'}`
|
||||
return { id, rect, radius }
|
||||
})
|
||||
@ -138,6 +150,7 @@ interface Inputs {
|
||||
interface JunctionPoints {
|
||||
points: Vec2[]
|
||||
maxRadius: number
|
||||
startsInPort: boolean
|
||||
}
|
||||
|
||||
function circleIntersection(x: number, r1: number, r2: number): number {
|
||||
@ -198,6 +211,11 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
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.
|
||||
@ -214,11 +232,13 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
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.
|
||||
@ -229,12 +249,14 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
const intersection = circleIntersection(circleOffset, theme.node.corner_radius, radius)
|
||||
sourceDY = -Math.abs(radius - intersection)
|
||||
} else if (halfSourceSize.y != 0) {
|
||||
sourceDY = -innerTheme.source_node_overlap + halfSourceSize.y
|
||||
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
|
||||
@ -283,6 +305,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
return {
|
||||
points: [source, j0, j1, attachmentTarget],
|
||||
maxRadius: radiusMax,
|
||||
startsInPort: horizontalRoomFor3CornersNoSqueeze,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -349,25 +372,41 @@ function render(sourcePos: Vec2, elements: Element[]): string {
|
||||
return out
|
||||
}
|
||||
|
||||
const sourceOriginPoint = computed(() => {
|
||||
const source = sourceRect.value
|
||||
if (source == null) return null
|
||||
const sourceStartPosY = Math.max(
|
||||
source.top + theme.node.corner_radius,
|
||||
source.bottom - theme.node.corner_radius,
|
||||
)
|
||||
return new Vec2(source.center().x, sourceStartPosY)
|
||||
})
|
||||
|
||||
const currentJunctionPoints = computed(() => {
|
||||
const target = targetPos.value
|
||||
const source = sourceRect.value
|
||||
if (target == null || source == null) return null
|
||||
const inputs: Inputs = {
|
||||
const origin = sourceOriginPoint.value
|
||||
if (target == null || source == null || origin == null) return null
|
||||
|
||||
return junctionPoints({
|
||||
sourceSize: source.size,
|
||||
targetOffset: target.sub(source.center()),
|
||||
}
|
||||
return junctionPoints(inputs)
|
||||
targetOffset: target.sub(origin),
|
||||
})
|
||||
})
|
||||
|
||||
const basePathElements = computed(() => {
|
||||
const jp = currentJunctionPoints.value
|
||||
if (jp == null) return undefined
|
||||
return pathElements(jp)
|
||||
})
|
||||
|
||||
const basePath = computed(() => {
|
||||
if (props.edge.source == null && props.edge.target == null) return undefined
|
||||
const jp = currentJunctionPoints.value
|
||||
if (jp == null) return undefined
|
||||
const { start, elements } = pathElements(jp)
|
||||
const source_ = sourceRect.value
|
||||
if (source_ == null) return undefined
|
||||
return render(source_.center().add(start), elements)
|
||||
const pathElements = basePathElements.value
|
||||
if (!pathElements) return
|
||||
const { start, elements } = pathElements
|
||||
const origin = sourceOriginPoint.value
|
||||
if (origin == null) return undefined
|
||||
return render(origin.add(start), elements)
|
||||
})
|
||||
|
||||
const activePath = computed(() => {
|
||||
@ -449,11 +488,11 @@ const backwardEdgeArrowTransform = computed<string | undefined>(() => {
|
||||
const points = currentJunctionPoints.value?.points
|
||||
if (points == null || points.length < 3) return
|
||||
const target = targetPos.value
|
||||
const source = sourceRect.value
|
||||
if (target == null || source == null) return
|
||||
if (target.y > source.pos.y - theme.edge.three_corner.backward_edge_arrow_threshold) return
|
||||
const origin = sourceOriginPoint.value
|
||||
if (target == null || origin == null) return
|
||||
if (target.y > origin.y - theme.edge.three_corner.backward_edge_arrow_threshold) return
|
||||
if (points[1] == null) return
|
||||
return svgTranslate(source.center().add(points[1]))
|
||||
return svgTranslate(origin.add(points[1]))
|
||||
})
|
||||
|
||||
const targetIsSelfArgument = computed(() => {
|
||||
|
@ -34,7 +34,16 @@ import { displayedIconOf } from '@/util/getIconName'
|
||||
import { setIfUndefined } from 'lib0/map'
|
||||
import type { ExternalId, VisualizationIdentifier } from 'shared/yjsModel'
|
||||
import type { EffectScope } from 'vue'
|
||||
import { computed, effectScope, onScopeDispose, onUnmounted, ref, watch, watchEffect } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
effectScope,
|
||||
onScopeDispose,
|
||||
onUnmounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from 'vue'
|
||||
|
||||
const MAXIMUM_CLICK_LENGTH_MS = 300
|
||||
const MAXIMUM_CLICK_DISTANCE_SQ = 50
|
||||
@ -62,6 +71,7 @@ const emit = defineEmits<{
|
||||
setNodeColor: [color: string | undefined]
|
||||
'update:edited': [cursorPosition: number]
|
||||
'update:rect': [rect: Rect]
|
||||
'update:hoverAnim': [progress: number]
|
||||
'update:visualizationId': [id: Opt<VisualizationIdentifier>]
|
||||
'update:visualizationRect': [rect: Rect | undefined]
|
||||
'update:visualizationVisible': [visible: boolean]
|
||||
@ -225,13 +235,6 @@ const visualizationHeight = computed(() => props.node.vis?.height ?? null)
|
||||
const isVisualizationVisible = computed(() => props.node.vis?.visible ?? false)
|
||||
const isVisualizationFullscreen = computed(() => props.node.vis?.fullscreen ?? false)
|
||||
|
||||
watchEffect(() => {
|
||||
const size = nodeSize.value
|
||||
if (!size.isZero()) {
|
||||
emit('update:rect', new Rect(props.node.position, nodeSize.value))
|
||||
}
|
||||
})
|
||||
|
||||
const bgStyleVariables = computed(() => {
|
||||
const { x: width, y: height } = nodeSize.value
|
||||
return {
|
||||
@ -401,6 +404,23 @@ const outputPorts = computed((): PortData[] => {
|
||||
})
|
||||
|
||||
const outputHovered = ref<AstId>()
|
||||
const anyPortDisconnected = computed(() => {
|
||||
for (const port of outputPortsSet.value) {
|
||||
if (graph.disconnectedEdgePorts.has(port)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
const portsVisible = computed(
|
||||
() =>
|
||||
selectionVisible.value ||
|
||||
(outputHovered.value && outputPortsSet.value.has(outputHovered.value)) ||
|
||||
anyPortDisconnected.value,
|
||||
)
|
||||
|
||||
const portsHoverAnimation = useApproach(() => (portsVisible.value ? 1 : 0), 50, 0.01)
|
||||
|
||||
watchEffect(() => emit('update:hoverAnim', portsHoverAnimation.value))
|
||||
|
||||
const hoverAnimations = new Map<AstId, [ReturnType<typeof useApproach>, EffectScope]>()
|
||||
watchEffect(() => {
|
||||
const ports = outputPortsSet.value
|
||||
@ -415,18 +435,7 @@ watchEffect(() => {
|
||||
// the setup top-level), we need to create a detached scope for each invocation.
|
||||
const scope = effectScope(true)
|
||||
const approach = scope.run(() =>
|
||||
useApproach(
|
||||
() =>
|
||||
(
|
||||
outputHovered.value === port ||
|
||||
graph.disconnectedEdgeTargets.has(port) ||
|
||||
selectionVisible.value
|
||||
) ?
|
||||
1
|
||||
: 0,
|
||||
50,
|
||||
0.01,
|
||||
),
|
||||
useApproach(() => (outputHovered.value === port ? 1 : 0), 50, 0.01),
|
||||
)!
|
||||
return [approach, scope]
|
||||
})
|
||||
@ -438,17 +447,40 @@ onScopeDispose(() => hoverAnimations.forEach(([_, scope]) => scope.stop()))
|
||||
|
||||
function portGroupStyle(port: PortData) {
|
||||
const [start, end] = port.clipRange
|
||||
const visBelowNode = graphSelectionSize.value.y - nodeSize.value.y
|
||||
return {
|
||||
'--hover-animation': hoverAnimations.get(port.portId)?.[0].value ?? 0,
|
||||
'--hover-animation': portsHoverAnimation.value,
|
||||
'--direct-hover-animation': hoverAnimations.get(port.portId)?.[0].value ?? 0,
|
||||
'--port-clip-start': start,
|
||||
'--port-clip-end': end,
|
||||
transform: `translateY(${visBelowNode}px`,
|
||||
}
|
||||
}
|
||||
|
||||
const visRect = shallowRef<Rect>()
|
||||
function updateVisualizationRect(rect: Rect | undefined) {
|
||||
visRect.value = rect
|
||||
emit('update:visualizationRect', rect)
|
||||
}
|
||||
|
||||
const editingComment = ref(false)
|
||||
|
||||
const { getNodeColor, getNodeColors } = injectNodeColors()
|
||||
const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
|
||||
const graphSelectionSize = computed(() =>
|
||||
isVisualizationVisible.value && visRect.value ? visRect.value.size : nodeSize.value,
|
||||
)
|
||||
|
||||
const nodeRect = computed(() => new Rect(props.node.position, nodeSize.value))
|
||||
const nodeOuterRect = computed(() =>
|
||||
isVisualizationVisible.value && visRect.value ? visRect.value : nodeRect.value,
|
||||
)
|
||||
watchEffect(() => {
|
||||
if (!nodeOuterRect.value.size.isZero()) {
|
||||
emit('update:rect', nodeOuterRect.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -458,7 +490,7 @@ const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
class="GraphNode"
|
||||
:style="{
|
||||
transform,
|
||||
minWidth: isVisualizationVisible ? `${visualizationWidth}px` : undefined,
|
||||
minWidth: isVisualizationVisible ? `${visualizationWidth ?? 200}px` : undefined,
|
||||
'--node-group-color': color,
|
||||
...(node.zIndex ? { 'z-index': node.zIndex } : {}),
|
||||
}"
|
||||
@ -478,10 +510,11 @@ const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
v-if="navigator && !edited"
|
||||
:class="{ dragged: isDragged }"
|
||||
:nodePosition="props.node.position"
|
||||
:nodeSize="nodeSize"
|
||||
:nodeSize="graphSelectionSize"
|
||||
:selected
|
||||
:nodeId
|
||||
:color
|
||||
:externalHovered="nodeHovered"
|
||||
@visible="selectionVisible = $event"
|
||||
@pointerenter="updateSelectionHover"
|
||||
@pointermove="updateSelectionHover"
|
||||
@ -531,7 +564,7 @@ const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
:width="visualizationWidth"
|
||||
:height="visualizationHeight"
|
||||
:isFocused="isOnlyOneSelected"
|
||||
@update:rect="emit('update:visualizationRect', $event)"
|
||||
@update:rect="updateVisualizationRect"
|
||||
@update:id="emit('update:visualizationId', $event)"
|
||||
@update:visible="emit('update:visualizationVisible', $event)"
|
||||
@update:fullscreen="emit('update:visualizationFullscreen', $event)"
|
||||
@ -605,6 +638,7 @@ const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
display: flex;
|
||||
|
||||
--output-port-max-width: 4px;
|
||||
--output-port-hovered-extra-width: 2px;
|
||||
--output-port-overlap: -8px;
|
||||
--output-port-hover-width: 20px;
|
||||
}
|
||||
@ -612,12 +646,14 @@ const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
.outputPort,
|
||||
.outputPortHoverArea {
|
||||
x: calc(0px - var(--output-port-width) / 2);
|
||||
y: calc(0px - var(--output-port-width) / 2);
|
||||
height: calc(var(--node-height) + var(--output-port-width));
|
||||
width: calc(var(--node-width) + var(--output-port-width));
|
||||
rx: calc(var(--node-border-radius) + var(--output-port-width) / 2);
|
||||
|
||||
fill: none;
|
||||
stroke: var(--node-color-port);
|
||||
stroke-width: calc(var(--output-port-width) + var(--output-port-overlap));
|
||||
stroke-width: calc(var(--output-port-width) + var(--output-port-overlap-anim));
|
||||
transition: stroke 0.2s ease;
|
||||
--horizontal-line: calc(var(--node-width) - var(--node-border-radius) * 2);
|
||||
--vertical-line: calc(var(--node-height) - var(--node-border-radius) * 2);
|
||||
@ -633,23 +669,22 @@ const matchableNodeColors = getNodeColors((node) => node !== nodeId.value)
|
||||
}
|
||||
|
||||
.outputPort {
|
||||
--output-port-overlap-anim: calc(var(--hover-animation) * var(--output-port-overlap));
|
||||
--output-port-width: calc(
|
||||
var(--output-port-max-width) * var(--hover-animation) - var(--output-port-overlap)
|
||||
var(--output-port-max-width) * var(--hover-animation) + var(--output-port-hovered-extra-width) *
|
||||
var(--direct-hover-animation) - var(--output-port-overlap-anim)
|
||||
);
|
||||
y: calc(0px - var(--output-port-width) / 2);
|
||||
height: calc(var(--node-height) + var(--output-port-width));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.outputPortHoverArea {
|
||||
--output-port-width: var(--output-port-hover-width);
|
||||
y: calc(
|
||||
0px + var(--output-port-hover-width) / 2 + var(--output-port-overlap) / 2 + var(--node-height) /
|
||||
2
|
||||
);
|
||||
height: calc(var(--node-height) / 2 + var(--output-port-hover-width) / 2);
|
||||
stroke-width: var(--output-port-hover-width);
|
||||
stroke: transparent;
|
||||
pointer-events: all;
|
||||
/* Make stroke visible to debug the active area: */
|
||||
/* stroke: red; */
|
||||
stroke-linecap: butt;
|
||||
pointer-events: stroke;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ const props = defineProps<{
|
||||
nodeSize: Vec2
|
||||
nodeId: AstId
|
||||
selected: boolean
|
||||
externalHovered: boolean
|
||||
color: string
|
||||
}>()
|
||||
|
||||
@ -16,7 +17,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const hovered = ref(false)
|
||||
const visible = computed(() => props.selected || hovered.value)
|
||||
const visible = computed(() => props.selected || props.externalHovered || hovered.value)
|
||||
|
||||
watchEffect(() => emit('visible', visible.value))
|
||||
|
||||
|
@ -58,6 +58,7 @@ const uploadingFiles = computed<[FileName, File][]>(() => {
|
||||
@setNodeColor="emit('setNodeColor', $event)"
|
||||
@update:edited="graphStore.setEditedNode(id, $event)"
|
||||
@update:rect="graphStore.updateNodeRect(id, $event)"
|
||||
@update:hoverAnim="graphStore.updateNodeHoverAnim(id, $event)"
|
||||
@update:visualizationId="
|
||||
graphStore.setNodeVisualization(id, $event != null ? { identifier: $event } : {})
|
||||
"
|
||||
|
@ -41,8 +41,8 @@ import {
|
||||
* - both of toolbars that are always visible (32px + 60px), and
|
||||
* - the 4px flex gap between the toolbars. */
|
||||
const MIN_WIDTH_PX = 200
|
||||
const MIN_HEIGHT_PX = 16
|
||||
const DEFAULT_HEIGHT_PX = 150
|
||||
const MIN_CONTENT_HEIGHT_PX = 32
|
||||
const DEFAULT_CONTENT_HEIGHT_PX = 150
|
||||
const TOP_WITH_TOOLBAR_PX = 72
|
||||
|
||||
// Used for testing.
|
||||
@ -236,24 +236,22 @@ watchEffect(async () => {
|
||||
|
||||
const isBelowToolbar = ref(false)
|
||||
|
||||
const toolbarHeight = computed(() => (isBelowToolbar.value ? TOP_WITH_TOOLBAR_PX : 0))
|
||||
|
||||
const rect = computed(
|
||||
() =>
|
||||
new Rect(
|
||||
props.nodePosition,
|
||||
new Vec2(
|
||||
Math.max(props.width ?? MIN_WIDTH_PX, props.nodeSize.x),
|
||||
Math.max(
|
||||
props.height ?? DEFAULT_HEIGHT_PX,
|
||||
(isBelowToolbar.value ? 0 : TOP_WITH_TOOLBAR_PX) + MIN_HEIGHT_PX,
|
||||
),
|
||||
Math.max(props.height ?? DEFAULT_CONTENT_HEIGHT_PX, MIN_CONTENT_HEIGHT_PX) +
|
||||
toolbarHeight.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
watchEffect(() => emit('update:rect', rect.value))
|
||||
onUnmounted(() => {
|
||||
emit('update:rect', undefined)
|
||||
})
|
||||
onUnmounted(() => emit('update:rect', undefined))
|
||||
|
||||
const allTypes = computed(() => Array.from(visualizationStore.types(props.typename)))
|
||||
|
||||
@ -277,7 +275,7 @@ provideVisualizationConfig({
|
||||
emit('update:width', value)
|
||||
},
|
||||
get height() {
|
||||
return rect.value.height
|
||||
return rect.value.height - toolbarHeight.value
|
||||
},
|
||||
set height(value) {
|
||||
emit('update:height', value)
|
||||
|
@ -95,4 +95,10 @@ const handler = {
|
||||
.bottom.left {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right,
|
||||
.bottom {
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,7 +3,7 @@ import ResizeHandles from '@/components/ResizeHandles.vue'
|
||||
import SmallPlusButton from '@/components/SmallPlusButton.vue'
|
||||
import SvgButton from '@/components/SvgButton.vue'
|
||||
import VisualizationSelector from '@/components/VisualizationSelector.vue'
|
||||
import { isTriggeredByKeyboard, useResizeObserver } from '@/composables/events'
|
||||
import { isTriggeredByKeyboard } from '@/composables/events'
|
||||
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
||||
import { Rect, type BoundsSet } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
@ -52,13 +52,13 @@ function hideSelector() {
|
||||
requestAnimationFrame(() => (isSelectorVisible.value = false))
|
||||
}
|
||||
|
||||
const realSize = useResizeObserver(contentNode)
|
||||
const contentSize = computed(() => new Vec2(config.width, config.height))
|
||||
|
||||
// Because ResizeHandles are applying the screen mouse movements, the bouds must be in `screen`
|
||||
// space.
|
||||
const clientBounds = computed({
|
||||
get() {
|
||||
return new Rect(Vec2.Zero, realSize.value.scale(config.scale))
|
||||
return new Rect(Vec2.Zero, contentSize.value.scale(config.scale))
|
||||
},
|
||||
set(value) {
|
||||
if (resizing.left || resizing.right) config.width = value.width / config.scale
|
||||
@ -68,9 +68,7 @@ const clientBounds = computed({
|
||||
|
||||
let resizing: BoundsSet = {}
|
||||
|
||||
// When dragging left resizer, we need to move node position accordingly. It may be done only by
|
||||
// reading the real width change, as `config.width` does not consider node's minimum width.
|
||||
watch(realSize, (newVal, oldVal) => {
|
||||
watch(contentSize, (newVal, oldVal) => {
|
||||
if (!resizing.left) return
|
||||
const delta = newVal.x - oldVal.x
|
||||
if (delta !== 0)
|
||||
@ -86,8 +84,8 @@ const nodeShortType = computed(() =>
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return {
|
||||
width: config.fullscreen ? undefined : `${Math.max(config.width ?? 0, config.nodeSize.x)}px`,
|
||||
height: config.fullscreen ? undefined : `${Math.max(config.height ?? 0, config.nodeSize.y)}px`,
|
||||
width: config.fullscreen ? undefined : `${config.width}px`,
|
||||
height: config.fullscreen ? undefined : `${config.height}px`,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -179,7 +179,7 @@ export function useGraphHover(isPortEnabled: (port: PortId) => boolean) {
|
||||
const hoveredNode = computed<NodeId | undefined>(() => {
|
||||
const element = hoveredElement.value?.closest('.GraphNode')
|
||||
if (!element) return undefined
|
||||
return dataAttribute<NodeId>(element, 'node-id')
|
||||
return dataAttribute<NodeId>(element, 'nodeId')
|
||||
})
|
||||
|
||||
return { hoveredNode, hoveredPort }
|
||||
|
@ -18,7 +18,7 @@ export interface VisualizationConfig {
|
||||
readonly isFocused: boolean
|
||||
readonly nodeType: string | undefined
|
||||
isBelowToolbar: boolean
|
||||
width: number | null
|
||||
width: number
|
||||
height: number
|
||||
nodePosition: Vec2
|
||||
fullscreen: boolean
|
||||
|
@ -84,6 +84,7 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
)
|
||||
|
||||
const nodeRects = reactive(new Map<NodeId, Rect>())
|
||||
const nodeHoverAnimations = reactive(new Map<NodeId, number>())
|
||||
const vizRects = reactive(new Map<NodeId, Rect>())
|
||||
// The currently visible nodes' areas (including visualization).
|
||||
const visibleNodeAreas = computed(() => {
|
||||
@ -246,22 +247,23 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
() => new Set(filterDefined([cbEditedEdge.value, mouseEditedEdge.value])),
|
||||
)
|
||||
|
||||
const disconnectedEdgeTargets = computed(() => {
|
||||
const targets = new Set<PortId>()
|
||||
const disconnectedEdgePorts = computed(() => {
|
||||
const ports = new Set<PortId>()
|
||||
for (const edge of unconnectedEdges.value) {
|
||||
if (edge.disconnectedEdgeTarget) targets.add(edge.disconnectedEdgeTarget)
|
||||
if (edge.disconnectedEdgeTarget) ports.add(edge.disconnectedEdgeTarget)
|
||||
if (edge.source) ports.add(edge.source)
|
||||
}
|
||||
if (editedNodeInfo.value) {
|
||||
const primarySubject = db.nodeIdToNode.get(editedNodeInfo.value.id)?.primarySubject
|
||||
if (primarySubject) targets.add(primarySubject)
|
||||
if (primarySubject) ports.add(primarySubject)
|
||||
}
|
||||
return targets
|
||||
return ports
|
||||
})
|
||||
|
||||
const connectedEdges = computed(() => {
|
||||
const edges = new Array<ConnectedEdge>()
|
||||
for (const [target, sources] of db.connections.allReverse()) {
|
||||
if (!disconnectedEdgeTargets.value.has(target)) {
|
||||
if (!disconnectedEdgePorts.value.has(target)) {
|
||||
for (const source of sources) {
|
||||
edges.push({ source, target })
|
||||
}
|
||||
@ -354,6 +356,7 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
const outerExpr = edit.getVersion(node.outerExpr)
|
||||
if (outerExpr) Ast.deleteFromParentBlock(outerExpr)
|
||||
nodeRects.delete(id)
|
||||
nodeHoverAnimations.delete(id)
|
||||
}
|
||||
},
|
||||
true,
|
||||
@ -464,6 +467,10 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
}
|
||||
}
|
||||
|
||||
function updateNodeHoverAnim(nodeId: NodeId, progress: number) {
|
||||
nodeHoverAnimations.set(nodeId, progress)
|
||||
}
|
||||
|
||||
const nodesToPlace = reactive<NodeId[]>([])
|
||||
const { place: placeNode } = usePlacement(visibleNodeAreas, Rect.Zero)
|
||||
|
||||
@ -726,10 +733,11 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
editedNodeInfo,
|
||||
mouseEditedEdge,
|
||||
cbEditedEdge,
|
||||
disconnectedEdgeTargets,
|
||||
disconnectedEdgePorts,
|
||||
connectedEdges,
|
||||
moduleSource,
|
||||
nodeRects,
|
||||
nodeHoverAnimations,
|
||||
vizRects,
|
||||
visibleNodeAreas,
|
||||
visibleArea,
|
||||
@ -752,6 +760,7 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
undoManager,
|
||||
topLevel,
|
||||
updateNodeRect,
|
||||
updateNodeHoverAnim,
|
||||
updateVizRect,
|
||||
addPortInstance,
|
||||
removePortInstance,
|
||||
|
@ -196,6 +196,11 @@ export class Rect {
|
||||
height: this.size.y,
|
||||
})
|
||||
}
|
||||
|
||||
expand(padding: number): Rect {
|
||||
const padVector = new Vec2(padding, padding)
|
||||
return new Rect(this.pos.sub(padVector), this.size.add(padVector).add(padVector))
|
||||
}
|
||||
}
|
||||
|
||||
Rect.Zero = new Rect(Vec2.Zero, Vec2.Zero)
|
||||
|
Loading…
Reference in New Issue
Block a user