fix(tools): set_font_range now produces valid style runs

- Use applyStyleToRange from text/style-runs.ts instead of naively
  appending runs (which created overlaps and invalid state)
- Apply fontWeight from style name (e.g. 'Bold' → 700)
- Apply color as fills on the style override
- Previously the color param was accepted but silently ignored

Fixes #214
This commit is contained in:
Danila Poyarkov 2026-04-22 16:01:03 +03:00
parent 5c0262fa80
commit d0b5c51c5a
2 changed files with 58 additions and 12 deletions

View file

@ -1,9 +1,11 @@
/* eslint-disable max-lines -- property setters share common patterns, splitting would scatter related tools */
import { parseColor } from '../color'
import { DEFAULT_SHADOW_COLOR } from '../constants'
import { styleToWeight } from '../text/fonts'
import { applyStyleToRange } from '../text/style-runs'
import { defineTool } from './schema'
import type { CharacterStyleOverride, Effect, SceneNode, StyleRun } from '../scene-graph'
import type { CharacterStyleOverride, Effect, SceneNode } from '../scene-graph'
import type { Matrix } from '../types'
export const setFill = defineTool({
@ -487,18 +489,21 @@ export const setFontRange = defineTool({
execute: (figma, args) => {
const node = figma.getNodeById(args.id)
if (!node) return { error: `Node "${args.id}" not found` }
const style: CharacterStyleOverride = {}
if (args.family) style.fontFamily = args.family
if (args.size) style.fontSize = args.size
if (args.style === 'italic' || args.style === 'Italic') style.italic = true
const run: StyleRun = {
start: args.start,
length: args.end - args.start,
style
const override: CharacterStyleOverride = {}
if (args.family) override.fontFamily = args.family
if (args.size) override.fontSize = args.size
if (args.style) {
const s = args.style.toLowerCase()
if (s.includes('italic')) override.italic = true
override.fontWeight = styleToWeight(args.style)
}
figma.graph.updateNode(node.id, {
styleRuns: [...(figma.graph.getNode(node.id)?.styleRuns ?? []), run]
})
if (args.color) {
override.fills = [{ type: 'SOLID', color: parseColor(args.color), opacity: 1, visible: true }]
}
const raw = figma.graph.getNode(node.id)
if (!raw) return { error: `Node "${args.id}" not found` }
const runs = applyStyleToRange(raw.styleRuns, args.start, args.end, override, raw.text.length)
figma.graph.updateNode(node.id, { styleRuns: runs })
return { id: args.id, range: { start: args.start, end: args.end } }
}
})

View file

@ -542,6 +542,47 @@ describe('set_constraints', () => {
})
})
describe('set_font_range', () => {
test('applies font style to text range and survives serialization', () => {
const { figma, graph } = setup()
const createText = ALL_TOOLS.find((t) => t.name === 'create_shape')!
const setText = ALL_TOOLS.find((t) => t.name === 'set_text')!
const setFontRange = ALL_TOOLS.find((t) => t.name === 'set_font_range')!
const created = createText.execute(figma, { type: 'TEXT', x: 0, y: 0, width: 200, height: 20 }) as any
setText.execute(figma, { id: created.id, text: 'Hello World' })
setFontRange.execute(figma, { id: created.id, start: 0, end: 5, family: 'Inter', size: 18, style: 'Bold' })
const node = graph.getNode(created.id)!
expect(node.styleRuns.length).toBeGreaterThan(0)
for (const run of node.styleRuns) {
expect(run.style).toBeDefined()
expect(typeof run.start).toBe('number')
expect(typeof run.length).toBe('number')
}
const boldRun = node.styleRuns.find((r) => r.style.fontWeight === 700)
expect(boldRun).toBeDefined()
expect(boldRun!.start).toBe(0)
expect(boldRun!.length).toBe(5)
})
test('applies color to text range', () => {
const { figma, graph } = setup()
const createText = ALL_TOOLS.find((t) => t.name === 'create_shape')!
const setText = ALL_TOOLS.find((t) => t.name === 'set_text')!
const setFontRange = ALL_TOOLS.find((t) => t.name === 'set_font_range')!
const created = createText.execute(figma, { type: 'TEXT', x: 0, y: 0, width: 200, height: 20 }) as any
setText.execute(figma, { id: created.id, text: 'Red text' })
setFontRange.execute(figma, { id: created.id, start: 0, end: 3, color: '#ff0000' })
const node = graph.getNode(created.id)!
const colorRun = node.styleRuns.find((r) => r.style.fills?.length)
expect(colorRun).toBeDefined()
expect(colorRun!.style.fills![0].color.r).toBeCloseTo(1)
})
})
describe('render', () => {
test('renders JSX string', async () => {
const { figma } = setup()