diff --git a/packages/core/src/clipboard.ts b/packages/core/src/clipboard.ts index c5b88ef18..6e7bc1c49 100644 --- a/packages/core/src/clipboard.ts +++ b/packages/core/src/clipboard.ts @@ -7,7 +7,7 @@ import { buildFigKiwi, parseFigKiwiChunks, decompressFigKiwiDataAsync -} from './kiwi-serialize' +} from './kiwi/kiwi-serialize' import { initCodec, getCompiledSchema, getSchemaBytes } from './kiwi/codec' import { decodeBinarySchema, compileSchema, ByteBuffer } from './kiwi/kiwi-schema' import { nodeChangeToProps, sortChildren } from './kiwi/kiwi-convert' diff --git a/packages/core/src/fig-export-worker.ts b/packages/core/src/fig-export-worker.ts index cce9582a2..657e0229d 100644 --- a/packages/core/src/fig-export-worker.ts +++ b/packages/core/src/fig-export-worker.ts @@ -1,5 +1,5 @@ import { zipSync, type Zippable } from 'fflate' -import { buildFigKiwi } from './kiwi-serialize' +import { buildFigKiwi } from './kiwi/kiwi-serialize' interface CompressMessage { schemaDeflated: Uint8Array diff --git a/packages/core/src/fig-export.ts b/packages/core/src/fig-export.ts index a13f7cd50..5be63b56a 100644 --- a/packages/core/src/fig-export.ts +++ b/packages/core/src/fig-export.ts @@ -1,7 +1,7 @@ import { zipSync, deflateSync, type Zippable } from 'fflate' import { CANVAS_BG_COLOR, IS_BROWSER, IS_TAURI } from './constants' -import { sceneNodeToKiwi, fractionalPosition, buildFigKiwi, buildFontDigestMap, safeColor } from './kiwi-serialize' +import { sceneNodeToKiwi, fractionalPosition, buildFigKiwi, buildFontDigestMap, safeColor } from './kiwi/kiwi-serialize' import { initCodec, getCompiledSchema, getSchemaBytes } from './kiwi/codec' import { stringToGuid } from './kiwi/kiwi-convert' import { renderThumbnail } from './render-image' diff --git a/packages/core/src/figma-api-proxy.ts b/packages/core/src/figma-api-proxy.ts index 625b33e40..f17128152 100644 --- a/packages/core/src/figma-api-proxy.ts +++ b/packages/core/src/figma-api-proxy.ts @@ -9,6 +9,7 @@ import type { LayoutMode, } from './scene-graph' import { normalizeColor } from './color' +import { FONT_WEIGHT_NAMES } from './fonts' import type { Rect } from './types' import { copyFills, copyStrokes, copyEffects } from './copy' @@ -17,42 +18,24 @@ const MIXED = Symbol('mixed') export type FigmaFontName = { family: string; style: string } export function weightToStyleName(weight: number, italic: boolean): string { - const names: Record = { - 100: 'Thin', - 200: 'Extra Light', - 300: 'Light', - 400: 'Regular', - 500: 'Medium', - 600: 'Semi Bold', - 700: 'Bold', - 800: 'Extra Bold', - 900: 'Black' - } - const base = names[weight] ?? 'Regular' + const base = FONT_WEIGHT_NAMES[weight] ?? 'Regular' return italic ? `${base} Italic` : base } +const STYLE_NAME_TO_WEIGHT: Record = Object.fromEntries([ + ...Object.entries(FONT_WEIGHT_NAMES).map(([w, name]) => [name.toLowerCase(), Number(w)]), + ['ultra light', 200], + ['', 400], + ['demi bold', 600], + ['ultra bold', 800], + ['heavy', 900] +]) + export function styleNameToWeight(style: string): { weight: number; italic: boolean } { const lower = style.toLowerCase() const italic = lower.includes('italic') const clean = lower.replace(/italic/i, '').trim() - const map: Record = { - thin: 100, - 'extra light': 200, - 'ultra light': 200, - light: 300, - regular: 400, - '': 400, - medium: 500, - 'semi bold': 600, - 'demi bold': 600, - bold: 700, - 'extra bold': 800, - 'ultra bold': 800, - black: 900, - heavy: 900 - } - return { weight: map[clean] ?? 400, italic } + return { weight: STYLE_NAME_TO_WEIGHT[clean] ?? 400, italic } } export const INTERNAL_ID = Symbol('id') diff --git a/packages/core/src/fonts.ts b/packages/core/src/fonts.ts index 97082c178..f3bc0e947 100644 --- a/packages/core/src/fonts.ts +++ b/packages/core/src/fonts.ts @@ -29,7 +29,7 @@ export function getFontProvider(): TypefaceFontProvider | null { return fontProvider } -export async function queryFonts(): Promise { +async function queryFonts(): Promise { if (!IS_BROWSER || !window.queryLocalFonts) return [] try { const fonts = await window.queryLocalFonts() @@ -353,17 +353,20 @@ export function setCJKFallbackFamily(family: string): void { cjkFallbackFamily = family } -export function weightToStyle(weight: number, italic = false): string { - let label = 'Regular' - if (weight <= 100) label = 'Thin' - else if (weight <= 200) label = 'ExtraLight' - else if (weight <= 300) label = 'Light' - else if (weight <= 400) label = 'Regular' - else if (weight <= 500) label = 'Medium' - else if (weight <= 600) label = 'SemiBold' - else if (weight <= 700) label = 'Bold' - else if (weight <= 800) label = 'ExtraBold' - else if (weight >= 900) label = 'Black' - if (italic) label += ' Italic' - return label +export const FONT_WEIGHT_NAMES: Record = { + 100: 'Thin', + 200: 'Extra Light', + 300: 'Light', + 400: 'Regular', + 500: 'Medium', + 600: 'Semi Bold', + 700: 'Bold', + 800: 'Extra Bold', + 900: 'Black' +} + +export function weightToStyle(weight: number, italic = false): string { + const rounded = Math.round(weight / 100) * 100 + const label = (FONT_WEIGHT_NAMES[rounded] ?? 'Regular').replace(/ /g, '') + return italic ? `${label} Italic` : label } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a418baddb..8d7867ec4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -97,6 +97,7 @@ export { computeLayout, computeAllLayouts, setTextMeasurer } from './layout' export type { TextMeasurer } from './layout' export { getCanvasKit, getGpuBackend, type CanvasKitOptions, type GpuBackend } from './canvaskit' export { + FONT_WEIGHT_NAMES, collectFontKeys, loadFont, listFamilies, @@ -171,7 +172,7 @@ export { sceneNodeToKiwi, fractionalPosition, mapToFigmaType -} from './kiwi-serialize' +} from './kiwi/kiwi-serialize' export { createElement, diff --git a/packages/core/src/kiwi-serialize.ts b/packages/core/src/kiwi/kiwi-serialize.ts similarity index 98% rename from packages/core/src/kiwi-serialize.ts rename to packages/core/src/kiwi/kiwi-serialize.ts index 77f33a500..1164c2381 100644 --- a/packages/core/src/kiwi-serialize.ts +++ b/packages/core/src/kiwi/kiwi-serialize.ts @@ -2,13 +2,13 @@ export const FIG_KIWI_VERSION = 106 import { deflateSync, inflateSync } from 'fflate' -import { weightToStyle, getLoadedFontData } from './fonts' -import { encodeVectorNetworkBlob } from './vector' -import { stringToGuid, VARIABLE_BINDING_FIELDS } from './kiwi/kiwi-convert' +import { weightToStyle, getLoadedFontData } from '../fonts' +import { encodeVectorNetworkBlob } from '../vector' +import { stringToGuid, VARIABLE_BINDING_FIELDS } from './kiwi-convert' -import type { NodeChange, Paint, VariableConsumptionEntry } from './kiwi/codec' -import type { SceneGraph, SceneNode, CharacterStyleOverride } from './scene-graph' -import type { Color, GUID } from './types' +import type { NodeChange, Paint, VariableConsumptionEntry } from './codec' +import type { SceneGraph, SceneNode, CharacterStyleOverride } from '../scene-graph' +import type { Color, GUID } from '../types' const fontDigestCache = new Map() diff --git a/packages/core/src/undo.ts b/packages/core/src/undo.ts index bd65b55ac..fbce15247 100644 --- a/packages/core/src/undo.ts +++ b/packages/core/src/undo.ts @@ -1,5 +1,3 @@ -import type { SceneNode } from './scene-graph' - export interface UndoEntry { label: string forward: () => void @@ -92,18 +90,4 @@ export class UndoManager { } } -export function createPropertyChange( - node: SceneNode, - changes: Partial, - label: string -): UndoEntry { - const previous = Object.fromEntries( - (Object.keys(changes) as (keyof SceneNode)[]).map((key) => [key, node[key]]) - ) as Partial - return { - label, - forward: () => Object.assign(node, changes), - inverse: () => Object.assign(node, previous) - } -} diff --git a/src/components/VariablesDialog.vue b/src/components/VariablesDialog.vue index c659776de..c5ae5cfad 100644 --- a/src/components/VariablesDialog.vue +++ b/src/components/VariablesDialog.vue @@ -69,20 +69,7 @@ function commitRenameCollection(id: string, input: HTMLInputElement) { const value = input.value.trim() const collection = store.graph.variableCollections.get(id) if (collection && value && value !== collection.name) { - const oldName = collection.name - store.undo.push({ - label: 'Rename collection', - forward: () => { - store.graph.variableCollections.set(id, { ...collection, name: value }) - store.requestRender() - }, - inverse: () => { - store.graph.variableCollections.set(id, { ...collection, name: oldName }) - store.requestRender() - } - }) - store.graph.variableCollections.set(id, { ...collection, name: value }) - store.requestRender() + store.renameCollection(id, value) } editingCollectionId.value = null } @@ -119,42 +106,15 @@ function shortName(variable: Variable): string { function commitNameEdit(variable: Variable, newName: string) { if (newName && newName !== variable.name) { - const oldName = variable.name - store.undo.push({ - label: 'Rename variable', - forward: () => { - variable.name = newName - store.requestRender() - }, - inverse: () => { - variable.name = oldName - store.requestRender() - } - }) - variable.name = newName - store.requestRender() + store.renameVariable(variable.id, newName) } } function updateColorValue(variable: Variable, modeId: string, color: Color) { - const oldValue = structuredClone(variable.valuesByMode[modeId]) - store.undo.push({ - label: 'Change variable value', - forward: () => { - variable.valuesByMode[modeId] = color - store.requestRender() - }, - inverse: () => { - variable.valuesByMode[modeId] = oldValue - store.requestRender() - } - }) - variable.valuesByMode[modeId] = color - store.requestRender() + store.updateVariableValue(variable.id, modeId, color) } function commitValueEdit(variable: Variable, modeId: string, newValue: string) { - const oldValue = structuredClone(variable.valuesByMode[modeId]) let parsed: VariableValue if (variable.type === 'COLOR') { parsed = parseColor(newValue.startsWith('#') ? newValue : `#${newValue}`) @@ -167,19 +127,7 @@ function commitValueEdit(variable: Variable, modeId: string, newValue: string) { } else { parsed = newValue } - store.undo.push({ - label: 'Change variable value', - forward: () => { - variable.valuesByMode[modeId] = parsed - store.requestRender() - }, - inverse: () => { - variable.valuesByMode[modeId] = oldValue - store.requestRender() - } - }) - variable.valuesByMode[modeId] = parsed - store.requestRender() + store.updateVariableValue(variable.id, modeId, parsed) } function addVariable() { @@ -201,19 +149,7 @@ function addVariable() { description: '', hiddenFromPublishing: false } - store.undo.push({ - label: 'Create variable', - forward: () => { - store.graph.addVariable(variable) - store.requestRender() - }, - inverse: () => { - store.graph.removeVariable(id) - store.requestRender() - } - }) - store.graph.addVariable(variable) - store.requestRender() + store.addVariable(variable) } function addCollection() { @@ -225,42 +161,12 @@ function addCollection() { defaultModeId: 'default', variableIds: [] } - const prevTab = activeTab.value - store.undo.push({ - label: 'Create collection', - forward: () => { - store.graph.addCollection(collection) - activeTab.value = id - store.requestRender() - }, - inverse: () => { - store.graph.removeCollection(id) - activeTab.value = prevTab - store.requestRender() - } - }) - store.graph.addCollection(collection) + store.addCollection(collection) activeTab.value = id - store.requestRender() } function removeVariable(id: string) { - const variable = store.graph.variables.get(id) - if (!variable) return - const snapshot = structuredClone(variable) - store.undo.push({ - label: 'Delete variable', - forward: () => { - store.graph.removeVariable(id) - store.requestRender() - }, - inverse: () => { - store.graph.addVariable(snapshot) - store.requestRender() - } - }) - store.graph.removeVariable(id) - store.requestRender() + store.removeVariable(id) } const columns = computed[]>(() => { diff --git a/src/components/properties/PositionSection.vue b/src/components/properties/PositionSection.vue index c9e91965d..14735eb83 100644 --- a/src/components/properties/PositionSection.vue +++ b/src/components/properties/PositionSection.vue @@ -19,107 +19,7 @@ const hValue = multiProp('height') const rotationValue = computed(() => isMulti.value ? multiProp('rotation').value : Math.round(node.value?.rotation ?? 0) ) - -type HAlign = 'left' | 'center' | 'right' -type VAlign = 'top' | 'center' | 'bottom' - -function alignHorizontal(align: HAlign) { - const selected = nodes.value - if (selected.length === 0) return - - let minX: number, maxX: number - - if (selected.length === 1) { - const parent = store.graph.getNode(selected[0].parentId ?? '') - if (!parent) return - minX = 0 - maxX = parent.width - } else { - minX = Infinity - maxX = -Infinity - for (const n of selected) { - const abs = store.graph.getAbsolutePosition(n.id) - minX = Math.min(minX, abs.x) - maxX = Math.max(maxX, abs.x + n.width) - } - } - - for (const n of selected) { - let targetX: number - if (selected.length === 1) { - if (align === 'left') targetX = minX - else if (align === 'right') targetX = maxX - n.width - else targetX = (minX + maxX) / 2 - n.width / 2 - store.updateNode(n.id, { x: targetX }) - } else { - const abs = store.graph.getAbsolutePosition(n.id) - if (align === 'left') targetX = minX - else if (align === 'right') targetX = maxX - n.width - else targetX = (minX + maxX) / 2 - n.width / 2 - store.updateNode(n.id, { x: n.x + (targetX - abs.x) }) - } - } - store.requestRender() -} - -function alignVertical(align: VAlign) { - const selected = nodes.value - if (selected.length === 0) return - - let minY: number, maxY: number - - if (selected.length === 1) { - const parent = store.graph.getNode(selected[0].parentId ?? '') - if (!parent) return - minY = 0 - maxY = parent.height - } else { - minY = Infinity - maxY = -Infinity - for (const n of selected) { - const abs = store.graph.getAbsolutePosition(n.id) - minY = Math.min(minY, abs.y) - maxY = Math.max(maxY, abs.y + n.height) - } - } - - for (const n of selected) { - let targetY: number - if (selected.length === 1) { - if (align === 'top') targetY = minY - else if (align === 'bottom') targetY = maxY - n.height - else targetY = (minY + maxY) / 2 - n.height / 2 - store.updateNode(n.id, { y: targetY }) - } else { - const abs = store.graph.getAbsolutePosition(n.id) - if (align === 'top') targetY = minY - else if (align === 'bottom') targetY = maxY - n.height - else targetY = (minY + maxY) / 2 - n.height / 2 - store.updateNode(n.id, { y: n.y + (targetY - abs.y) }) - } - } - store.requestRender() -} - -function flipHorizontal() { - for (const n of nodes.value) { - store.updateNodeWithUndo(n.id, { flipX: !n.flipX }, 'Flip horizontal') - } - store.requestRender() -} - -function flipVertical() { - for (const n of nodes.value) { - store.updateNodeWithUndo(n.id, { flipY: !n.flipY }, 'Flip vertical') - } -} - -function rotate90() { - for (const n of nodes.value) { - store.updateNodeWithUndo(n.id, { rotation: (n.rotation + 90) % 360 }, 'Rotate 90°') - } - store.requestRender() -} +const ids = computed(() => nodes.value.map((n) => n.id))