Overhaul node computeSize - use actual text width (#962)

### Node resize overhaul

- Precisely calculates node minimum size
- Prevents input & output overlap
- Prevents (normal*) widgets from rendering text over the edge of nodes
- Performance impact was sub-millisecond for normal usage in a 500-node
graph


![image](https://github.com/user-attachments/assets/5b6a6cc7-a752-4d7e-bcdf-b4bc8df26c51)

_Minimum size for a few example node configurations_

### Widgets

- Converts hard-coded draw render values to class static properties
- Adds widget button draw function for left/right arrow widgets

_*_ Exception: `control_after_generate`, as it is not a true input /
widget. A check may be added later to handle this special case.
This commit is contained in:
filtered
2025-04-25 00:31:34 +10:00
committed by GitHub
parent 79ce3199d4
commit 080a8da8f4
10 changed files with 97 additions and 52 deletions

View File

@@ -72,6 +72,7 @@ import {
import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange"
import { findFirstNode, getAllNestedItems } from "./utils/collections"
import { toClass } from "./utils/type"
import { BaseWidget } from "./widgets/BaseWidget"
import { WIDGET_TYPE_MAP } from "./widgets/widgetMap"
interface IShowSearchOptions {
@@ -240,6 +241,11 @@ export class LGraphCanvas implements ConnectionColorContext {
black: { color: "#222", bgcolor: "#000", groupcolor: "#444" },
}
/**
* @internal Exclusively a workaround for design limitation in {@link LGraphNode.computeSize}.
*/
static _measureText?: (text: string, fontStyle?: string) => number
/**
* The state of this canvas, e.g. whether it is being dragged, or read-only.
*
@@ -742,6 +748,17 @@ export class LGraphCanvas implements ConnectionColorContext {
this.setCanvas(canvas, options.skip_events)
this.clear()
LGraphCanvas._measureText = (text: string, fontStyle = this.inner_text_font) => {
const { ctx } = this
const { font } = ctx
try {
ctx.font = fontStyle
return ctx.measureText(text).width
} finally {
ctx.font = font
}
}
if (!options.skip_render) {
this.startRendering()
}
@@ -4041,7 +4058,7 @@ export class LGraphCanvas implements ConnectionColorContext {
} else {
// Regular widget, probably
ctx.roundRect(
nodeX + 15,
nodeX + BaseWidget.margin,
nodeY + overWidget.y,
overWidget.width ?? area[2],
height,

View File

@@ -45,6 +45,7 @@ import {
import { findFreeSlotOfType } from "./utils/collections"
import { distributeSpace } from "./utils/spaceDistribution"
import { toClass } from "./utils/type"
import { BaseWidget } from "./widgets/BaseWidget"
import { WIDGET_TYPE_MAP } from "./widgets/widgetMap"
// #region Types
@@ -1529,7 +1530,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
const ctorSize = this.constructor.size
if (ctorSize) return [ctorSize[0], ctorSize[1]]
const { inputs, outputs } = this
const { inputs, outputs, widgets } = this
let rows = Math.max(
inputs ? inputs.filter(input => !isWidgetInputSlot(input)).length : 1,
outputs ? outputs.length : 1,
@@ -1539,45 +1540,66 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
// although it should be graphcanvas.inner_text_font size
const font_size = LiteGraph.NODE_TEXT_SIZE
const title_width = compute_text_size(this.title)
const padLeft = LiteGraph.NODE_TITLE_HEIGHT
const padRight = padLeft * 0.33
const title_width = padLeft + compute_text_size(this.title, this.titleFontStyle) + padRight
let input_width = 0
let widgetWidth = 0
let output_width = 0
if (inputs) {
for (const input of inputs) {
const text = input.label || input.localized_name || input.name || ""
const text_width = compute_text_size(text)
if (input_width < text_width)
input_width = text_width
const text_width = compute_text_size(text, this.innerFontStyle)
if (isWidgetInputSlot(input)) {
const widget = this.getWidgetFromSlot(input)
if (widget && !this.isWidgetVisible(widget)) continue
if (text_width > widgetWidth) widgetWidth = text_width
} else {
if (text_width > input_width) input_width = text_width
}
}
}
if (outputs) {
for (const output of outputs) {
const text = output.label || output.localized_name || output.name || ""
const text_width = compute_text_size(text)
const text_width = compute_text_size(text, this.innerFontStyle)
if (output_width < text_width)
output_width = text_width
}
}
size[0] = Math.max(input_width + output_width + 10, title_width)
size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH)
if (this.widgets?.length)
size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5)
const minWidth = LiteGraph.NODE_WIDTH * (widgets?.length ? 1.5 : 1)
// Text + slot width + centre padding
const centrePadding = input_width && output_width ? 5 : 0
const slotsWidth = input_width + output_width + (2 * LiteGraph.NODE_SLOT_HEIGHT) + centrePadding
// Total distance from edge of node to the inner edge of the widget 'previous' arrow button
const widgetMargin = BaseWidget.margin + BaseWidget.arrowMargin + BaseWidget.arrowWidth
const widgetPadding = BaseWidget.minValueWidth + (2 * widgetMargin)
if (widgetWidth) widgetWidth += widgetPadding
size[0] = Math.max(slotsWidth, widgetWidth, title_width, minWidth)
size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT
// Get widget height & expand size if necessary
let widgets_height = 0
if (this.widgets?.length) {
for (const widget of this.widgets) {
if (widgets?.length) {
for (const widget of widgets) {
if (!this.isWidgetVisible(widget)) continue
let widget_height = 0
if (widget.computeSize) {
widget_height += widget.computeSize(size[0])[1]
} else if (widget.computeLayoutSize) {
widget_height += widget.computeLayoutSize(this).minHeight
// Expand widget width if necessary
const { minHeight, minWidth } = widget.computeLayoutSize(this)
const widgetWidth = minWidth + widgetPadding
if (widgetWidth > size[0]) size[0] = widgetWidth
widget_height += minHeight
} else {
widget_height += LiteGraph.NODE_WIDGET_HEIGHT
}
@@ -1594,10 +1616,9 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
else
size[1] += widgets_height
function compute_text_size(text: string) {
return text
? font_size * text.length * 0.6
: 0
function compute_text_size(text: string, fontStyle: string) {
return LGraphCanvas._measureText?.(text, fontStyle) ??
font_size * (text?.length ?? 0) * 0.6
}
if (this.constructor.min_height && size[1] < this.constructor.min_height) {
@@ -3375,7 +3396,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
const show_text = !lowQuality
ctx.save()
ctx.globalAlpha = editorAlpha
const margin = 15
const { margin } = BaseWidget
for (const w of widgets) {
if (!this.isWidgetVisible(w)) continue

View File

@@ -13,6 +13,15 @@ export interface DrawWidgetOptions {
}
export abstract class BaseWidget implements IBaseWidget {
/** From node edge to widget edge */
static margin = 15
/** From widget edge to tip of arrow button */
static arrowMargin = 6
/** Arrow button width */
static arrowWidth = 10
/** Absolute minimum display width of widget values */
static minValueWidth = 42
linkedWidgets?: IWidget[]
name: string
options: IWidgetOptions<unknown>
@@ -82,6 +91,28 @@ export abstract class BaseWidget implements IBaseWidget {
*/
abstract drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void
drawArrowButtons(ctx: CanvasRenderingContext2D, margin: number, y: number, width: number) {
const { height } = this
const { arrowMargin, arrowWidth } = BaseWidget
const arrowTipX = margin + arrowMargin
const arrowInnerX = arrowTipX + arrowWidth
// Draw left arrow
ctx.fillStyle = this.text_color
ctx.beginPath()
ctx.moveTo(arrowInnerX, y + 5)
ctx.lineTo(arrowTipX, y + height * 0.5)
ctx.lineTo(arrowInnerX, y + height - 5)
ctx.fill()
// Draw right arrow
ctx.beginPath()
ctx.moveTo(width - arrowInnerX, y + 5)
ctx.lineTo(width - arrowTipX, y + height * 0.5)
ctx.lineTo(width - arrowInnerX, y + height - 5)
ctx.fill()
}
/**
* Handles the click event for the widget
* @param options The options for handling the click event

View File

@@ -20,7 +20,7 @@ export class BooleanWidget extends BaseWidget implements IBooleanWidget {
y,
width,
show_text = true,
margin = 15,
margin = BaseWidget.margin,
}: DrawWidgetOptions) {
const { height } = this

View File

@@ -27,7 +27,7 @@ export class ButtonWidget extends BaseWidget implements IButtonWidget {
y,
width,
show_text = true,
margin = 15,
margin = BaseWidget.margin,
}: DrawWidgetOptions) {
// Store original context attributes
const originalTextAlign = ctx.textAlign

View File

@@ -28,7 +28,7 @@ export class ComboWidget extends BaseWidget implements IComboWidget {
y,
width,
show_text = true,
margin = 15,
margin = BaseWidget.margin,
}: DrawWidgetOptions) {
// Store original context attributes
const originalTextAlign = ctx.textAlign
@@ -51,19 +51,7 @@ export class ComboWidget extends BaseWidget implements IComboWidget {
if (show_text) {
if (!this.computedDisabled) {
ctx.stroke()
// Draw left arrow
ctx.fillStyle = this.text_color
ctx.beginPath()
ctx.moveTo(margin + 16, y + 5)
ctx.lineTo(margin + 6, y + height * 0.5)
ctx.lineTo(margin + 16, y + height - 5)
ctx.fill()
// Draw right arrow
ctx.beginPath()
ctx.moveTo(width - margin - 16, y + 5)
ctx.lineTo(width - margin - 6, y + height * 0.5)
ctx.lineTo(width - margin - 16, y + height - 5)
ctx.fill()
this.drawArrowButtons(ctx, margin, y, width)
}
// Draw label

View File

@@ -49,7 +49,7 @@ export class KnobWidget extends BaseWidget implements IKnobWidget {
y,
width,
show_text = true,
margin = 15,
margin = BaseWidget.margin,
}: DrawWidgetOptions,
): void {
// Store original context attributes

View File

@@ -43,7 +43,7 @@ export class NumberWidget extends BaseWidget implements INumericWidget {
y,
width,
show_text = true,
margin = 15,
margin = BaseWidget.margin,
}: DrawWidgetOptions) {
// Store original context attributes
const originalTextAlign = ctx.textAlign
@@ -66,19 +66,7 @@ export class NumberWidget extends BaseWidget implements INumericWidget {
if (show_text) {
if (!this.computedDisabled) {
ctx.stroke()
// Draw left arrow
ctx.fillStyle = this.text_color
ctx.beginPath()
ctx.moveTo(margin + 16, y + 5)
ctx.lineTo(margin + 6, y + height * 0.5)
ctx.lineTo(margin + 16, y + height - 5)
ctx.fill()
// Draw right arrow
ctx.beginPath()
ctx.moveTo(width - margin - 16, y + 5)
ctx.lineTo(width - margin - 6, y + height * 0.5)
ctx.lineTo(width - margin - 16, y + height - 5)
ctx.fill()
this.drawArrowButtons(ctx, margin, y, width)
}
// Draw label

View File

@@ -31,7 +31,7 @@ export class SliderWidget extends BaseWidget implements ISliderWidget {
y,
width,
show_text = true,
margin = 15,
margin = BaseWidget.margin,
}: DrawWidgetOptions) {
// Store original context attributes
const originalTextAlign = ctx.textAlign

View File

@@ -26,7 +26,7 @@ export class TextWidget extends BaseWidget implements IStringWidget {
y,
width,
show_text = true,
margin = 15,
margin = BaseWidget.margin,
}: DrawWidgetOptions) {
// Store original context attributes
const originalTextAlign = ctx.textAlign