@@ -29,6 +29,7 @@ import GraphNodeMessage from '@/components/GraphEditor/GraphNodeMessage.vue'
2929import GraphNodeSubmenu from ' @/components/GraphEditor/GraphNodeSubmenu.vue'
3030import GraphVisualization from ' @/components/GraphEditor/GraphVisualization.vue'
3131import type { NodeCreationOptions } from ' @/components/GraphEditor/nodeCreation'
32+ import { useNodesDisplacing } from ' @/components/GraphEditor/nodesDisplacing'
3233import { useResizeHandles } from ' @/components/resizeHandles'
3334import ResizeHandles from ' @/components/ResizeHandles.vue'
3435import SvgIcon from ' @/components/SvgIcon.vue'
@@ -90,6 +91,8 @@ const nodeExecution = useNodeExecution()
9091const nodeId = computed (() => asNodeId (props .node .rootExpr .externalId ))
9192const primaryApplication = computed (() => props .node .primaryApplication )
9293
94+ const scale = computed (() => navigator ?.scale ?? 1 )
95+
9396const nodePosition = computed (() => {
9497 // Positions of nodes that are not yet placed are set to `Infinity`.
9598 if (props .node .position .equals (Vec2 .Infinity )) return Vec2 .Zero
@@ -99,8 +102,25 @@ const nodePosition = computed(() => {
99102onUnmounted (() => graph .unregisterNodeRect (nodeId .value ))
100103
101104const rootNode = ref <HTMLElement >()
102- const contentNode = ref <HTMLElement >()
103- const nodeSize = useResizeObserver (rootNode )
105+ const widgetTreeNode = ref <HTMLElement >()
106+
107+ const widgetsDomSizeClientPx = useResizeObserver (widgetTreeNode , false )
108+ const widgetsDomSize = ref (new Vec2 (0 , 0 ))
109+ // Maintain the size in scene px. The values reported by the resize observer are in client px, so they are dependent on
110+ // the scale; however, changes to the scale don't cause resize events--so the resize observer is non-reactively (via the
111+ // DOM) dependent on reactive state. Thus, we must correct for the scale by non-reactively sampling it at the time a
112+ // resize is observed.
113+ watch (widgetsDomSizeClientPx , (size ) => (widgetsDomSize .value = size .scale (1 / scale .value )), {
114+ immediate: true ,
115+ flush: ' sync' ,
116+ })
117+ // Compute the node's natural size based on the size of its widgets. We measure the widget tree instead of the node
118+ // directly, because measuring the node would cause a cycle:
119+ // - This value is used as in input to determine the size of the visualization.
120+ // - The size of the visualization affects the size of the node.
121+ const nodeDomSize = computed (() =>
122+ widgetsDomSize .value .add (new Vec2 (NODE_CONTENT_PADDING * 2 , NODE_CONTENT_PADDING * 2 )),
123+ )
104124
105125providePopoverRoot (rootNode )
106126
@@ -180,25 +200,25 @@ function ensureSelected() {
180200
181201const outputHovered = computed (() => graph .nodeOutputHovered .get (nodeId .value ))
182202
183- const scale = computed (() => navigator ?.scale ?? 1 )
184- const nodeRect = computed (() => new Rect (props .node .position , nodeSize .value ))
185-
203+ const { displaceNodesForResize } = useNodesDisplacing ()
186204const {
187205 visualizationWidth,
188206 isVisualizationEnabled,
189207 isVisualizationPreviewed,
190- visRect ,
208+ vizHeight ,
191209 visualization,
192210} = useNodeVisualization ({
193211 vis : () => props .node .vis ,
194212 nodeHovered : () => nodeHovered .value || outputHovered .value ,
195- nodeRect ,
213+ nodeWidgetsSize: nodeDomSize ,
214+ nodePos : () => props .node .position ,
196215 scale ,
197216 isFocused: detailedView ,
198217 typeinfo : () => expressionInfo .value ?.typeInfo ,
199218 dataSource : () => ({ type: ' node' , nodeId: props .node .rootExpr .externalId }) as const ,
200219 hidden: toRef (props , ' edited' ),
201220 emit ,
221+ onResize : (rect0 , rect1 ) => displaceNodesForResize (nodeId .value , rect0 , rect1 ),
202222})
203223
204224watch (isVisualizationPreviewed , (newVal ) => {
@@ -248,15 +268,15 @@ const nodeEditHandler = nodeEditBindings.handler({
248268 edit : () => actionHandlers [' component.startEditing' ].action (),
249269})
250270
251- // / The visualization's contribution to the node's height.
252- const vizBelowNode = computed (() => (visRect .value ? visRect .value .size .y - nodeSize .value .y : 0 ))
253-
254- const nodeOuterRect = ref <Rect >()
271+ let prevNodeRect: Rect | undefined = undefined
255272watchEffect (() => {
256- const newValue = visRect .value ?? nodeRect .value
257- if (! newValue .size .isZero () && ! nodeOuterRect .value ?.equals (newValue )) {
258- nodeOuterRect .value = newValue
259- emit (' update:rect' , newValue )
273+ if (nodeDomSize .value .isZero ()) return
274+ const width = Math .max (nodeDomSize .value .x , visualizationWidth .value )
275+ const height = nodeDomSize .value .y + vizHeight .value
276+ const newRect = new Rect (props .node .position , new Vec2 (width , height ))
277+ if (! prevNodeRect ?.equals (newRect )) {
278+ emit (' update:rect' , newRect )
279+ prevNodeRect = newRect
260280 }
261281})
262282
@@ -286,15 +306,16 @@ function useRecomputation() {
286306 * takes the size of the largest resizable widget present. If the user resizes the node, and the node is in expanded
287307 * mode, the specified height overrides any widget preferences.
288308 */
289- const nodeHeight = computed (() => props .node .height )
309+ const nodeHeightOverride = computed (() => props .node .height )
310+ const nodeHeightOverridden = computed (() => props .node .height != null )
290311const nodeStyle = computed (() => {
291312 return {
292313 transform: transform .value ,
293- minWidth: isVisualizationEnabled . value ? ` ${visualizationWidth .value ?? 200 }px ` : undefined ,
294- height: nodeHeight .value ? ` ${nodeHeight .value }px ` : undefined ,
314+ minWidth: ` ${visualizationWidth .value ?? 200 }px ` ,
315+ height: nodeHeightOverride .value ? ` ${nodeHeightOverride .value }px ` : undefined ,
295316 ' --node-group-color' : baseColor .value ,
296317 ... (props .node .zIndex ? { ' z-index' : props .node .zIndex } : {}),
297- ' --viz-below-node' : ` ${vizBelowNode .value }px ` ,
318+ ' --viz-below-node' : ` ${vizHeight .value }px ` ,
298319 }
299320})
300321
@@ -310,7 +331,7 @@ const { progressAnimating, backgroundProgressEvents } = watchProgress()
310331
311332const showProgressBar = computed (() => nodeProgress .value !== 100 || progressAnimating .value )
312333
313- const nodeClass = computed (() => {
334+ const nodeClass = computed < Record < string , boolean >> (() => {
314335 return {
315336 selected: selected .value ,
316337 pending: pending .value ,
@@ -320,7 +341,7 @@ const nodeClass = computed(() => {
320341 menuVisible: menuVisible .value ,
321342 menuFull: menuFull .value ,
322343 edited: props .edited ,
323- nodeHeightOverridden: nodeHeight .value != null ,
344+ nodeHeightOverridden: nodeHeightOverridden .value ,
324345 }
325346})
326347
@@ -462,7 +483,7 @@ const nodeName = computed(() => props.node.pattern?.code())
462483// === Node resizing ===
463484
464485const resizeHandles = useResizeHandles ({
465- size: nodeSize ,
486+ size: nodeDomSize ,
466487 scale ,
467488})
468489resizeHandles .onResizeHeight ((value ) => emit (' update:height' , value ))
@@ -517,7 +538,6 @@ resizeHandles.onResizeHeight((value) => emit('update:height', value))
517538 </div >
518539 </template >
519540 <div
520- ref =" contentNode"
521541 :class =" { content: true, dragged: isDragged }"
522542 :style =" contentNodeStyle"
523543 v-on =" pointerEvents"
@@ -526,6 +546,7 @@ resizeHandles.onResizeHeight((value) => emit('update:height', value))
526546 @pointermove =" updateNodeHover"
527547 >
528548 <ComponentWidgetTree
549+ ref="widgetTreeNode"
529550 :ast =" props .node .innerExpr "
530551 :nodeId =" nodeId "
531552 :rootElement =" rootNode "
0 commit comments