From d0b5c51c5a79b10f0dd3d154dc516c112ca451fb Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 22 Apr 2026 16:01:03 +0300 Subject: [PATCH] fix(tools): set_font_range now produces valid style runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- packages/core/src/tools/modify.ts | 29 +++++++++++++--------- tests/engine/tools.test.ts | 41 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/core/src/tools/modify.ts b/packages/core/src/tools/modify.ts index f0522a806..fffafa4c4 100644 --- a/packages/core/src/tools/modify.ts +++ b/packages/core/src/tools/modify.ts @@ -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 } } } }) diff --git a/tests/engine/tools.test.ts b/tests/engine/tools.test.ts index ad3de43cf..568fc72f9 100644 --- a/tests/engine/tools.test.ts +++ b/tests/engine/tools.test.ts @@ -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()