fix(figma-api): size script text to its content, as Figma does (#950)
* fix(figma-api): size script text to its content, as Figma does figma.createText() made fixed 100px text at 14px, and the API never re-measured text, so scripts saw sizes Figma does not. New text is now empty 12px WIDTH_AND_HEIGHT text, one line tall; API updates re-measure auto-sizing text through the editor's textAutoResizeChanges; and resize() sets textAutoResize to NONE. Values were recorded in live Figma. * test(figma-api): hold recorded text sizes to the stated 1 px
This commit is contained in:
parent
974905289a
commit
0201feb35f
|
|
@ -132,6 +132,7 @@
|
|||
- Resize auto-layout frames that fill across their parent with it: one with a fixed size on that axis kept its old size. A filling child still counts toward a hugging parent's size, as in Figma.
|
||||
- Set `layoutSizingHorizontal` and `layoutSizingVertical` in scripts as Figma does: Fill is recorded on the child for that axis instead of on the frame's own sizing, which `.fig` export dropped, text switches its `textAutoResize`, and sizing Figma refuses, such as Hug on a frame without auto layout, throws Figma's error.
|
||||
- Read geometry in scripts after an edit without waiting for the script to finish: `x`, `y`, `width`, `height`, `relativeTransform`, `absoluteTransform`, `absoluteBoundingBox`, and `absoluteRenderBounds` lay out what the script changed first, as in Figma, so a hugging parent reports its new size right after a child is added.
|
||||
- Size text from scripts as Figma does: `figma.createText()` makes empty 12px text that sizes itself to its content instead of a fixed 100px box, auto-sizing text resizes when its characters, font, or size change, and `resize()` fixes its size.
|
||||
- Draw segmented controls in the properties panel at the height of the fields beside them.
|
||||
- Keep saving AI chat history in Safari Private Browsing after a message with an image or a reply that changed the document. Safari cannot store image data that way in a private window, so the conversation stopped saving from that point and showed "Chat history could not be saved".
|
||||
- Keep a component set's dashed border one pixel wide at every zoom; zooming in after opening a page scaled it into thick dashes until the page was redrawn.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
import type { GroupFitOptions, SceneGraph, SceneNode } from '@open-pencil/scene-graph'
|
||||
|
||||
import { assertNodeEditable } from '#core/editor/capabilities'
|
||||
import { textAutoResizeChanges } from '#core/editor/text/auto-resize'
|
||||
import type { NodeProxyHost } from '#core/figma-api/proxy'
|
||||
|
||||
export interface NodeProxyInternals {
|
||||
|
|
@ -77,6 +78,8 @@ export function updateNode(
|
|||
.map((key) => [key, Reflect.get(changes, key)])
|
||||
) as Partial<SceneNode>
|
||||
if (Object.keys(applied).length === 0) return
|
||||
// Auto-sizing text measures its new content, as the editor's updates do.
|
||||
Object.assign(applied, textAutoResizeChanges(g.getNode(id), applied))
|
||||
g.updateNode(id, applied)
|
||||
recordInstanceOverride(g, id, Object.keys(applied))
|
||||
if (Object.keys(applied).some((key) => GEOMETRY_FIELDS.has(key))) {
|
||||
|
|
|
|||
|
|
@ -212,8 +212,20 @@ export function installBasicNodeProxyAccessors(
|
|||
const scene = graph(this, internals)
|
||||
const node = raw(this, internals)
|
||||
const before = containerTransform(node, scene)
|
||||
scene.updateNode(node.id, { width, height })
|
||||
recordInstanceOverride(scene, node.id, ['width', 'height'])
|
||||
// Text given a size stops sizing itself to its content, as in Figma.
|
||||
const fixesText =
|
||||
node.type === 'TEXT' &&
|
||||
(node.textAutoResize === 'WIDTH_AND_HEIGHT' || node.textAutoResize === 'HEIGHT')
|
||||
scene.updateNode(node.id, {
|
||||
width,
|
||||
height,
|
||||
...(fixesText ? { textAutoResize: 'NONE' as const } : {})
|
||||
})
|
||||
recordInstanceOverride(scene, node.id, [
|
||||
'width',
|
||||
'height',
|
||||
...(fixesText ? ['textAutoResize'] : [])
|
||||
])
|
||||
setTransform(this, internals, () => before)
|
||||
},
|
||||
resizeWithoutConstraints(this: ProxyThis, width: number, height: number): void {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { newLayerDefaults } from '#core/editor/shapes/defaults'
|
|||
import { booleanOperationPaints, createBooleanOperation } from '#core/editor/structure/boolean'
|
||||
import { wrapNodes } from '#core/editor/structure/container-wrap'
|
||||
import { ungroupNode } from '#core/editor/structure/group'
|
||||
import { textAutoResizeChanges } from '#core/editor/text/auto-resize'
|
||||
import { setDefaultPageBackground } from '#core/figma-api/page-backgrounds'
|
||||
import type { RasterCodec } from '#core/io/formats/raster'
|
||||
import { reconcileVariableLayouts } from '#core/layout/variables'
|
||||
|
|
@ -187,7 +188,13 @@ export class FigmaAPI implements NodeProxyHost {
|
|||
}
|
||||
|
||||
createText(): FigmaTextNode {
|
||||
return this._createNode('TEXT') as FigmaTextNode
|
||||
const text = this._createNode('TEXT') as FigmaTextNode
|
||||
// Figma's plugin text starts empty at 12px and sizes itself to its content: no width, one
|
||||
// line tall.
|
||||
const node = this.graph.getNode(text.id)
|
||||
const defaults = { fontSize: 12, textAutoResize: 'WIDTH_AND_HEIGHT' as const, width: 0 }
|
||||
this.graph.updateNode(text.id, { ...defaults, ...textAutoResizeChanges(node, defaults) })
|
||||
return text
|
||||
}
|
||||
|
||||
createLine(): FigmaLineNode {
|
||||
|
|
|
|||
61
packages/core/tests/figma-api/text-autosize.test.ts
Normal file
61
packages/core/tests/figma-api/text-autosize.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { beforeAll, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { expectDefined } from '#core-tests/helpers/assert'
|
||||
import { fontManager } from '#core/text/fonts'
|
||||
|
||||
import { FigmaAPI } from '@open-pencil/core/figma-api'
|
||||
import { SceneGraph } from '@open-pencil/scene-graph'
|
||||
|
||||
// Recorded with the same script in live Figma on 2026-10-07, Inter Regular. Line metrics round
|
||||
// differently by up to a pixel.
|
||||
|
||||
beforeAll(async () => {
|
||||
fontManager.markLoaded(
|
||||
'Inter',
|
||||
'Regular',
|
||||
expectDefined(await fontManager.fetchBundledFont('/Inter-Regular.ttf'), 'bundled Inter font')
|
||||
)
|
||||
})
|
||||
|
||||
function text() {
|
||||
const figma = new FigmaAPI(new SceneGraph())
|
||||
return figma.createText() as ReturnType<FigmaAPI['createText']> & TextNode
|
||||
}
|
||||
|
||||
function size(node: { width: number; height: number }) {
|
||||
return [node.width, node.height]
|
||||
}
|
||||
|
||||
describe('plugin text sizing', () => {
|
||||
test('new text is empty, 12px, and sizes itself to its content', () => {
|
||||
const node = text()
|
||||
expect(node.textAutoResize).toBe('WIDTH_AND_HEIGHT')
|
||||
expect(node.fontSize).toBe(12)
|
||||
expect(size(node)).toEqual([0, 15])
|
||||
|
||||
node.characters = 'Hello'
|
||||
expect(size(node)).toEqual([29, 15])
|
||||
node.characters = 'Hello world, a longer line of text'
|
||||
expect(Math.abs(node.width - 180)).toBeLessThanOrEqual(1)
|
||||
node.characters = 'Hello'
|
||||
node.fontSize = 24
|
||||
expect(node.width).toBe(58)
|
||||
expect(Math.abs(node.height - 29)).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('resizing text fixes its size', () => {
|
||||
const node = text()
|
||||
// Read through a call, so the assignment below does not narrow what TypeScript expects.
|
||||
const mode = (): string => node.textAutoResize
|
||||
node.characters = 'Hello'
|
||||
node.resize(200, 50)
|
||||
expect(mode()).toBe('NONE')
|
||||
expect(size(node)).toEqual([200, 50])
|
||||
|
||||
node.textAutoResize = 'HEIGHT'
|
||||
node.resize(100, 10)
|
||||
expect(mode()).toBe('NONE')
|
||||
node.characters = 'Hello there, this wraps across lines'
|
||||
expect(size(node)).toEqual([100, 10])
|
||||
})
|
||||
})
|
||||
|
|
@ -46,6 +46,8 @@ describe('Figma Plugin API layout compatibility', () => {
|
|||
frame.appendChild(text)
|
||||
text.x = 11
|
||||
text.y = 13
|
||||
const textWidth = graph.getNode(text.id)?.width ?? 0
|
||||
expect(textWidth).toBeGreaterThan(0)
|
||||
|
||||
frame.rescale(2)
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ describe('Figma Plugin API layout compatibility', () => {
|
|||
expect(text.fontSize).toBe(20)
|
||||
expect(text.letterSpacing).toBe(4)
|
||||
expect(text.lineHeight).toBe(28)
|
||||
expect(graph.getNode(text.id)?.width).toBe(200)
|
||||
expect(graph.getNode(text.id)?.width).toBe(textWidth * 2)
|
||||
})
|
||||
|
||||
test('rescale rejects occurrence descendants without mutation but permits definition children', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue