Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Dependency-free ESM: runs in any modern browser as-is, no build step.

import { Store, newId } from './store.js'
import { themeOf, SIZES, FONT_SIZES, FONTS, GEO_IDS, COLOR_IDS, GRID_IDS, GRID_STEP, GRID_BAND, GRID_MAJOR } from './palette.js'
import { themeOf, SIZES, FONT_SIZES, FONTS, GEO_IDS, COLOR_IDS, GRID_IDS, GRID_STEP, GRID_BAND, GRID_MAJOR, fontStackOf } from './palette.js'
import {
localBounds, pageBounds, toLocal, drawShape, hitShape, marqueeHits,
scaleShape, textLayout, noteLayout, NOTE_W, sampleLinePts, resetTextLayouts,
Expand Down Expand Up @@ -1084,7 +1084,7 @@ export class Editor {
} else if (shape.type === 'geo' && ed.field === 'label') {
const p = shape.props
const fs = FONT_SIZES[p.labelSize || 's'] * (p.labelScale || 1)
const fam = FONTS[p.font || 'draw']
const fam = fontStackOf(p.font || 'draw')
pos = this.pageToScreen(shape.x + 8, shape.y + 8)
w = p.w - 16
h = p.h - 16
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export { Editor, TOOLS } from './editor.js'
export { Store, newId, isDiffEmpty, invertDiff, composeDiff, packDiff, unpackDiff, streamDiffs } from './store.js'
export { buildUI } from './ui.js'
export {
themeOf, THEMES, COLOR_IDS, SIZE_IDS, DASH_IDS, FILL_IDS, GEO_IDS, GRID_IDS,
SIZES, FONT_SIZES, FONTS,
themeOf, THEMES, COLOR_IDS, SIZE_IDS, DASH_IDS, FILL_IDS, FONT_IDS, GEO_IDS, GRID_IDS,
SIZES, FONT_SIZES, FONTS, setFontStacks, resetFontStacks, fontStackOf,
} from './palette.js'
export { pageBounds, localBounds, drawShape, hitShape } from './shapes.js'
export { strokeOutline, inkOptions } from './freehand.js'
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const NOTE_FONT_SIZES = { s: 16, m: 20, l: 26, xl: 32 }

export const DASH_IDS = ['draw', 'solid', 'dashed', 'dotted']
export const FILL_IDS = ['none', 'semi', 'solid', 'pattern']
export const FONT_IDS = ['draw', 'sans', 'serif', 'mono']

export const FONTS = {
// 'Quickdraw Hand' is Libre Baskerville, bundled via quickdraw.css; the rest are
Expand All @@ -115,6 +116,26 @@ export const FONTS = {
mono: "'SF Mono', ui-monospace, Menlo, monospace",
}

// Pluggable font stacks: remap any style id to a full CSS font-family stack
// (or register a brand-new id) — `setFontStacks({ sans: "'Inter', ..." })`.
// Everything that measures or draws type resolves through fontStackOf(), so
// one call reconfigures the editor, the UI previews and headless export
// alike. Process-global, like the backend injection: configure once at
// startup. Custom ids work anywhere a `font` prop is read.
const fontOverrides = {}
export function setFontStacks(stacks = {}) {
for (const [id, stack] of Object.entries(stacks)) {
if (typeof id !== 'string' || !id) throw new TypeError('font id must be a non-empty string')
if (typeof stack !== 'string' || !stack.trim()) throw new TypeError(`font stack for '${id}' must be a non-empty CSS font-family string`)
fontOverrides[id] = stack
}
return { ...fontOverrides }
}
export function resetFontStacks() {
for (const k of Object.keys(fontOverrides)) delete fontOverrides[k]
}
export const fontStackOf = (id) => fontOverrides[id] ?? FONTS[id] ?? FONTS.draw

export const GEO_IDS = ['rectangle', 'ellipse', 'triangle', 'diamond', 'hexagon', 'star', 'cloud']

// Highlighter: wide translucent band that multiplies into the paper.
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/shapes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
// Dependency-free ESM (see palette.js).

import {
SIZES, INK_SIZES, FONT_SIZES, NOTE_FONT_SIZES, FONTS, HIGHLIGHT_ALPHA, HIGHLIGHT_SCALE,
SIZES, INK_SIZES, FONT_SIZES, NOTE_FONT_SIZES, HIGHLIGHT_ALPHA, HIGHLIGHT_SCALE,
fontStackOf,
} from './palette.js'
import {
ptsBounds, rotWith, distToPolyline, pointInPolygon, pointInEllipse,
Expand Down Expand Up @@ -159,7 +160,7 @@ export function textLayout(shape) {
const hit = layoutCache.get(p)
if (hit) return hit
const fontSize = FONT_SIZES[p.size] * (p.scale || 1)
const font = FONTS[p.font || 'draw']
const font = fontStackOf(p.font || 'draw')
const lh = fontSize * 1.32
const maxW = p.autosize === false && p.w ? p.w : 0
const lines = wrapLines(p.text, font, fontSize, maxW)
Expand All @@ -174,7 +175,7 @@ export function noteLayout(shape) {
const hit = layoutCache.get(p)
if (hit) return hit
const fontSize = NOTE_FONT_SIZES[p.size]
const font = FONTS[p.font || 'draw']
const font = fontStackOf(p.font || 'draw')
const lh = fontSize * 1.35
const lines = wrapLines(p.text, font, fontSize, NOTE_W - NOTE_PAD * 2)
const textH = lines.length * lh
Expand All @@ -190,7 +191,7 @@ function geoLabelLayout(shape) {
let hit = layoutCache.get(key)
if (hit) return hit
const fontSize = FONT_SIZES[p.labelSize || 's'] * (p.labelScale || 1)
const font = FONTS[p.font || 'draw']
const font = fontStackOf(p.font || 'draw')
const lh = fontSize * 1.3
const lines = wrapLines(p.label, font, fontSize, Math.max(24, p.w - LABEL_PAD * 2))
hit = { lines, fontSize, font, lh, textH: lines.length * lh }
Expand Down
42 changes: 40 additions & 2 deletions packages/core/test/palette.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it, expect } from 'vitest'
// @vitest-environment jsdom
import { describe, it, expect, afterEach } from 'vitest'
import {
THEMES, themeOf, COLOR_IDS, SIZE_IDS, DASH_IDS, FILL_IDS, GEO_IDS, GRID_IDS,
SIZES, FONT_SIZES, FONTS,
SIZES, FONT_SIZES, FONTS, FONT_IDS, setFontStacks, resetFontStacks, fontStackOf,
} from '../src/palette.js'

describe('palette', () => {
Expand Down Expand Up @@ -50,3 +51,40 @@ describe('palette', () => {
}
})
})

describe('font stacks', () => {
afterEach(() => resetFontStacks())

it('defaults resolve every rostered id; unknown ids fall back to draw', () => {
for (const id of FONT_IDS) expect(fontStackOf(id)).toBe(FONTS[id])
expect(fontStackOf('nope')).toBe(FONTS.draw)
expect(fontStackOf()).toBe(FONTS.draw)
})

it('setFontStacks remaps ids (and registers new ones); reset restores', () => {
setFontStacks({ sans: "'Inter', sans-serif", brand: "'Brand', serif" })
expect(fontStackOf('sans')).toBe("'Inter', sans-serif")
expect(fontStackOf('brand')).toBe("'Brand', serif")
expect(fontStackOf('draw')).toBe(FONTS.draw)
resetFontStacks()
expect(fontStackOf('sans')).toBe(FONTS.sans)
expect(fontStackOf('brand')).toBe(FONTS.draw)
})

it('rejects empty ids and stacks', () => {
expect(() => setFontStacks({ '': 'serif' })).toThrow(TypeError)
expect(() => setFontStacks({ sans: '' })).toThrow(TypeError)
expect(() => setFontStacks({ sans: 42 })).toThrow(TypeError)
})

it('text layout follows the override', async () => {
const { textLayout } = await import('../src/shapes.js')
const shape = (font) => ({
id: 't', typeName: 'shape', type: 'text', x: 0, y: 0, rot: 0, z: 1,
props: { text: 'hi', size: 'm', color: 'black', font },
})
expect(textLayout(shape('sans')).font).toBe(FONTS.sans)
setFontStacks({ sans: "'Inter', sans-serif" })
expect(textLayout(shape('sans')).font).toBe("'Inter', sans-serif")
})
})
13 changes: 12 additions & 1 deletion packages/core/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type ColorId =
export type SizeId = 's' | 'm' | 'l' | 'xl'
export type DashId = 'draw' | 'solid' | 'dashed' | 'dotted'
export type FillId = 'none' | 'semi' | 'solid' | 'pattern'
export type FontId = 'draw' | 'sans' | 'serif' | 'mono'
export type FontId = 'draw' | 'sans' | 'serif' | 'mono' | (string & {})
export type GeoId = 'rectangle' | 'ellipse' | 'triangle' | 'diamond' | 'hexagon' | 'star' | 'cloud'
export type ThemeId = 'light' | 'dark'
export type GridId = 'none' | 'lines' | 'ruled' | 'dots' | 'crosses' | 'iso'
Expand Down Expand Up @@ -105,6 +105,17 @@ export const THEMES: Record<ThemeId, Theme>
export const SIZES: Record<SizeId, number>
export const FONT_SIZES: Record<SizeId, number>
export const FONTS: Record<FontId, string>
/**
* Remap font style ids to full CSS font-family stacks (or register new ids):
* setFontStacks({ sans: "'Inter', system-ui, sans-serif" }). One call
* reconfigures measurement, drawing, the editor textarea and UI previews.
* Process-global: configure once at startup.
*/
export function setFontStacks(stacks: Record<string, string>): Record<string, string>
/** Clear all custom font stacks set with setFontStacks. */
export function resetFontStacks(): void
/** Resolve a font style id to its stack (custom override, bundled default, else draw). */
export function fontStackOf(id?: string): string

export function themeOf(id?: string): Theme
export function newId(prefix?: string): string
Expand Down