Lint all packages: fix 193 errors across core, mcp, acp
Extend lint coverage from src/ to all packages (178 files, 140 rules). Core (37 files): - Remove 98 unnecessary conditions (dead ?./?? on non-nullish values) - Remove 38 unnecessary type assertions (! and as casts) - Convert 12 patterns to optional chain (a && a.b → a?.b) - Fix 10 consistent-type-imports (merge duplicates, inline type keyword) - Add parentheses to 16 nested ternaries - Convert 4 indexed for-loops to for-of - Merge 4 duplicate import statements MCP (3 files): - Replace typeof undefined check with 'in' operator - Capture graph in local const to avoid ! assertions ACP (1 file): - Fix 5 TypeScript errors (type predicates, object shapes, dead code) - Remove unnecessary template expression Vendored kiwi-schema gets broader rule overrides to avoid touching it. Core complexity stays as warning (24 functions over limit).
This commit is contained in:
parent
d9154ef29d
commit
f3eac5ac8b
14
oxlint.json
14
oxlint.json
|
|
@ -92,7 +92,13 @@
|
|||
"files": ["**/kiwi/kiwi-schema/**"],
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-non-null-assertion": "off"
|
||||
"typescript/no-non-null-assertion": "off",
|
||||
"typescript/prefer-for-of": "off",
|
||||
"typescript/prefer-optional-chain": "off",
|
||||
"typescript/consistent-type-imports": "off",
|
||||
"typescript/no-unnecessary-condition": "off",
|
||||
"import/no-mutable-exports": "off",
|
||||
"complexity": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -110,6 +116,12 @@
|
|||
"rules": {
|
||||
"open-pencil/no-raw-console-format": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/core/src/**"],
|
||||
"rules": {
|
||||
"complexity": "warn"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["node_modules", "dist", "desktop", "*.config.*"]
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"build": "bun run lint && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"lint": "oxlint -c oxlint.json --type-aware --type-check src/ packages/cli/src/",
|
||||
"lint": "oxlint -c oxlint.json --type-aware --type-check src/ packages/core/src/ packages/cli/src/ packages/mcp/src/ packages/acp/src/",
|
||||
"format": "oxfmt --write src/",
|
||||
"check": "bun run lint",
|
||||
"test": "playwright test --project=openpencil",
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export function createAgent(
|
|||
commands.push({
|
||||
name: bt.name,
|
||||
description: bt.description,
|
||||
input: { type: 'unstructured' }
|
||||
input: { hint: 'JSON arguments or key=value pairs' }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ export function createAgent(
|
|||
commands.push({
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
input: { type: 'unstructured' }
|
||||
input: { hint: 'JSON arguments or key=value pairs' }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ export function createAgent(
|
|||
const signal = session.pendingPrompt.signal
|
||||
|
||||
const userText = params.prompt
|
||||
.filter((b): b is acp.TextContent => b.type === 'text')
|
||||
.filter((b): b is acp.TextContent & { type: 'text' } => b.type === 'text')
|
||||
.map((b) => b.text)
|
||||
.join('\n')
|
||||
|
||||
|
|
@ -290,26 +290,13 @@ export function createAgent(
|
|||
update: {
|
||||
sessionUpdate: 'tool_call',
|
||||
toolCallId,
|
||||
title: `${toolName}`,
|
||||
title: toolName,
|
||||
kind,
|
||||
status: 'pending',
|
||||
rawInput: toolArgs
|
||||
}
|
||||
})
|
||||
|
||||
if (signal.aborted) {
|
||||
await connection.sessionUpdate({
|
||||
sessionId: params.sessionId,
|
||||
update: {
|
||||
sessionUpdate: 'tool_call_update',
|
||||
toolCallId,
|
||||
status: 'cancelled'
|
||||
}
|
||||
})
|
||||
session.pendingPrompt = null
|
||||
return { stopReason: 'cancelled' }
|
||||
}
|
||||
|
||||
await connection.sessionUpdate({
|
||||
sessionId: params.sessionId,
|
||||
update: {
|
||||
|
|
@ -324,8 +311,8 @@ export function createAgent(
|
|||
|
||||
if (builtin) {
|
||||
result = await builtin.execute(toolArgs, session)
|
||||
} else {
|
||||
result = await coreTool!.execute(makeFigma(session), toolArgs)
|
||||
} else if (coreTool) {
|
||||
result = await coreTool.execute(makeFigma(session), toolArgs)
|
||||
}
|
||||
|
||||
await connection.sessionUpdate({
|
||||
|
|
|
|||
|
|
@ -62,5 +62,5 @@ export async function getCanvasKit(options?: CanvasKitOptions): Promise<CanvasKi
|
|||
})
|
||||
}
|
||||
|
||||
return instance!
|
||||
return instance
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export async function parseFigmaClipboard(
|
|||
}
|
||||
|
||||
const blobs: Uint8Array[] = (msg.blobs ?? []).map((b) =>
|
||||
b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes) as number[])
|
||||
b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes))
|
||||
)
|
||||
|
||||
return { nodes: msg.nodeChanges ?? [], meta, blobs }
|
||||
|
|
@ -87,13 +87,12 @@ export function figmaNodesBounds(
|
|||
|
||||
const parentTypes = new Map<string, string>()
|
||||
for (const nc of nodeChanges) {
|
||||
if (!nc.guid) continue
|
||||
const id = `${nc.guid.sessionID}:${nc.guid.localID}`
|
||||
parentTypes.set(id, nc.type ?? '')
|
||||
}
|
||||
|
||||
for (const nc of nodeChanges) {
|
||||
if (!nc.guid || !nc.type || NON_VISUAL_TYPES.has(nc.type)) continue
|
||||
if (!nc.type || NON_VISUAL_TYPES.has(nc.type)) continue
|
||||
const parentId = nc.parentIndex?.guid
|
||||
? `${nc.parentIndex.guid.sessionID}:${nc.parentIndex.guid.localID}`
|
||||
: null
|
||||
|
|
@ -125,7 +124,6 @@ export function importClipboardNodes(
|
|||
const guidMap = new Map<string, KiwiNodeChange>()
|
||||
const parentMap = new Map<string, string>()
|
||||
for (const nc of nodeChanges) {
|
||||
if (!nc.guid) continue
|
||||
const id = `${nc.guid.sessionID}:${nc.guid.localID}`
|
||||
guidMap.set(id, nc)
|
||||
if (nc.parentIndex?.guid) {
|
||||
|
|
@ -210,7 +208,7 @@ export function importClipboardNodes(
|
|||
// Remap componentId from original Figma GUIDs to our node IDs
|
||||
for (const [, ourId] of created) {
|
||||
const node = graph.getNode(ourId)
|
||||
if (!node || node.type !== 'INSTANCE' || !node.componentId) continue
|
||||
if (node?.type !== 'INSTANCE' || !node.componentId) continue
|
||||
const ourComponentId = created.get(node.componentId)
|
||||
if (ourComponentId) graph.updateNode(ourId, { componentId: ourComponentId })
|
||||
}
|
||||
|
|
@ -232,8 +230,8 @@ export function importClipboardNodes(
|
|||
// so the node at least renders its own fills/strokes/layout.
|
||||
for (const [, ourId] of created) {
|
||||
const node = graph.getNode(ourId)
|
||||
if (!node || node.type !== 'INSTANCE') continue
|
||||
if (node.childIds.length === 0 && !graph.getNode(node.componentId)) {
|
||||
if (node?.type !== 'INSTANCE') continue
|
||||
if (node.childIds.length === 0 && (!node.componentId || !graph.getNode(node.componentId))) {
|
||||
graph.updateNode(ourId, { type: 'FRAME', componentId: '' })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ export function parseColor(input: string): Color {
|
|||
if (!parsed) return { ...BLACK }
|
||||
const rgb = toRgb(parsed)
|
||||
return {
|
||||
r: rgb?.r ?? 0,
|
||||
g: rgb?.g ?? 0,
|
||||
b: rgb?.b ?? 0,
|
||||
r: rgb.r,
|
||||
g: rgb.g,
|
||||
b: rgb.b,
|
||||
a: parsed.alpha ?? 1
|
||||
}
|
||||
}
|
||||
|
|
@ -24,14 +24,14 @@ export function normalizeColor(color?: Partial<Color>): Color {
|
|||
}
|
||||
|
||||
export function colorToHex(color: Color): string {
|
||||
return (formatHex({ mode: 'rgb', r: color.r, g: color.g, b: color.b }) ?? '#000000').toUpperCase()
|
||||
return formatHex({ mode: 'rgb', r: color.r, g: color.g, b: color.b }).toUpperCase()
|
||||
}
|
||||
|
||||
export function colorToHex8(color: Color, alpha?: number): string {
|
||||
const a = alpha ?? color.a
|
||||
if (a >= 1) return colorToHex(color)
|
||||
return (
|
||||
formatHex8({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: a }) ?? '#000000FF'
|
||||
formatHex8({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: a })
|
||||
).toUpperCase()
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export function colorToRgba255(color: Color) {
|
|||
}
|
||||
|
||||
export function colorToCSS(color: Color): string {
|
||||
return formatRgb({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: color.a }) ?? 'rgb(0, 0, 0)'
|
||||
return formatRgb({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: color.a })
|
||||
}
|
||||
|
||||
export function colorToCSSCompact(color: Color): string {
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ function variableValueToKiwi(
|
|||
value: VariableValue,
|
||||
type: string
|
||||
): { value: Record<string, unknown>; dataType: string; resolvedDataType: string } {
|
||||
if (typeof value === 'object' && value !== null && 'aliasId' in value) {
|
||||
if (typeof value === 'object' && 'aliasId' in value) {
|
||||
return {
|
||||
value: { alias: { guid: stringToGuid(value.aliasId) } },
|
||||
dataType: 'ALIAS',
|
||||
resolvedDataType: type === 'COLOR' ? 'COLOR' : type === 'BOOLEAN' ? 'BOOLEAN' : type === 'STRING' ? 'STRING' : 'FLOAT'
|
||||
resolvedDataType: { COLOR: 'COLOR', BOOLEAN: 'BOOLEAN', STRING: 'STRING' }[type] ?? 'FLOAT'
|
||||
}
|
||||
}
|
||||
if (type === 'COLOR' && typeof value === 'object' && value !== null && 'r' in value) {
|
||||
if (type === 'COLOR' && typeof value === 'object' && 'r' in value) {
|
||||
return {
|
||||
value: { colorValue: { r: value.r, g: value.g, b: value.b, a: value.a } },
|
||||
dataType: 'COLOR',
|
||||
|
|
@ -165,14 +165,8 @@ export async function exportFigFile(
|
|||
if (!variable) continue
|
||||
|
||||
const varGuid = stringToGuid(varId)
|
||||
const resolvedType =
|
||||
variable.type === 'COLOR'
|
||||
? 'COLOR'
|
||||
: variable.type === 'BOOLEAN'
|
||||
? 'BOOLEAN'
|
||||
: variable.type === 'STRING'
|
||||
? 'STRING'
|
||||
: 'FLOAT'
|
||||
const typeMap: Record<string, string> = { COLOR: 'COLOR', BOOLEAN: 'BOOLEAN', STRING: 'STRING' }
|
||||
const resolvedType = typeMap[variable.type] ?? 'FLOAT'
|
||||
|
||||
const entries = Object.entries(variable.valuesByMode).map(([modeId, value]) => ({
|
||||
modeID: stringToGuid(modeId),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { SceneGraph } from './scene-graph'
|
||||
|
||||
import type {
|
||||
SceneGraph,
|
||||
SceneNode,
|
||||
NodeType,
|
||||
Fill,
|
||||
|
|
@ -234,11 +233,11 @@ class FigmaNodeProxy {
|
|||
set cornerRadius(v: number | typeof MIXED) {
|
||||
if (v === MIXED) return
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
||||
cornerRadius: v as number,
|
||||
topLeftRadius: v as number,
|
||||
topRightRadius: v as number,
|
||||
bottomRightRadius: v as number,
|
||||
bottomLeftRadius: v as number,
|
||||
cornerRadius: v,
|
||||
topLeftRadius: v,
|
||||
topRightRadius: v,
|
||||
bottomRightRadius: v,
|
||||
bottomLeftRadius: v,
|
||||
independentCorners: false
|
||||
})
|
||||
}
|
||||
|
|
@ -705,7 +704,7 @@ class FigmaNodeProxy {
|
|||
set layoutAlign(v: string) {
|
||||
const mapped = v === 'STRETCH' ? 'STRETCH' : 'AUTO'
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], {
|
||||
layoutAlignSelf: mapped as SceneNode['layoutAlignSelf']
|
||||
layoutAlignSelf: mapped
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1101,7 +1100,7 @@ export class FigmaAPI {
|
|||
|
||||
ungroup(node: FigmaNodeProxy): void {
|
||||
const raw = this.graph.getNode(node[INTERNAL_ID])
|
||||
if (!raw || raw.type !== 'GROUP') return
|
||||
if (raw?.type !== 'GROUP') return
|
||||
const parentId = raw.parentId ?? this._currentPageId
|
||||
for (const childId of [...raw.childIds]) {
|
||||
this.graph.reparentNode(childId, parentId)
|
||||
|
|
@ -1293,10 +1292,13 @@ export class FigmaAPI {
|
|||
|
||||
notify(message: string): { cancel: () => void } {
|
||||
if (typeof console !== 'undefined') console.log(`[figma.notify] ${message}`)
|
||||
// eslint-disable-next-line no-empty-function
|
||||
return { cancel() {} }
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-empty-function
|
||||
commitUndo(): void {}
|
||||
// eslint-disable-next-line no-empty-function
|
||||
triggerUndo(): void {}
|
||||
|
||||
exportImage?: (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {
|
|||
CJK_FALLBACK_FAMILIES_MACOS,
|
||||
CJK_FALLBACK_FAMILIES_WINDOWS,
|
||||
CJK_FALLBACK_FAMILIES_LINUX,
|
||||
CJK_GOOGLE_FONT
|
||||
CJK_GOOGLE_FONT,
|
||||
GOOGLE_FONTS_API_KEY
|
||||
} from './constants'
|
||||
import type { SceneGraph } from './scene-graph'
|
||||
|
||||
|
|
@ -59,8 +60,6 @@ const BUNDLED_FONTS: Record<string, string> = {
|
|||
'Inter|Regular': '/Inter-Regular.ttf'
|
||||
}
|
||||
|
||||
import { GOOGLE_FONTS_API_KEY } from './constants'
|
||||
|
||||
const googleFontsCache = new Map<string, Record<string, string>>()
|
||||
const googleFontsFailed = new Set<string>()
|
||||
|
||||
|
|
@ -136,7 +135,7 @@ export async function loadFont(family: string, style = 'Regular'): Promise<Array
|
|||
}
|
||||
|
||||
// Try local font access API first (browser only)
|
||||
if (typeof window !== 'undefined' && window.queryLocalFonts) {
|
||||
if (window.queryLocalFonts) {
|
||||
try {
|
||||
const fonts = await window.queryLocalFonts()
|
||||
const normalized = normalizeFontFamily(family)
|
||||
|
|
@ -261,7 +260,7 @@ export function collectFontKeys(graph: SceneGraph, nodeIds: string[]): Array<[st
|
|||
fontKeys.add(`${family}\0${weightToStyle(node.fontWeight || 400, node.italic)}`)
|
||||
for (const run of node.styleRuns) {
|
||||
const f = run.style.fontFamily ?? family
|
||||
const w = run.style.fontWeight ?? node.fontWeight ?? 400
|
||||
const w = run.style.fontWeight ?? node.fontWeight
|
||||
const i = run.style.italic ?? node.italic
|
||||
fontKeys.add(`${f}\0${weightToStyle(w, i)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import type { GUID } from './types'
|
|||
const fontDigestCache = new Map<string, Uint8Array>()
|
||||
|
||||
async function computeFontDigest(data: ArrayBuffer): Promise<Uint8Array> {
|
||||
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
||||
if (typeof crypto !== 'undefined') {
|
||||
const hash = await crypto.subtle.digest('SHA-1', data)
|
||||
return new Uint8Array(hash)
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ function buildDerivedTextData(
|
|||
const key = `${family}|${style}`
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
fontMeta!.push({
|
||||
fontMeta.push({
|
||||
key: { family, style, postscript: '' },
|
||||
fontLineHeight: 1.2,
|
||||
fontDigest: digestMap.get(key),
|
||||
|
|
@ -421,7 +421,8 @@ export function sceneNodeToKiwi(
|
|||
const variable = graph.variables.get(varId)
|
||||
if (!variable) continue
|
||||
const varGuid = stringToGuid(varId)
|
||||
const resolvedType = variable.type === 'COLOR' ? 'COLOR' : variable.type === 'BOOLEAN' ? 'BOOLEAN' : variable.type === 'STRING' ? 'STRING' : 'FLOAT'
|
||||
const typeMap: Record<string, string> = { COLOR: 'COLOR', BOOLEAN: 'BOOLEAN', STRING: 'STRING' }
|
||||
const resolvedType = typeMap[variable.type] ?? 'FLOAT'
|
||||
entries.push({
|
||||
variableData: {
|
||||
value: { alias: { guid: varGuid } },
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ let compiledSchema: CompiledSchema | null = null
|
|||
*/
|
||||
export async function initCodec(): Promise<void> {
|
||||
if (compiledSchema) return
|
||||
compiledSchema = compileSchema(figmaSchema as Schema) as CompiledSchema
|
||||
compiledSchema = compileSchema(figmaSchema) as CompiledSchema
|
||||
}
|
||||
|
||||
export function getCompiledSchema() {
|
||||
|
|
@ -38,7 +38,7 @@ export function getCompiledSchema() {
|
|||
}
|
||||
|
||||
export function getSchemaBytes(): Uint8Array {
|
||||
return encodeBinarySchema(figmaSchema as Schema)
|
||||
return encodeBinarySchema(figmaSchema)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -477,7 +477,7 @@ export function encodePaintWithVariableBinding(
|
|||
const { colorVariableBinding: _, ...basePaint } = paint
|
||||
|
||||
const baseBytes = compiledSchema.encodePaint(basePaint)
|
||||
const baseArray = Array.from(baseBytes) as number[]
|
||||
const baseArray = Array.from(baseBytes)
|
||||
|
||||
// Remove trailing 00
|
||||
if (baseArray[baseArray.length - 1] === 0) {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export async function parseFigFile(buffer: ArrayBuffer): Promise<SceneGraph> {
|
|||
}
|
||||
|
||||
const blobs: Uint8Array[] = (message.blobs ?? []).map((b) =>
|
||||
b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes) as number[])
|
||||
b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes))
|
||||
)
|
||||
|
||||
const images = new Map<string, Uint8Array>()
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export function importNodeChanges(
|
|||
const childrenMap = new Map<string, string[]>()
|
||||
|
||||
for (const nc of nodeChanges) {
|
||||
if (!nc.guid) continue
|
||||
if (nc.phase === 'REMOVED') continue
|
||||
const id = guidToString(nc.guid)
|
||||
changeMap.set(id, nc)
|
||||
|
|
@ -87,8 +86,8 @@ export function importNodeChanges(
|
|||
if (nc.type !== 'VARIABLE_SET') continue
|
||||
|
||||
const modes = (nc.variableSetModes ?? []).map((m) => {
|
||||
const modeId = m.id ? guidToString(m.id) : 'default'
|
||||
return { modeId, name: m.name ?? 'Mode' }
|
||||
const modeId = guidToString(m.id)
|
||||
return { modeId, name: m.name }
|
||||
})
|
||||
if (modes.length === 0) modes.push({ modeId: 'default', name: 'Default' })
|
||||
|
||||
|
|
@ -150,7 +149,8 @@ export function importNodeChanges(
|
|||
if (Object.keys(valuesByMode).length === 0) {
|
||||
const col = graph.variableCollections.get(collectionId)
|
||||
const defaultMode = col?.defaultModeId ?? 'default'
|
||||
valuesByMode[defaultMode] = type === 'BOOLEAN' ? false : type === 'STRING' ? '' : type === 'COLOR' ? { r: 0, g: 0, b: 0, a: 1 } : 0
|
||||
const defaultValue = type === 'BOOLEAN' ? false : (type === 'STRING' ? '' : null)
|
||||
valuesByMode[defaultMode] = defaultValue ?? (type === 'COLOR' ? { r: 0, g: 0, b: 0, a: 1 } : 0)
|
||||
}
|
||||
|
||||
graph.addVariable({
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export function populateAndApplyOverrides(
|
|||
while (populateQueue.length > 0) {
|
||||
const nodeId = populateQueue.pop()!
|
||||
const node = graph.getNode(nodeId)
|
||||
if (!node || node.type !== 'INSTANCE' || !node.componentId || node.childIds.length > 0) continue
|
||||
if (node?.type !== 'INSTANCE' || !node.componentId || node.childIds.length > 0) continue
|
||||
const comp = graph.getNode(node.componentId)
|
||||
if (comp && comp.childIds.length > 0) {
|
||||
graph.populateInstanceChildren(nodeId, node.componentId)
|
||||
|
|
@ -245,7 +245,7 @@ export function populateAndApplyOverrides(
|
|||
|
||||
function repopulateInstance(nodeId: string, compId: string) {
|
||||
const node = graph.getNode(nodeId)
|
||||
if (!node || node.type !== 'INSTANCE') return
|
||||
if (node?.type !== 'INSTANCE') return
|
||||
|
||||
for (const childId of [...node.childIds]) graph.deleteNode(childId)
|
||||
graph.updateNode(nodeId, { componentId: compId })
|
||||
|
|
@ -285,7 +285,7 @@ export function populateAndApplyOverrides(
|
|||
if (ownAssignments) {
|
||||
const valueByDef = new Map<string, ComponentPropAssignment['value']>()
|
||||
for (const a of ownAssignments) {
|
||||
if (a.defID) valueByDef.set(guidToString(a.defID), a.value)
|
||||
valueByDef.set(guidToString(a.defID), a.value)
|
||||
}
|
||||
applyPropAssignments(node.id, valueByDef, propRefsMap)
|
||||
}
|
||||
|
|
@ -303,7 +303,7 @@ export function populateAndApplyOverrides(
|
|||
|
||||
const valueByDef = new Map<string, ComponentPropAssignment['value']>()
|
||||
for (const a of assignments) {
|
||||
if (a.defID) valueByDef.set(guidToString(a.defID), a.value)
|
||||
valueByDef.set(guidToString(a.defID), a.value)
|
||||
}
|
||||
applyPropAssignments(node.id, valueByDef, propRefsMap)
|
||||
}
|
||||
|
|
@ -329,7 +329,7 @@ export function populateAndApplyOverrides(
|
|||
|
||||
const valueByDef = new Map<string, ComponentPropAssignment['value']>()
|
||||
for (const a of ov.componentPropAssignments) {
|
||||
if (a.defID) valueByDef.set(guidToString(a.defID), a.value)
|
||||
valueByDef.set(guidToString(a.defID), a.value)
|
||||
}
|
||||
applyPropAssignments(targetId, valueByDef, propRefsMap)
|
||||
}
|
||||
|
|
@ -354,7 +354,6 @@ export function populateAndApplyOverrides(
|
|||
const refs = findPropRefs(child.componentId, propRefsMap)
|
||||
if (refs) {
|
||||
for (const ref of refs) {
|
||||
if (!ref.defID) continue
|
||||
const val = valueByDef.get(guidToString(ref.defID))
|
||||
if (!val) continue
|
||||
|
||||
|
|
@ -387,7 +386,7 @@ export function populateAndApplyOverrides(
|
|||
while (o < scaled.length) {
|
||||
const cmd = scaled[o++]
|
||||
if (cmd === 0) continue
|
||||
const coords = cmd === 1 || cmd === 2 ? 1 : cmd === 4 ? 3 : -1
|
||||
const coords = cmd === 1 || cmd === 2 ? 1 : (cmd === 4 ? 3 : -1)
|
||||
if (coords < 0) {
|
||||
console.warn(`scaleGeometryBlobs: unknown path command ${cmd} at offset ${o - 1}`)
|
||||
break
|
||||
|
|
@ -414,8 +413,7 @@ export function populateAndApplyOverrides(
|
|||
const nodeId = guidToNodeId.get(ncId)
|
||||
if (!nodeId) continue
|
||||
|
||||
for (let i = 0; i < derived.length; i++) {
|
||||
const d = derived[i]
|
||||
for (const d of derived) {
|
||||
const guids = d.guidPath?.guids
|
||||
if (!guids?.length) continue
|
||||
|
||||
|
|
@ -549,16 +547,16 @@ export function populateAndApplyOverrides(
|
|||
|
||||
function syncNodeProps(source: SceneNode, target: SceneNode) {
|
||||
const updates: Partial<SceneNode> = {}
|
||||
if (source.text !== undefined && source.text !== target.text) updates.text = source.text
|
||||
if (source.visible !== undefined && source.visible !== target.visible) updates.visible = source.visible
|
||||
if (source.opacity !== undefined && source.opacity !== target.opacity) updates.opacity = source.opacity
|
||||
if (source.fills !== undefined && source.fills !== target.fills) updates.fills = copyFills(source.fills)
|
||||
if (source.strokes !== undefined && source.strokes !== target.strokes) updates.strokes = copyStrokes(source.strokes)
|
||||
if (source.effects !== undefined && source.effects !== target.effects) updates.effects = copyEffects(source.effects)
|
||||
if (source.styleRuns !== undefined && source.styleRuns !== target.styleRuns) updates.styleRuns = copyStyleRuns(source.styleRuns)
|
||||
if (source.layoutGrow !== undefined && source.layoutGrow !== target.layoutGrow) updates.layoutGrow = source.layoutGrow
|
||||
if (source.textAutoResize !== undefined && source.textAutoResize !== target.textAutoResize) updates.textAutoResize = source.textAutoResize
|
||||
if (source.locked !== undefined && source.locked !== target.locked) updates.locked = source.locked
|
||||
if (source.text !== target.text) updates.text = source.text
|
||||
if (source.visible !== target.visible) updates.visible = source.visible
|
||||
if (source.opacity !== target.opacity) updates.opacity = source.opacity
|
||||
if (source.fills !== target.fills) updates.fills = copyFills(source.fills)
|
||||
if (source.strokes !== target.strokes) updates.strokes = copyStrokes(source.strokes)
|
||||
if (source.effects !== target.effects) updates.effects = copyEffects(source.effects)
|
||||
if (source.styleRuns !== target.styleRuns) updates.styleRuns = copyStyleRuns(source.styleRuns)
|
||||
if (source.layoutGrow !== target.layoutGrow) updates.layoutGrow = source.layoutGrow
|
||||
if (source.textAutoResize !== target.textAutoResize) updates.textAutoResize = source.textAutoResize
|
||||
if (source.locked !== target.locked) updates.locked = source.locked
|
||||
if (Object.keys(updates).length > 0) graph.updateNode(target.id, updates)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,14 +89,14 @@ export function convertFills(paints?: Paint[]): Fill[] {
|
|||
if (!paints) return []
|
||||
return paints.map((p) => {
|
||||
const base: Fill = {
|
||||
type: (p.type ?? 'SOLID') as FillType,
|
||||
type: p.type as FillType,
|
||||
color: convertColor(p.color),
|
||||
opacity: p.opacity ?? 1,
|
||||
visible: p.visible ?? true,
|
||||
blendMode: (p.blendMode ?? 'NORMAL') as BlendMode
|
||||
}
|
||||
|
||||
if (p.type?.startsWith('GRADIENT') && p.stops) {
|
||||
if (p.type.startsWith('GRADIENT') && p.stops) {
|
||||
base.gradientStops = p.stops.map((s) => ({
|
||||
color: convertColor(s.color),
|
||||
position: s.position
|
||||
|
|
@ -115,7 +115,7 @@ export function convertFills(paints?: Paint[]): Fill[] {
|
|||
base.imageHash = img.hash
|
||||
}
|
||||
}
|
||||
base.imageScaleMode = (p.imageScaleMode as ImageScaleMode) ?? 'FILL'
|
||||
base.imageScaleMode = (p.imageScaleMode ?? 'FILL') as ImageScaleMode
|
||||
if (p.transform) {
|
||||
base.imageTransform = convertGradientTransform(p.transform)
|
||||
}
|
||||
|
|
@ -141,9 +141,9 @@ function convertStrokes(
|
|||
visible: p.visible ?? true,
|
||||
align: (align === 'INSIDE'
|
||||
? 'INSIDE'
|
||||
: align === 'OUTSIDE'
|
||||
: (align === 'OUTSIDE'
|
||||
? 'OUTSIDE'
|
||||
: 'CENTER') as Stroke['align'],
|
||||
: 'CENTER')),
|
||||
cap: (cap ?? 'NONE') as StrokeCap,
|
||||
join: (join ?? 'MITER') as StrokeJoin,
|
||||
dashPattern: dashPattern ?? []
|
||||
|
|
@ -153,13 +153,13 @@ function convertStrokes(
|
|||
function convertEffects(effects?: KiwiEffect[]): Effect[] {
|
||||
if (!effects) return []
|
||||
return effects.map((e) => ({
|
||||
type: e.type as Effect['type'],
|
||||
type: e.type,
|
||||
color: convertColor(e.color),
|
||||
offset: e.offset ?? { x: 0, y: 0 },
|
||||
radius: e.radius ?? 0,
|
||||
spread: e.spread ?? 0,
|
||||
visible: e.visible ?? true,
|
||||
blendMode: (e.blendMode as BlendMode) ?? 'NORMAL'
|
||||
blendMode: (e.blendMode ?? 'NORMAL') as BlendMode
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -335,8 +335,8 @@ function importStyleRuns(nc: NodeChange): StyleRun[] {
|
|||
const style: CharacterStyleOverride = {}
|
||||
if (override.fontName) {
|
||||
style.fontFamily = override.fontName.family
|
||||
style.fontWeight = styleToWeight(override.fontName.style ?? '')
|
||||
style.italic = override.fontName.style?.toLowerCase().includes('italic') ?? false
|
||||
style.fontWeight = styleToWeight(override.fontName.style)
|
||||
style.italic = override.fontName.style.toLowerCase().includes('italic')
|
||||
}
|
||||
if (override.fontSize !== undefined) style.fontSize = override.fontSize
|
||||
if (override.letterSpacing) {
|
||||
|
|
@ -346,7 +346,7 @@ function importStyleRuns(nc: NodeChange): StyleRun[] {
|
|||
const lh = convertLineHeight(override.lineHeight, override.fontSize ?? nc.fontSize)
|
||||
if (lh != null) style.lineHeight = lh
|
||||
}
|
||||
const deco = override.textDecoration as string | undefined
|
||||
const deco = override.textDecoration
|
||||
if (deco) style.textDecoration = mapTextDecoration(deco)
|
||||
if (Object.keys(style).length > 0) styleMap.set(id, style)
|
||||
}
|
||||
|
|
@ -385,13 +385,12 @@ function resolveVectorNetwork(
|
|||
}
|
||||
| undefined
|
||||
|
||||
if (!vectorData || vectorData.vectorNetworkBlob === undefined) return null
|
||||
if (vectorData?.vectorNetworkBlob === undefined) return null
|
||||
const idx = vectorData.vectorNetworkBlob
|
||||
if (idx < 0 || idx >= blobs.length) return null
|
||||
|
||||
try {
|
||||
const network = decodeVectorNetworkBlob(blobs[idx], vectorData.styleOverrideTable)
|
||||
if (!network) return null
|
||||
|
||||
const ns = vectorData.normalizedSize
|
||||
const nodeW = nc.size?.x ?? 0
|
||||
|
|
@ -430,7 +429,7 @@ export function resolveGeometryPaths(
|
|||
if (p.commandsBlob === undefined || p.commandsBlob < 0 || p.commandsBlob >= blobs.length)
|
||||
continue
|
||||
const blob = blobs[p.commandsBlob]
|
||||
if (!blob || blob.length === 0) continue
|
||||
if (blob.length === 0) continue
|
||||
result.push({
|
||||
windingRule: (p.windingRule === 'EVENODD' ? 'EVENODD' : 'NONZERO') as WindingRule,
|
||||
commandsBlob: blob
|
||||
|
|
@ -476,7 +475,7 @@ export function nodeChangeToProps(
|
|||
rotation = Math.atan2(nc.transform.m10 * sx, nc.transform.m00 * sx) * (180 / Math.PI)
|
||||
}
|
||||
|
||||
const dashPattern = (nc.dashPattern as number[]) ?? []
|
||||
const dashPattern = nc.dashPattern ?? []
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
|
|
@ -513,16 +512,16 @@ export function nodeChangeToProps(
|
|||
fontSize: nc.fontSize ?? 14,
|
||||
fontFamily: nc.fontName?.family ?? DEFAULT_FONT_FAMILY,
|
||||
fontWeight: styleToWeight(nc.fontName?.style ?? ''),
|
||||
italic: nc.fontName?.style?.toLowerCase().includes('italic') ?? false,
|
||||
italic: nc.fontName?.style.toLowerCase().includes('italic') ?? false,
|
||||
textAlignHorizontal:
|
||||
(nc.textAlignHorizontal as 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED') ?? 'LEFT',
|
||||
textAlignVertical: (nc.textAlignVertical as TextAlignVertical) ?? 'TOP',
|
||||
textAutoResize: (nc.textAutoResize as TextAutoResize) ?? 'NONE',
|
||||
textCase: (nc.textCase as TextCase) ?? 'ORIGINAL',
|
||||
(nc.textAlignHorizontal ?? 'LEFT') as 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED',
|
||||
textAlignVertical: (nc.textAlignVertical ?? 'TOP') as TextAlignVertical,
|
||||
textAutoResize: (nc.textAutoResize ?? 'NONE') as TextAutoResize,
|
||||
textCase: (nc.textCase ?? 'ORIGINAL') as TextCase,
|
||||
textDecoration: mapTextDecoration(nc.textDecoration as string),
|
||||
lineHeight: convertLineHeight(nc.lineHeight, nc.fontSize),
|
||||
letterSpacing: convertLetterSpacing(nc.letterSpacing, nc.fontSize),
|
||||
maxLines: (nc.maxLines as number) ?? null,
|
||||
maxLines: (nc.maxLines ?? null) as number | null,
|
||||
styleRuns: importStyleRuns(nc),
|
||||
horizontalConstraint: mapConstraint(nc.horizontalConstraint as string),
|
||||
verticalConstraint: mapConstraint(nc.verticalConstraint as string),
|
||||
|
|
@ -537,9 +536,9 @@ export function nodeChangeToProps(
|
|||
primaryAxisAlign: mapStackJustify(nc.stackPrimaryAlignItems ?? nc.stackJustify),
|
||||
counterAxisAlign: mapStackCounterAlign(nc.stackCounterAlignItems ?? nc.stackCounterAlign),
|
||||
layoutWrap: nc.stackWrap === 'WRAP' ? 'WRAP' : 'NO_WRAP',
|
||||
counterAxisSpacing: (nc.stackCounterSpacing as number) ?? 0,
|
||||
counterAxisSpacing: nc.stackCounterSpacing ?? 0,
|
||||
layoutPositioning: nc.stackPositioning === 'ABSOLUTE' ? 'ABSOLUTE' : 'AUTO',
|
||||
layoutGrow: (nc.stackChildPrimaryGrow as number) ?? 0,
|
||||
layoutGrow: nc.stackChildPrimaryGrow ?? 0,
|
||||
layoutAlignSelf: (nc.stackChildAlignSelf as string) === 'STRETCH' ? 'STRETCH' : 'AUTO',
|
||||
vectorNetwork: resolveVectorNetwork(nc, blobs),
|
||||
fillGeometry: resolveGeometryPaths(nc.fillGeometry, blobs),
|
||||
|
|
@ -548,25 +547,25 @@ export function nodeChangeToProps(
|
|||
strokeCap: (nc.strokeCap ?? 'NONE') as StrokeCap,
|
||||
strokeJoin: (nc.strokeJoin ?? 'MITER') as StrokeJoin,
|
||||
dashPattern,
|
||||
borderTopWeight: (nc.borderTopWeight as number) ?? 0,
|
||||
borderRightWeight: (nc.borderRightWeight as number) ?? 0,
|
||||
borderBottomWeight: (nc.borderBottomWeight as number) ?? 0,
|
||||
borderLeftWeight: (nc.borderLeftWeight as number) ?? 0,
|
||||
independentStrokeWeights: (nc.borderStrokeWeightsIndependent as boolean) ?? false,
|
||||
borderTopWeight: (nc.borderTopWeight ?? 0) as number,
|
||||
borderRightWeight: (nc.borderRightWeight ?? 0) as number,
|
||||
borderBottomWeight: (nc.borderBottomWeight ?? 0) as number,
|
||||
borderLeftWeight: (nc.borderLeftWeight ?? 0) as number,
|
||||
independentStrokeWeights: (nc.borderStrokeWeightsIndependent ?? false) as boolean,
|
||||
strokeMiterLimit: DEFAULT_STROKE_MITER_LIMIT,
|
||||
minWidth: (nc.minWidth as number) ?? null,
|
||||
maxWidth: (nc.maxWidth as number) ?? null,
|
||||
minHeight: (nc.minHeight as number) ?? null,
|
||||
maxHeight: (nc.maxHeight as number) ?? null,
|
||||
isMask: (nc.isMask as boolean) ?? false,
|
||||
maskType: ((nc.maskType as string) ?? 'ALPHA') as 'ALPHA' | 'VECTOR' | 'LUMINANCE',
|
||||
minWidth: (nc.minWidth ?? null) as number | null,
|
||||
maxWidth: (nc.maxWidth ?? null) as number | null,
|
||||
minHeight: (nc.minHeight ?? null) as number | null,
|
||||
maxHeight: (nc.maxHeight ?? null) as number | null,
|
||||
isMask: (nc.isMask ?? false) as boolean,
|
||||
maskType: (nc.maskType ?? 'ALPHA') as 'ALPHA' | 'VECTOR' | 'LUMINANCE',
|
||||
counterAxisAlignContent:
|
||||
(nc.stackCounterAlignContent as string) === 'SPACE_BETWEEN' ? 'SPACE_BETWEEN' : 'AUTO',
|
||||
itemReverseZIndex: (nc.stackReverseZIndex as boolean) ?? false,
|
||||
strokesIncludedInLayout: (nc.strokesIncludedInLayout as boolean) ?? false,
|
||||
itemReverseZIndex: (nc.stackReverseZIndex ?? false) as boolean,
|
||||
strokesIncludedInLayout: (nc.strokesIncludedInLayout ?? false) as boolean,
|
||||
expanded: true,
|
||||
textTruncation: (nc.textTruncation as string) === 'ENDING' ? 'ENDING' : 'DISABLED',
|
||||
autoRename: (nc.autoRename as boolean) ?? true,
|
||||
autoRename: (nc.autoRename ?? true) as boolean,
|
||||
boundVariables: extractBoundVariables(nc),
|
||||
clipsContent: nc.frameMaskDisabled === false,
|
||||
componentId: extractSymbolId(nc)
|
||||
|
|
@ -596,7 +595,7 @@ export function sortChildren(
|
|||
children.sort((a, b) => {
|
||||
const aPos = nodeMap.get(a)?.parentIndex?.position ?? ''
|
||||
const bPos = nodeMap.get(b)?.parentIndex?.position ?? ''
|
||||
return aPos < bPos ? -1 : aPos > bPos ? 1 : 0
|
||||
return aPos < bPos ? -1 : (aPos > bPos ? 1 : 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -680,9 +679,8 @@ export function convertOverrideToProps(ov: Record<string, unknown>): Partial<Sce
|
|||
updates.strokes = updates.strokes ?? []
|
||||
}
|
||||
if (ov.strokeAlign != null && updates.strokes) {
|
||||
const align = (
|
||||
ov.strokeAlign === 'INSIDE' ? 'INSIDE' : ov.strokeAlign === 'OUTSIDE' ? 'OUTSIDE' : 'CENTER'
|
||||
) as Stroke['align']
|
||||
const align =
|
||||
ov.strokeAlign === 'INSIDE' ? 'INSIDE' : (ov.strokeAlign === 'OUTSIDE' ? 'OUTSIDE' : 'CENTER')
|
||||
for (const s of updates.strokes) s.align = align
|
||||
}
|
||||
if (ov.borderTopWeight != null) updates.borderTopWeight = ov.borderTopWeight as number
|
||||
|
|
|
|||
|
|
@ -158,7 +158,6 @@ export function parseVarint(data: Uint8Array, pos: number): [number, number] {
|
|||
|
||||
while (pos < data.length) {
|
||||
const byte = data[pos]
|
||||
if (byte === undefined) break
|
||||
pos++
|
||||
value |= (byte & KIWI.VARINT_VALUE_MASK) << shift
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function mapGridTrack(track: GridTrack): { type: GridTrackType; value: number }
|
|||
return { type: GridTrackType.Fr, value: track.value }
|
||||
case 'FIXED':
|
||||
return { type: GridTrackType.Points, value: track.value }
|
||||
case 'AUTO':
|
||||
default:
|
||||
return { type: GridTrackType.Auto, value: 0 }
|
||||
}
|
||||
}
|
||||
|
|
@ -156,8 +156,7 @@ function buildYogaTree(graph: SceneGraph, frame: SceneNode): YogaNode {
|
|||
)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
for (const child of children) {
|
||||
|
||||
if (child.layoutPositioning === 'ABSOLUTE') continue
|
||||
|
||||
|
|
@ -352,6 +351,7 @@ function applyYogaLayout(graph: SceneGraph, frame: SceneNode, yogaNode: YogaNode
|
|||
|
||||
const yogaChild = yogaNode.getChild(yogaIndex)
|
||||
yogaIndex++
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!yogaChild) continue
|
||||
|
||||
graph.updateNode(child.id, {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export class CaptureStack {
|
|||
profile.selfTime = profile.endTime - profile.startTime - childrenTime
|
||||
|
||||
const parent = this.stack[this.stack.length - 1]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (parent) {
|
||||
parent.children.push(profile)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export class GPUTimer {
|
|||
if (this.pending.length >= MAX_PENDING_QUERIES) return
|
||||
|
||||
const query = this.gl.createQuery()
|
||||
if (!query) return
|
||||
|
||||
this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query)
|
||||
this.activeQuery = query
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SkiaRenderer } from './renderer'
|
||||
import type { SceneGraph } from './scene-graph'
|
||||
import type { CanvasKit } from 'canvaskit-wasm'
|
||||
import type { CanvasKit, Canvas } from 'canvaskit-wasm'
|
||||
|
||||
export type ExportFormat = 'PNG' | 'JPG' | 'WEBP' | 'SVG'
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ function renderToSurface(
|
|||
height: number,
|
||||
format: ExportFormat,
|
||||
quality: number,
|
||||
setup: (canvas: import('canvaskit-wasm').Canvas) => void
|
||||
setup: (canvas: Canvas) => void
|
||||
): Uint8Array | null {
|
||||
const surface = ck.MakeSurface(width, height)
|
||||
if (!surface) return null
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ const JSX_ENTITY: Record<string, string> = {
|
|||
}
|
||||
|
||||
function escapeJSXText(text: string): string {
|
||||
return text.replace(/[{}<>&]/g, (c) => JSX_ENTITY[c]!)
|
||||
return text.replace(/[{}<>&]/g, (c) => JSX_ENTITY[c])
|
||||
}
|
||||
|
||||
function formatProp(key: string, value: unknown): string {
|
||||
|
|
@ -476,7 +476,7 @@ function collectTailwindClasses(node: SceneNode, graph: SceneGraph): string[] {
|
|||
if (shadow) classes.push(`shadow-[${shadow}]`)
|
||||
} else if (effect.type === 'LAYER_BLUR' || effect.type === 'FOREGROUND_BLUR') {
|
||||
classes.push(`blur-[${effect.radius}px]`)
|
||||
} else if (effect.type === 'BACKGROUND_BLUR') {
|
||||
} else {
|
||||
classes.push(`backdrop-blur-[${effect.radius}px]`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ export function createElement(
|
|||
...props,
|
||||
children:
|
||||
flatChildren.length === 1
|
||||
? (flatChildren as ReactNode[])
|
||||
: flatChildren.length > 0
|
||||
? flatChildren
|
||||
: (flatChildren.length > 0
|
||||
? flatChildren
|
||||
: undefined
|
||||
: undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { isTreeNode } from './tree'
|
|||
import type { SceneGraph, SceneNode, NodeType, LayoutMode, Stroke } from '../scene-graph'
|
||||
import type { TreeNode } from './tree'
|
||||
|
||||
const TYPE_MAP: Record<string, NodeType> = {
|
||||
const TYPE_MAP: Partial<Record<string, NodeType>> = {
|
||||
frame: 'FRAME',
|
||||
view: 'FRAME',
|
||||
rectangle: 'RECTANGLE',
|
||||
|
|
@ -83,7 +83,7 @@ export function renderTree(
|
|||
tree: TreeNode,
|
||||
options: RenderOptions = {}
|
||||
): RenderResult {
|
||||
const parentId = options.parentId ?? graph.getPages()[0]?.id ?? graph.rootId
|
||||
const parentId = options.parentId ?? graph.getPages()[0].id
|
||||
|
||||
const result = renderNode(graph, tree, parentId)
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ function propsToOverrides(props: Record<string, unknown>, isText: boolean): Part
|
|||
}
|
||||
|
||||
if (typeof props.stroke === 'string') {
|
||||
const strokeWidth = (props.strokeWidth as number) ?? 1
|
||||
const strokeWidth = (props.strokeWidth as number | undefined) ?? 1
|
||||
o.strokes = [parseStroke(props.stroke, strokeWidth)]
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ function propsToOverrides(props: Record<string, unknown>, isText: boolean): Part
|
|||
if (props.pointCount !== undefined) o.pointCount = props.pointCount as number
|
||||
|
||||
if (typeof props.shadow === 'string') {
|
||||
const parts = (props.shadow as string).split(/\s+/)
|
||||
const parts = props.shadow.split(/\s+/)
|
||||
if (parts.length >= 4) {
|
||||
const c = parseColor(parts.slice(3).join(' '))
|
||||
o.effects = [
|
||||
|
|
@ -272,8 +272,8 @@ function propsToOverrides(props: Record<string, unknown>, isText: boolean): Part
|
|||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: c,
|
||||
offset: { x: parseFloat(parts[0]!), y: parseFloat(parts[1]!) },
|
||||
radius: parseFloat(parts[2]!),
|
||||
offset: { x: parseFloat(parts[0]), y: parseFloat(parts[1]) },
|
||||
radius: parseFloat(parts[2]),
|
||||
spread: 0,
|
||||
visible: true
|
||||
}
|
||||
|
|
@ -286,7 +286,7 @@ function propsToOverrides(props: Record<string, unknown>, isText: boolean): Part
|
|||
...(o.effects ?? []),
|
||||
{
|
||||
type: 'LAYER_BLUR',
|
||||
radius: props.blur as number,
|
||||
radius: props.blur,
|
||||
visible: true,
|
||||
color: { ...TRANSPARENT },
|
||||
offset: { x: 0, y: 0 },
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ import type { SnapGuide } from '../snap'
|
|||
import type { TextEditor } from '../text-editor'
|
||||
import type { Rect, Vector } from '../types'
|
||||
import type { Canvas } from 'canvaskit-wasm'
|
||||
import type { SkiaRenderer } from './renderer'
|
||||
import type { RenderOverlays } from './renderer'
|
||||
import type { SkiaRenderer, RenderOverlays } from './renderer'
|
||||
|
||||
export function drawHoverHighlight(
|
||||
r: SkiaRenderer,
|
||||
|
|
@ -219,7 +218,7 @@ export function drawSelectionLabels(
|
|||
const glyphIds = r.sizeFont.getGlyphIDs(sizeText)
|
||||
const widths = r.sizeFont.getGlyphWidths(glyphIds)
|
||||
let textWidth = 0
|
||||
for (let i = 0; i < widths.length; i++) textWidth += widths[i]
|
||||
for (const w of widths) textWidth += w
|
||||
const pillW = textWidth + SIZE_PILL_PADDING_X * 2
|
||||
const pillH = SIZE_PILL_HEIGHT
|
||||
const pillX = smx - pillW / 2
|
||||
|
|
@ -737,7 +736,7 @@ export function drawRemoteCursors(
|
|||
const glyphIds = font.getGlyphIDs(cursor.name)
|
||||
const widths = font.getGlyphWidths(glyphIds)
|
||||
let textWidth = 0
|
||||
for (let i = 0; i < widths.length; i++) textWidth += widths[i]
|
||||
for (const w of widths) textWidth += w
|
||||
|
||||
r.auxFill.setColor(r.ck.Color4f(cr, g, b, 1))
|
||||
const bgRect = r.ck.RRectXY(
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ import type { SceneNode, SceneGraph, Fill, Stroke } from '../scene-graph'
|
|||
import type { SnapGuide } from '../snap'
|
||||
import type { TextEditor } from '../text-editor'
|
||||
import type { Color, Rect, Vector } from '../types'
|
||||
import type { Image as CKImage, Path } from 'canvaskit-wasm'
|
||||
import type {
|
||||
Image as CKImage,
|
||||
Path,
|
||||
CanvasKit,
|
||||
Surface,
|
||||
Canvas,
|
||||
|
|
@ -45,7 +46,8 @@ import type {
|
|||
TypefaceFontProvider,
|
||||
SkPicture,
|
||||
ImageFilter,
|
||||
MaskFilter
|
||||
MaskFilter,
|
||||
Paragraph
|
||||
} from 'canvaskit-wasm'
|
||||
|
||||
import {
|
||||
|
|
@ -399,7 +401,7 @@ export class SkiaRenderer {
|
|||
this.fontsLoaded = true
|
||||
this.invalidateAllPictures()
|
||||
|
||||
ensureCJKFallback().then((family) => {
|
||||
void ensureCJKFallback().then((family) => {
|
||||
if (family) this.invalidateAllPictures()
|
||||
})
|
||||
}
|
||||
|
|
@ -564,7 +566,7 @@ export class SkiaRenderer {
|
|||
|
||||
const id = [...selectedIds][0]
|
||||
const node = graph.getNode(id)
|
||||
if (!node || node.type !== 'FRAME') return null
|
||||
if (node?.type !== 'FRAME') return null
|
||||
|
||||
const parent = node.parentId ? graph.getNode(node.parentId) : null
|
||||
const isTopLevel = !parent || parent.type === 'CANVAS' || parent.type === 'SECTION'
|
||||
|
|
@ -730,7 +732,7 @@ export class SkiaRenderer {
|
|||
this.worldViewport = prevViewport
|
||||
this.scenePictureVersion = sceneVersion
|
||||
this.scenePicturePageId = this.pageId
|
||||
canvas.drawPicture(this.scenePicture!)
|
||||
canvas.drawPicture(this.scenePicture)
|
||||
}
|
||||
|
||||
invalidateVectorPath(nodeId: string): void {
|
||||
|
|
@ -794,7 +796,7 @@ export class SkiaRenderer {
|
|||
node: SceneNode,
|
||||
color?: Float32Array,
|
||||
{ halfLeading = false }: { halfLeading?: boolean } = {}
|
||||
): import('canvaskit-wasm').Paragraph {
|
||||
): Paragraph {
|
||||
const ck = this.ck
|
||||
const baseColor = color ?? ck.BLACK
|
||||
const baseFontSize = node.fontSize || DEFAULT_FONT_SIZE
|
||||
|
|
@ -969,11 +971,11 @@ export class SkiaRenderer {
|
|||
this.penVertexFill.delete()
|
||||
this.penVertexStroke.delete()
|
||||
this.effectLayerPaint.delete()
|
||||
for (const filter of this.imageFilterCache.values()) filter?.delete()
|
||||
for (const filter of this.imageFilterCache.values()) filter.delete()
|
||||
this.imageFilterCache.clear()
|
||||
for (const filter of this.maskFilterCache.values()) filter?.delete()
|
||||
for (const filter of this.maskFilterCache.values()) filter.delete()
|
||||
this.maskFilterCache.clear()
|
||||
for (const pic of this.nodePictureCache.values()) pic?.delete()
|
||||
for (const pic of this.nodePictureCache.values()) pic.delete()
|
||||
this.nodePictureCache.clear()
|
||||
this.scenePicture?.delete()
|
||||
this._flashPaint?.delete()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { DROP_HIGHLIGHT_ALPHA, DROP_HIGHLIGHT_STROKE, SECTION_CORNER_RADIUS } from '../constants'
|
||||
import type { SceneNode, SceneGraph } from '../scene-graph'
|
||||
import type { Canvas } from 'canvaskit-wasm'
|
||||
import type { SkiaRenderer } from './renderer'
|
||||
import type { RenderOverlays } from './renderer'
|
||||
import type { Canvas, EmbindEnumEntity } from 'canvaskit-wasm'
|
||||
import type { SkiaRenderer, RenderOverlays } from './renderer'
|
||||
|
||||
export function renderNode(
|
||||
r: SkiaRenderer,
|
||||
|
|
@ -133,7 +132,7 @@ export function renderSection(
|
|||
const rrect = r.ck.RRectXY(rect, SECTION_CORNER_RADIUS, SECTION_CORNER_RADIUS)
|
||||
|
||||
for (let fi = 0; fi < node.fills.length; fi++) {
|
||||
const fill = node.fills[fi]!
|
||||
const fill = node.fills[fi]
|
||||
if (!fill.visible) continue
|
||||
r.applyFill(fill, node, graph, fi)
|
||||
r.fillPaint.setAlphaf(fill.opacity)
|
||||
|
|
@ -142,7 +141,7 @@ export function renderSection(
|
|||
}
|
||||
|
||||
for (let si = 0; si < node.strokes.length; si++) {
|
||||
const stroke = node.strokes[si]!
|
||||
const stroke = node.strokes[si]
|
||||
if (!stroke.visible) continue
|
||||
const sc = r.resolveStrokeColor(stroke, si, node, graph)
|
||||
r.strokePaint.setColor(r.ck.Color4f(sc.r, sc.g, sc.b, sc.a))
|
||||
|
|
@ -167,7 +166,7 @@ export function renderComponentSet(
|
|||
const rrect = r.ck.RRectXY(rect, 5, 5)
|
||||
|
||||
for (let fi = 0; fi < node.fills.length; fi++) {
|
||||
const fill = node.fills[fi]!
|
||||
const fill = node.fills[fi]
|
||||
if (!fill.visible) continue
|
||||
r.applyFill(fill, node, graph, fi)
|
||||
r.fillPaint.setAlphaf(fill.opacity)
|
||||
|
|
@ -235,7 +234,7 @@ export function renderShapeUncached(
|
|||
r.renderEffects(canvas, node, rect, hasRadius, 'behind')
|
||||
|
||||
for (let fi = 0; fi < node.fills.length; fi++) {
|
||||
const fill = node.fills[fi]!
|
||||
const fill = node.fills[fi]
|
||||
if (!fill.visible) continue
|
||||
r.applyFill(fill, node, graph, fi)
|
||||
r.fillPaint.setAlphaf(fill.opacity)
|
||||
|
|
@ -247,7 +246,7 @@ export function renderShapeUncached(
|
|||
const sg = node.type === 'VECTOR' ? r.getStrokeGeometry(node) : null
|
||||
const vectorPaths = !sg && node.type === 'VECTOR' ? r.getVectorPaths(node) : null
|
||||
for (let si = 0; si < node.strokes.length; si++) {
|
||||
const stroke = node.strokes[si]!
|
||||
const stroke = node.strokes[si]
|
||||
if (!stroke.visible) continue
|
||||
const sc = r.resolveStrokeColor(stroke, si, node, graph)
|
||||
|
||||
|
|
@ -260,12 +259,12 @@ export function renderShapeUncached(
|
|||
}
|
||||
|
||||
if (vectorPaths) {
|
||||
const capMap: Record<string, import('canvaskit-wasm').EmbindEnumEntity> = {
|
||||
const capMap: Record<string, EmbindEnumEntity> = {
|
||||
NONE: r.ck.StrokeCap.Butt,
|
||||
ROUND: r.ck.StrokeCap.Round,
|
||||
SQUARE: r.ck.StrokeCap.Square
|
||||
}
|
||||
const joinMap: Record<string, import('canvaskit-wasm').EmbindEnumEntity> = {
|
||||
const joinMap: Record<string, EmbindEnumEntity> = {
|
||||
MITER: r.ck.StrokeJoin.Miter,
|
||||
ROUND: r.ck.StrokeJoin.Round,
|
||||
BEVEL: r.ck.StrokeJoin.Bevel
|
||||
|
|
@ -294,7 +293,7 @@ export function renderShapeUncached(
|
|||
r.strokePaint.setAlphaf(stroke.opacity)
|
||||
|
||||
if (stroke.cap) {
|
||||
const capMap: Record<string, import('canvaskit-wasm').EmbindEnumEntity> = {
|
||||
const capMap: Record<string, EmbindEnumEntity> = {
|
||||
NONE: r.ck.StrokeCap.Butt,
|
||||
ROUND: r.ck.StrokeCap.Round,
|
||||
SQUARE: r.ck.StrokeCap.Square
|
||||
|
|
@ -302,7 +301,7 @@ export function renderShapeUncached(
|
|||
r.strokePaint.setStrokeCap(capMap[stroke.cap] ?? r.ck.StrokeCap.Butt)
|
||||
}
|
||||
if (stroke.join) {
|
||||
const joinMap: Record<string, import('canvaskit-wasm').EmbindEnumEntity> = {
|
||||
const joinMap: Record<string, EmbindEnumEntity> = {
|
||||
MITER: r.ck.StrokeJoin.Miter,
|
||||
ROUND: r.ck.StrokeJoin.Round,
|
||||
BEVEL: r.ck.StrokeJoin.Bevel
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SceneNode, Stroke } from '../scene-graph'
|
||||
import type { Canvas } from 'canvaskit-wasm'
|
||||
import type { Canvas, Paint } from 'canvaskit-wasm'
|
||||
import type { SkiaRenderer } from './renderer'
|
||||
|
||||
export function drawNodeStroke(
|
||||
|
|
@ -129,28 +129,28 @@ export function drawIndividualSideStrokes(
|
|||
|
||||
const tw = node.borderTopWeight
|
||||
if (tw > 0) {
|
||||
const y = inside ? tw / 2 : outside ? -tw / 2 : 0
|
||||
const y = inside ? tw / 2 : (outside ? -tw / 2 : 0)
|
||||
r.strokePaint.setStrokeWidth(tw)
|
||||
canvas.drawLine(0, y, w, y, r.strokePaint)
|
||||
}
|
||||
|
||||
const rw = node.borderRightWeight
|
||||
if (rw > 0) {
|
||||
const x = inside ? w - rw / 2 : outside ? w + rw / 2 : w
|
||||
const x = inside ? w - rw / 2 : (outside ? w + rw / 2 : w)
|
||||
r.strokePaint.setStrokeWidth(rw)
|
||||
canvas.drawLine(x, 0, x, h, r.strokePaint)
|
||||
}
|
||||
|
||||
const bw = node.borderBottomWeight
|
||||
if (bw > 0) {
|
||||
const y = inside ? h - bw / 2 : outside ? h + bw / 2 : h
|
||||
const y = inside ? h - bw / 2 : (outside ? h + bw / 2 : h)
|
||||
r.strokePaint.setStrokeWidth(bw)
|
||||
canvas.drawLine(0, y, w, y, r.strokePaint)
|
||||
}
|
||||
|
||||
const lw = node.borderLeftWeight
|
||||
if (lw > 0) {
|
||||
const x = inside ? lw / 2 : outside ? -lw / 2 : 0
|
||||
const x = inside ? lw / 2 : (outside ? -lw / 2 : 0)
|
||||
r.strokePaint.setStrokeWidth(lw)
|
||||
canvas.drawLine(x, 0, x, h, r.strokePaint)
|
||||
}
|
||||
|
|
@ -160,7 +160,7 @@ export function strokeNodeShape(
|
|||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
node: SceneNode,
|
||||
paint: import('canvaskit-wasm').Paint
|
||||
paint: Paint
|
||||
): void {
|
||||
const rect = r.ck.LTRBRect(0, 0, node.width, node.height)
|
||||
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ export const nodeCommand: RpcCommand<NodeArgs, NodeResult | { error: string }> =
|
|||
fontFamily: node.fontFamily,
|
||||
fontSize: node.fontSize,
|
||||
fontWeight: node.fontWeight,
|
||||
text: node.text?.length ? (node.text.length > 200 ? node.text.slice(0, 200) + '…' : node.text) : null,
|
||||
text: node.text.length ? (node.text.length > 200 ? node.text.slice(0, 200) + '…' : node.text) : null,
|
||||
parent: parent ? { id: parent.id, name: parent.name, type: parent.type } : null,
|
||||
children: node.childIds.length,
|
||||
boundVariables: boundVars
|
||||
|
|
@ -277,15 +277,14 @@ export interface VariablesArgs {
|
|||
function formatVariableValue(variable: Variable, graph: SceneGraph): string {
|
||||
const modeId = graph.getActiveModeId(variable.collectionId)
|
||||
const raw = variable.valuesByMode[modeId]
|
||||
if (raw === undefined) return '–'
|
||||
|
||||
if (typeof raw === 'object' && raw !== null && 'aliasId' in raw) {
|
||||
const alias = graph.variables.get((raw as { aliasId: string }).aliasId)
|
||||
return alias ? `→ ${alias.name}` : `→ ${(raw as { aliasId: string }).aliasId}`
|
||||
if (typeof raw === 'object' && 'aliasId' in raw) {
|
||||
const alias = graph.variables.get(raw.aliasId)
|
||||
return alias ? `→ ${alias.name}` : `→ ${raw.aliasId}`
|
||||
}
|
||||
|
||||
if (typeof raw === 'object' && raw !== null && 'r' in raw) {
|
||||
return colorToHex(raw as Color).toLowerCase()
|
||||
if (typeof raw === 'object' && 'r' in raw) {
|
||||
return colorToHex(raw).toLowerCase()
|
||||
}
|
||||
|
||||
return String(raw)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { BLACK, DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from './consta
|
|||
import { copyEffects, copyFills, copyStrokes, copyStyleRuns } from './copy'
|
||||
|
||||
export type { GUID, Color } from './types'
|
||||
import type { Matrix, Vector } from './types'
|
||||
import type { Matrix, Vector, Color, Rect } from './types'
|
||||
|
||||
export type HandleMirroring = 'NONE' | 'ANGLE' | 'ANGLE_AND_LENGTH'
|
||||
export type WindingRule = 'NONZERO' | 'EVENODD'
|
||||
|
|
@ -58,7 +58,6 @@ export type NodeType =
|
|||
| 'CONNECTOR'
|
||||
| 'SHAPE_WITH_TEXT'
|
||||
|
||||
import type { Color, Matrix, Rect } from './types'
|
||||
|
||||
export type FillType =
|
||||
| 'SOLID'
|
||||
|
|
@ -526,9 +525,18 @@ export class SceneGraph {
|
|||
const collection = this.variableCollections.get(collectionId)
|
||||
if (!collection) throw new Error(`Collection "${collectionId}" not found`)
|
||||
const id = generateId()
|
||||
const defaultValue =
|
||||
value ??
|
||||
(type === 'COLOR' ? { ...BLACK } : type === 'FLOAT' ? 0 : type === 'BOOLEAN' ? false : '')
|
||||
let defaultValue: VariableValue
|
||||
if (value !== undefined) {
|
||||
defaultValue = value
|
||||
} else if (type === 'COLOR') {
|
||||
defaultValue = { ...BLACK }
|
||||
} else if (type === 'FLOAT') {
|
||||
defaultValue = 0
|
||||
} else if (type === 'BOOLEAN') {
|
||||
defaultValue = false
|
||||
} else {
|
||||
defaultValue = ''
|
||||
}
|
||||
const valuesByMode: Record<string, VariableValue> = {}
|
||||
for (const mode of collection.modes) {
|
||||
valuesByMode[mode.modeId] = structuredClone(defaultValue)
|
||||
|
|
@ -592,8 +600,7 @@ export class SceneGraph {
|
|||
if (!variable) return undefined
|
||||
const resolvedModeId = modeId ?? this.getActiveModeId(variable.collectionId)
|
||||
const value = variable.valuesByMode[resolvedModeId]
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value === 'object' && value !== null && 'aliasId' in value) {
|
||||
if (typeof value === 'object' && 'aliasId' in value) {
|
||||
const seen = visited ?? new Set<string>()
|
||||
seen.add(variableId)
|
||||
return this.resolveVariable(value.aliasId, undefined, seen)
|
||||
|
|
@ -603,7 +610,7 @@ export class SceneGraph {
|
|||
|
||||
resolveColorVariable(variableId: string): Color | undefined {
|
||||
const value = this.resolveVariable(variableId)
|
||||
if (value && typeof value === 'object' && 'r' in value) return value as Color
|
||||
if (value && typeof value === 'object' && 'r' in value) return value
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
@ -962,10 +969,10 @@ export class SceneGraph {
|
|||
'borderLeftWeight'
|
||||
]
|
||||
|
||||
private static copyProp<K extends keyof SceneNode>(
|
||||
private static copyProp(
|
||||
target: Partial<SceneNode> | SceneNode,
|
||||
source: SceneNode,
|
||||
key: K
|
||||
key: keyof SceneNode
|
||||
): void {
|
||||
const val = source[key]
|
||||
if (key === 'fills') {
|
||||
|
|
@ -977,7 +984,7 @@ export class SceneGraph {
|
|||
} else if (key === 'styleRuns') {
|
||||
;(target as Record<string, unknown>)[key] = copyStyleRuns(val as StyleRun[])
|
||||
} else {
|
||||
target[key] = (Array.isArray(val) ? structuredClone(val) : val) as SceneNode[K]
|
||||
;(target as Record<string, unknown>)[key] = Array.isArray(val) ? structuredClone(val) : val
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -987,7 +994,7 @@ export class SceneGraph {
|
|||
overrides: Partial<SceneNode> = {}
|
||||
): SceneNode | null {
|
||||
const component = this.nodes.get(componentId)
|
||||
if (!component || component.type !== 'COMPONENT') return null
|
||||
if (component?.type !== 'COMPONENT') return null
|
||||
|
||||
const props: Partial<SceneNode> = { name: component.name, componentId }
|
||||
for (const key of SceneGraph.INSTANCE_SYNC_PROPS) {
|
||||
|
|
@ -1030,7 +1037,7 @@ export class SceneGraph {
|
|||
|
||||
syncInstances(componentId: string): void {
|
||||
const component = this.nodes.get(componentId)
|
||||
if (!component || component.type !== 'COMPONENT') return
|
||||
if (component?.type !== 'COMPONENT') return
|
||||
|
||||
for (const instance of this.getInstances(componentId)) {
|
||||
// Sync instance-level props (unless overridden)
|
||||
|
|
@ -1112,7 +1119,7 @@ export class SceneGraph {
|
|||
|
||||
detachInstance(instanceId: string): void {
|
||||
const node = this.nodes.get(instanceId)
|
||||
if (!node || node.type !== 'INSTANCE') return
|
||||
if (node?.type !== 'INSTANCE') return
|
||||
node.type = 'FRAME'
|
||||
node.componentId = null
|
||||
node.overrides = {}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ function formatColor(color: Color, opacity = 1): string {
|
|||
// --- Path data ---
|
||||
|
||||
export function geometryBlobToSVGPath(blob: Uint8Array): string {
|
||||
if (!blob || blob.length === 0) return ''
|
||||
if (blob.length === 0) return ''
|
||||
const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)
|
||||
let o = 0
|
||||
const parts: string[] = []
|
||||
|
|
@ -301,11 +301,7 @@ function createFilterDef(effects: Effect[], ctx: SVGExportContext): { id: string
|
|||
operator: 'over'
|
||||
})
|
||||
)
|
||||
} else if (
|
||||
effect.type === 'LAYER_BLUR' ||
|
||||
effect.type === 'BACKGROUND_BLUR' ||
|
||||
effect.type === 'FOREGROUND_BLUR'
|
||||
) {
|
||||
} else {
|
||||
const stdDev = round(effect.radius / 2)
|
||||
primitives.push(svg('feGaussianBlur', { stdDeviation: stdDev }))
|
||||
}
|
||||
|
|
@ -606,24 +602,24 @@ function renderTextNode(node: SceneNode, fillAttr: string | null): SVGNode {
|
|||
'text-anchor':
|
||||
node.textAlignHorizontal === 'CENTER'
|
||||
? 'middle'
|
||||
: node.textAlignHorizontal === 'RIGHT'
|
||||
: (node.textAlignHorizontal === 'RIGHT'
|
||||
? 'end'
|
||||
: undefined,
|
||||
: undefined),
|
||||
'text-decoration':
|
||||
node.textDecoration === 'UNDERLINE'
|
||||
? 'underline'
|
||||
: node.textDecoration === 'STRIKETHROUGH'
|
||||
: (node.textDecoration === 'STRIKETHROUGH'
|
||||
? 'line-through'
|
||||
: undefined,
|
||||
: undefined),
|
||||
'letter-spacing': node.letterSpacing ? round(node.letterSpacing) : undefined
|
||||
}
|
||||
|
||||
const x =
|
||||
node.textAlignHorizontal === 'CENTER'
|
||||
? round(node.width / 2)
|
||||
: node.textAlignHorizontal === 'RIGHT'
|
||||
: (node.textAlignHorizontal === 'RIGHT'
|
||||
? round(node.width)
|
||||
: 0
|
||||
: 0)
|
||||
const y = node.fontSize || 14
|
||||
|
||||
if (node.styleRuns.length > 0) {
|
||||
|
|
@ -833,7 +829,7 @@ export function renderNodesToSVG(
|
|||
? { ...node, x: round(offsetX), y: round(offsetY) }
|
||||
: node
|
||||
|
||||
const rendered = renderNode(clone as SceneNode, ctx)
|
||||
const rendered = renderNode(clone, ctx)
|
||||
if (rendered) contentNodes.push(rendered)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { FigmaAPI } from '../figma-api'
|
||||
import type { ToolDef, ParamDef, ParamType } from './schema'
|
||||
import type * as valibot from 'valibot'
|
||||
|
||||
export interface AIAdapterOptions {
|
||||
getFigma: () => FigmaAPI
|
||||
|
|
@ -39,7 +40,7 @@ export function toolsToAI(
|
|||
tools: ToolDef[],
|
||||
options: AIAdapterOptions,
|
||||
deps: {
|
||||
v: typeof import('valibot')
|
||||
v: typeof valibot
|
||||
valibotSchema: (schema: any) => any
|
||||
tool: (opts: any) => any
|
||||
}
|
||||
|
|
@ -77,7 +78,7 @@ export function toolsToAI(
|
|||
return result
|
||||
}
|
||||
|
||||
function paramToValibot(v: typeof import('valibot'), param: ParamDef): unknown {
|
||||
function paramToValibot(v: typeof valibot, param: ParamDef): unknown {
|
||||
const typeMap: Record<ParamType, () => unknown> = {
|
||||
string: () => (param.enum ? v.picklist(param.enum as [string, ...string[]]) : v.string()),
|
||||
number: () => {
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ function serializeNodeProps(raw: SceneNode): string {
|
|||
|
||||
for (const effect of raw.effects) {
|
||||
const parts: string[] = [effect.type]
|
||||
if (effect.radius !== undefined) parts.push(`r=${effect.radius}`)
|
||||
if (effect.color) parts.push(`c=${colorToHex(effect.color)}`)
|
||||
if (effect.offset) parts.push(`x=${effect.offset.x} y=${effect.offset.y}`)
|
||||
if (effect.spread !== undefined) parts.push(`s=${effect.spread}`)
|
||||
parts.push(`r=${effect.radius}`)
|
||||
parts.push(`c=${colorToHex(effect.color)}`)
|
||||
parts.push(`x=${effect.offset.x} y=${effect.offset.y}`)
|
||||
parts.push(`s=${effect.spread}`)
|
||||
lines.push(`effect: ${parts.join(' ')}`)
|
||||
}
|
||||
|
||||
|
|
@ -156,12 +156,12 @@ export const analyzeColors = defineTool({
|
|||
const boundVars = raw.boundVariables
|
||||
for (const fill of raw.fills) {
|
||||
if (fill.type === 'SOLID' && fill.visible) {
|
||||
trackColor(colorMap, fill.color, boundVars?.['fills'] ? String(boundVars['fills']) : null)
|
||||
trackColor(colorMap, fill.color, boundVars['fills'] ? String(boundVars['fills']) : null)
|
||||
}
|
||||
}
|
||||
for (const stroke of raw.strokes) {
|
||||
if (stroke.visible) {
|
||||
trackColor(colorMap, stroke.color, boundVars?.['strokes'] ? String(boundVars['strokes']) : null)
|
||||
trackColor(colorMap, stroke.color, boundVars['strokes'] ? String(boundVars['strokes']) : null)
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
@ -415,7 +415,7 @@ export const analyzeClusters = defineTool({
|
|||
|
||||
let confidence = 100
|
||||
if (nodes.length >= 2) {
|
||||
const base = nodes[0]!
|
||||
const base = nodes[0]
|
||||
let score = 0
|
||||
for (const n of nodes.slice(1)) {
|
||||
const sizeDiff = Math.abs(n.width - base.width) + Math.abs(n.height - base.height)
|
||||
|
|
@ -582,6 +582,7 @@ export const evalCode = defineTool({
|
|||
code: { type: 'string', description: 'JavaScript code to execute', required: true }
|
||||
},
|
||||
execute: async (figma, { code }) => {
|
||||
// eslint-disable-next-line no-empty-function
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||
const wrapped = code.trim().startsWith('return') ? code : `return (async () => { ${code} })()`
|
||||
const fn = new AsyncFunction('figma', wrapped)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const createShape = defineTool({
|
|||
POLYGON: () => figma.createPolygon(),
|
||||
SECTION: () => figma.createSection()
|
||||
}
|
||||
const node = createMap[args.type]!()
|
||||
const node = createMap[args.type]()
|
||||
node.x = args.x
|
||||
node.y = args.y
|
||||
node.resize(args.width, args.height)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export const groupNodes = defineTool({
|
|||
.map((id) => figma.getNodeById(id))
|
||||
.filter((n): n is FigmaNodeProxy => n !== null)
|
||||
if (nodes.length < 2) return { error: 'Need at least 2 nodes to group' }
|
||||
const parent = nodes[0]!.parent ?? figma.currentPage
|
||||
const parent = nodes[0].parent ?? figma.currentPage
|
||||
const group = figma.group(nodes, parent)
|
||||
return nodeSummary(group)
|
||||
}
|
||||
|
|
@ -304,7 +304,7 @@ export const arrangeNodes = defineTool({
|
|||
}
|
||||
|
||||
if (nodes.length === 0) return { error: 'No nodes to arrange' }
|
||||
const first = nodes[0]!
|
||||
const first = nodes[0]
|
||||
|
||||
if (mode === 'row') {
|
||||
let x = first.x
|
||||
|
|
@ -330,7 +330,7 @@ export const arrangeNodes = defineTool({
|
|||
let rowHeight = 0
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]!
|
||||
const node = nodes[i]
|
||||
if (i > 0 && i % cols === 0) {
|
||||
x = startX
|
||||
y += rowHeight + gap
|
||||
|
|
|
|||
|
|
@ -103,14 +103,10 @@ export const pathScale = defineTool({
|
|||
v.y = cy + (v.y - cy) * factor
|
||||
}
|
||||
for (const s of vn.segments) {
|
||||
if (s.tangentStart) {
|
||||
s.tangentStart.x *= factor
|
||||
s.tangentStart.y *= factor
|
||||
}
|
||||
if (s.tangentEnd) {
|
||||
s.tangentEnd.x *= factor
|
||||
s.tangentEnd.y *= factor
|
||||
}
|
||||
s.tangentStart.x *= factor
|
||||
s.tangentStart.y *= factor
|
||||
s.tangentEnd.x *= factor
|
||||
s.tangentEnd.y *= factor
|
||||
}
|
||||
figma.graph.updateNode(id, { vectorNetwork: vn } as any)
|
||||
return { id, factor }
|
||||
|
|
@ -142,14 +138,10 @@ export const pathFlip = defineTool({
|
|||
else v.y = h - v.y
|
||||
}
|
||||
for (const s of vn.segments) {
|
||||
if (s.tangentStart) {
|
||||
if (axis === 'horizontal') s.tangentStart.x = -s.tangentStart.x
|
||||
else s.tangentStart.y = -s.tangentStart.y
|
||||
}
|
||||
if (s.tangentEnd) {
|
||||
if (axis === 'horizontal') s.tangentEnd.x = -s.tangentEnd.x
|
||||
else s.tangentEnd.y = -s.tangentEnd.y
|
||||
}
|
||||
if (axis === 'horizontal') s.tangentStart.x = -s.tangentStart.x
|
||||
else s.tangentStart.y = -s.tangentStart.y
|
||||
if (axis === 'horizontal') s.tangentEnd.x = -s.tangentEnd.x
|
||||
else s.tangentEnd.y = -s.tangentEnd.y
|
||||
}
|
||||
figma.graph.updateNode(id, { vectorNetwork: vn } as any)
|
||||
return { id, axis }
|
||||
|
|
@ -288,7 +280,7 @@ export const exportImage = defineTool({
|
|||
args.ids && args.ids.length > 0
|
||||
? args.ids
|
||||
: figma.currentPage.children.map((n) => n.id)
|
||||
const format = ((args.format as string) ?? 'PNG').toUpperCase() as 'PNG' | 'JPG' | 'WEBP'
|
||||
const format = (args.format ?? 'PNG').toUpperCase() as 'PNG' | 'JPG' | 'WEBP'
|
||||
const data = await figma.exportImage(ids, {
|
||||
scale: args.scale ?? 1,
|
||||
format
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function decodeVectorNetworkBlob(
|
|||
vertices.push({
|
||||
x,
|
||||
y,
|
||||
handleMirroring: (override?.handleMirroring as HandleMirroring) ?? 'NONE'
|
||||
handleMirroring: (override?.handleMirroring as HandleMirroring | undefined) ?? 'NONE'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +301,7 @@ function buildChains(segments: VectorSegment[], _vertexCount: number): number[][
|
|||
let current = startVertex
|
||||
const chain: number[] = []
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
while (true) {
|
||||
const segs = adj.get(current)
|
||||
if (!segs) break
|
||||
|
|
@ -367,7 +368,7 @@ export function geometryBlobToPath(
|
|||
windingRule: WindingRule
|
||||
): Path {
|
||||
const path = new ck.Path()
|
||||
if (!blob || !(blob.buffer instanceof ArrayBuffer)) return path
|
||||
if (!(blob.buffer instanceof ArrayBuffer)) return path
|
||||
const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)
|
||||
let o = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const sessions = new Map<string, { server: ReturnType<typeof createServer>; tran
|
|||
|
||||
async function getOrCreateSession(sessionId?: string) {
|
||||
if (sessionId && sessions.has(sessionId)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by has() above
|
||||
return sessions.get(sessionId)!
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +80,7 @@ app.all('/mcp', async (c) => {
|
|||
return transport.handleRequest(c.req.raw)
|
||||
})
|
||||
|
||||
const isBun = typeof globalThis.Bun !== 'undefined'
|
||||
const isBun = 'Bun' in globalThis
|
||||
|
||||
if (isBun) {
|
||||
Bun.serve({ fetch: app.fetch, port, hostname: host })
|
||||
|
|
|
|||
|
|
@ -77,17 +77,19 @@ export function createServer(version: string, options: CreateServerOptions = {})
|
|||
|
||||
function makeFigma(): FigmaAPI {
|
||||
if (!graph) throw new Error('No document loaded. Use open_file or new_document first.')
|
||||
const api = new FigmaAPI(graph)
|
||||
const g = graph
|
||||
const api = new FigmaAPI(g)
|
||||
if (currentPageId) api.currentPage = api.wrapNode(currentPageId)
|
||||
api.exportImage = async (nodeIds, opts) => {
|
||||
const ck = await getCanvasKit()
|
||||
const surface = ck.MakeSurface(1, 1)!
|
||||
const surface = ck.MakeSurface(1, 1)
|
||||
if (!surface) throw new Error('Failed to create CanvasKit surface')
|
||||
const renderer = new SkiaRenderer(ck, surface)
|
||||
renderer.viewportWidth = 1
|
||||
renderer.viewportHeight = 1
|
||||
renderer.dpr = 1
|
||||
const pageId = currentPageId ?? graph!.getPages()[0]?.id ?? graph!.rootId
|
||||
return renderNodesToImage(ck, renderer, graph!, pageId, nodeIds, {
|
||||
const pageId = currentPageId ?? g.getPages()[0].id
|
||||
return renderNodesToImage(ck, renderer, g, pageId, nodeIds, {
|
||||
scale: opts.scale ?? 1,
|
||||
format: (opts.format ?? 'PNG') as ExportFormat
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue