Estimate text width for WIDTH_AND_HEIGHT auto-resize in layout

Text nodes with textAutoResize=WIDTH_AND_HEIGHT now use an estimated
content width instead of the node's current width when computing
auto-layout. Fixes month headers (and similar centered text in
auto-layout) appearing left-aligned after paste.
This commit is contained in:
Danila Poyarkov 2026-03-04 00:07:45 +03:00
parent 9eaab86733
commit d7e2015967
5 changed files with 42 additions and 9 deletions

View file

@ -48,7 +48,8 @@ export { FigmaAPI, FigmaNodeProxy, type FigmaFontName } from './figma-api'
export { ALL_TOOLS, defineTool, toolsToAI } from './tools'
export type { ToolDef, ParamDef, ParamType } from './tools'
export { SkiaRenderer, type RenderOverlays } from './renderer'
export { computeLayout, computeAllLayouts } from './layout'
export { computeLayout, computeAllLayouts, setTextMeasurer } from './layout'
export type { TextMeasurer } from './layout'
export { getCanvasKit, getGpuBackend, type CanvasKitOptions, type GpuBackend } from './canvaskit'
export {
loadFont,

View file

@ -11,6 +11,14 @@ import Yoga, {
import type { SceneGraph, SceneNode } from './scene-graph'
export type TextMeasurer = (node: SceneNode) => { width: number; height: number } | null
let globalTextMeasurer: TextMeasurer | null = null
export function setTextMeasurer(measurer: TextMeasurer | null): void {
globalTextMeasurer = measurer
}
export function computeLayout(graph: SceneGraph, frameId: string): void {
const frame = graph.getNode(frameId)
if (!frame || frame.layoutMode === 'NONE') return
@ -155,19 +163,25 @@ function configureChildAsLeaf(yogaChild: YogaNode, child: SceneNode, parent: Sce
const isRow = parent.layoutMode === 'HORIZONTAL'
const stretchCross = child.layoutAlignSelf === 'STRETCH' || parent.counterAxisAlign === 'STRETCH'
const measured = child.type === 'TEXT' && child.textAutoResize === 'WIDTH_AND_HEIGHT'
? measureTextSize(child)
: null
const w = measured ? measured.width : child.width
const h = child.height
if (child.layoutGrow > 0) {
yogaChild.setFlexGrow(child.layoutGrow)
if (!stretchCross) {
if (isRow) yogaChild.setHeight(child.height)
else yogaChild.setWidth(child.width)
if (isRow) yogaChild.setHeight(h)
else yogaChild.setWidth(w)
}
} else {
if (isRow) {
yogaChild.setWidth(child.width)
if (!stretchCross) yogaChild.setHeight(child.height)
yogaChild.setWidth(w)
if (!stretchCross) yogaChild.setHeight(h)
} else {
yogaChild.setHeight(child.height)
if (!stretchCross) yogaChild.setWidth(child.width)
yogaChild.setHeight(h)
if (!stretchCross) yogaChild.setWidth(w)
}
}
@ -176,6 +190,11 @@ function configureChildAsLeaf(yogaChild: YogaNode, child: SceneNode, parent: Sce
}
}
function measureTextSize(node: SceneNode): { width: number; height: number } | null {
if (!globalTextMeasurer) return null
return globalTextMeasurer(node)
}
function setSizing(
yogaNode: YogaNode,
axis: 'width' | 'height',

View file

@ -1963,6 +1963,18 @@ export class SkiaRenderer {
}
}
measureTextNode(node: SceneNode): { width: number; height: number } | null {
if (!this.fontsLoaded || !this.fontProvider || !this.isNodeFontLoaded(node)) return null
if (node.type !== 'TEXT' || !node.text) return null
const paragraph = this.buildParagraph(node)
paragraph.layout(node.textAutoResize === 'WIDTH_AND_HEIGHT' ? 1e6 : node.width || 1e6)
const width = paragraph.getLongestLine()
const height = paragraph.getHeight()
paragraph.delete()
return { width: Math.ceil(width), height: Math.ceil(height) }
}
isNodeFontLoaded(node: SceneNode): boolean {
const families = new Set<string>()
families.add(node.fontFamily || DEFAULT_FONT_FAMILY)

View file

@ -1 +1 @@
export { computeLayout, computeAllLayouts } from '@open-pencil/core'
export { computeLayout, computeAllLayouts, setTextMeasurer } from '@open-pencil/core'

View file

@ -22,7 +22,7 @@ import {
prefetchFigmaSchema
} from '@/engine/clipboard'
import { exportFigFile } from '@/engine/fig-export'
import { computeLayout, computeAllLayouts } from '@/engine/layout'
import { computeLayout, computeAllLayouts, setTextMeasurer } from '@/engine/layout'
import { renderNodesToImage } from '@/engine/render-image'
import { SceneGraph } from '@/engine/scene-graph'
import { TextEditor } from '@/engine/text-editor'
@ -603,6 +603,7 @@ export function createEditorStore() {
_ck = ck
_renderer = renderer
_textEditor = new TextEditor(ck)
setTextMeasurer((node) => renderer.measureTextNode(node))
}
function buildFigFile() {