fix(fig): render imported text from derived glyphs
This commit is contained in:
parent
7e86d57378
commit
142a973193
|
|
@ -8,6 +8,7 @@ import { vectorNetworkToCenterlinePath } from '#core/vector'
|
|||
import { renderBooleanOperation } from './boolean'
|
||||
import { nodeHasRadius } from './shapes'
|
||||
import type { SkiaRenderer, RenderOverlays } from './renderer'
|
||||
import { drawFigmaDerivedText } from './text-derived'
|
||||
|
||||
function drawVisibleFills(
|
||||
r: SkiaRenderer,
|
||||
|
|
@ -575,6 +576,10 @@ export function renderText(r: SkiaRenderer, canvas: Canvas, node: SceneNode, fil
|
|||
return
|
||||
}
|
||||
}
|
||||
if (drawFigmaDerivedText(r, canvas, node)) {
|
||||
canvas.restore()
|
||||
return
|
||||
}
|
||||
if (r.fontsLoaded && r.fontProvider) {
|
||||
const paragraph = r.buildParagraph(node, r.fillPaint.getColor())
|
||||
canvas.drawParagraph(paragraph, 0, paragraphY)
|
||||
|
|
|
|||
25
packages/core/src/canvas/text-derived.ts
Normal file
25
packages/core/src/canvas/text-derived.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Canvas } from 'canvaskit-wasm'
|
||||
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
import { geometryBlobToPath } from '#core/vector'
|
||||
|
||||
import type { SkiaRenderer } from './renderer'
|
||||
|
||||
export function drawFigmaDerivedText(
|
||||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
node: SceneNode
|
||||
): boolean {
|
||||
if (!node.figmaDerivedTextGlyphs?.length) return false
|
||||
|
||||
for (const glyph of node.figmaDerivedTextGlyphs) {
|
||||
const path = geometryBlobToPath(r.ck, glyph.commandsBlob, 'NONZERO')
|
||||
canvas.save()
|
||||
canvas.translate(glyph.x, glyph.y)
|
||||
canvas.scale(glyph.fontSize, -glyph.fontSize)
|
||||
canvas.drawPath(path, r.fillPaint)
|
||||
canvas.restore()
|
||||
path.delete()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -258,8 +258,24 @@ function importedTextLineHeight(nc: NodeChange): number | null {
|
|||
return convertLineHeight(nc.lineHeight, nc.fontSize)
|
||||
}
|
||||
|
||||
function convertFigmaDerivedTextGlyphs(nc: NodeChange, blobs: Uint8Array[]) {
|
||||
return (nc.derivedTextData?.glyphs ?? [])
|
||||
.map((glyph) => {
|
||||
const blob = glyph.commandsBlob === undefined ? undefined : blobs[glyph.commandsBlob]
|
||||
if (!blob) return null
|
||||
return {
|
||||
commandsBlob: blob,
|
||||
x: glyph.position.x,
|
||||
y: glyph.position.y,
|
||||
fontSize: glyph.fontSize
|
||||
}
|
||||
})
|
||||
.filter((glyph): glyph is NonNullable<typeof glyph> => !!glyph)
|
||||
}
|
||||
|
||||
function convertTextProps(
|
||||
nc: NodeChange
|
||||
nc: NodeChange,
|
||||
blobs: Uint8Array[]
|
||||
): Pick<
|
||||
SceneNode,
|
||||
| 'text'
|
||||
|
|
@ -278,6 +294,7 @@ function convertTextProps(
|
|||
| 'styleRuns'
|
||||
| 'textTruncation'
|
||||
| 'textDirection'
|
||||
| 'figmaDerivedTextGlyphs'
|
||||
> {
|
||||
return {
|
||||
text: nc.textData?.characters ?? '',
|
||||
|
|
@ -302,7 +319,8 @@ function convertTextProps(
|
|||
textDirection:
|
||||
(getOpenPencilPluginValue(nc, TEXT_DIRECTION_PLUGIN_KEY) as
|
||||
| SceneNode['textDirection']
|
||||
| null) || 'AUTO'
|
||||
| null) || 'AUTO',
|
||||
figmaDerivedTextGlyphs: convertFigmaDerivedTextGlyphs(nc, blobs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -426,7 +444,7 @@ export function nodeChangeToProps(
|
|||
),
|
||||
effects: convertEffects(nc.effects),
|
||||
...convertCornerProps(nc),
|
||||
...convertTextProps(nc),
|
||||
...convertTextProps(nc, blobs),
|
||||
horizontalConstraint: mapConstraint(nc.horizontalConstraint as string),
|
||||
verticalConstraint: mapConstraint(nc.verticalConstraint as string),
|
||||
...convertLayoutProps(nc),
|
||||
|
|
|
|||
|
|
@ -397,12 +397,10 @@ export class SceneGraph {
|
|||
set.add(id)
|
||||
}
|
||||
}
|
||||
if (
|
||||
node.type === 'TEXT' &&
|
||||
node.textPicture &&
|
||||
Object.keys(changes).some((k) => SceneGraph.TEXT_PICTURE_KEYS.has(k))
|
||||
) {
|
||||
node.textPicture = null
|
||||
if (node.type === 'TEXT') {
|
||||
const textChanged = Object.keys(changes).some((k) => SceneGraph.TEXT_PICTURE_KEYS.has(k))
|
||||
if (node.textPicture && textChanged) node.textPicture = null
|
||||
if (node.figmaDerivedTextGlyphs && 'text' in changes) node.figmaDerivedTextGlyphs = null
|
||||
}
|
||||
if (changes.vectorNetwork) {
|
||||
changes = { ...changes, vectorNetwork: normalizeVectorNetwork(changes.vectorNetwork) }
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export function createDefaultNode(
|
|||
flipX: false,
|
||||
flipY: false,
|
||||
textPicture: null,
|
||||
figmaDerivedTextGlyphs: null,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,13 @@ export interface PluginRelaunchDataEntry {
|
|||
isDeleted: boolean
|
||||
}
|
||||
|
||||
export interface FigmaDerivedTextGlyph {
|
||||
commandsBlob: Uint8Array
|
||||
x: number
|
||||
y: number
|
||||
fontSize: number
|
||||
}
|
||||
|
||||
export interface SymbolLink {
|
||||
uri: string
|
||||
displayName?: string
|
||||
|
|
@ -363,6 +370,7 @@ export interface SceneNode {
|
|||
flipY: boolean
|
||||
|
||||
textPicture: Uint8Array | null
|
||||
figmaDerivedTextGlyphs: FigmaDerivedTextGlyph[] | null
|
||||
}
|
||||
|
||||
export type ComponentPropertyType = 'VARIANT' | 'TEXT' | 'BOOLEAN' | 'INSTANCE_SWAP'
|
||||
|
|
|
|||
|
|
@ -21,6 +21,49 @@ describe('fig-import: text properties', () => {
|
|||
expect(n.textAlignHorizontal).toBe('CENTER')
|
||||
})
|
||||
|
||||
test('imports derived glyph geometry for Figma text rendering', () => {
|
||||
const glyphBlob = new Uint8Array([0])
|
||||
const graph = importNodeChanges(
|
||||
[
|
||||
doc(),
|
||||
canvas(),
|
||||
node('TEXT', 10, 1, {
|
||||
textData: { characters: 'A' },
|
||||
fontSize: 14,
|
||||
derivedTextData: {
|
||||
layoutSize: { x: 10, y: 10 },
|
||||
glyphs: [
|
||||
{
|
||||
commandsBlob: 0,
|
||||
position: { x: 2, y: 8 },
|
||||
fontSize: 14,
|
||||
firstCharacter: 0,
|
||||
advance: 1,
|
||||
rotation: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
} as Partial<NodeChange>)
|
||||
],
|
||||
[glyphBlob]
|
||||
)
|
||||
const n = graph.getChildren(graph.getPages()[0].id)[0]
|
||||
expect(n.figmaDerivedTextGlyphs).toEqual([
|
||||
{
|
||||
commandsBlob: glyphBlob,
|
||||
x: 2,
|
||||
y: 8,
|
||||
fontSize: 14
|
||||
}
|
||||
])
|
||||
|
||||
graph.updateNode(n.id, { opacity: 0.5 })
|
||||
expect(graph.getNode(n.id)?.figmaDerivedTextGlyphs).toHaveLength(1)
|
||||
|
||||
graph.updateNode(n.id, { text: 'B' })
|
||||
expect(graph.getNode(n.id)?.figmaDerivedTextGlyphs).toBeNull()
|
||||
})
|
||||
|
||||
test('uses derived line metrics for imported Figma text rendering', () => {
|
||||
const graph = importNodeChanges([
|
||||
doc(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue