chore: format codebase
This commit is contained in:
parent
a38c0ac7b5
commit
abd795bd9a
3
.github/workflows/app.yml
vendored
3
.github/workflows/app.yml
vendored
|
|
@ -22,6 +22,9 @@ jobs:
|
|||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- name: Format check
|
||||
run: bun run format:check
|
||||
|
||||
- run: bun run build
|
||||
|
||||
- uses: cloudflare/wrangler-action@v3
|
||||
|
|
|
|||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -34,6 +34,9 @@ jobs:
|
|||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- name: Format check
|
||||
run: bun run format:check
|
||||
|
||||
- name: Lint & typecheck
|
||||
run: bun run check
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"lint": "bun run lint:structure && oxlint -c oxlint.json --type-aware --type-check src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/",
|
||||
"lint:structure": "oxlint -c oxlint.json vite.config.ts vite/ src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/ tests/ scripts/",
|
||||
"format": "oxfmt --write .oxfmtrc.json vite.config.ts vite/ src/ packages/core/src/ packages/cli/src/ packages/mcp/src/ packages/vue/src/ tests scripts/",
|
||||
"format:check": "bun run format && status=$(git status --porcelain -uall) && test -z \"$status\" || (echo \"$status\" && exit 1)",
|
||||
"check": "bun run build:packages && bun run lint && tsgo --noEmit && bun run check:vue && bun run check:i18n && bun run check:packages && bun run check:arch && bun run test:dupes",
|
||||
"check:i18n": "bun scripts/check-locales.ts",
|
||||
"check:packages": "bun scripts/check-package-metadata.ts",
|
||||
|
|
|
|||
|
|
@ -119,7 +119,9 @@ async function exportFromFile(format: string, args: ExportArgs) {
|
|||
}
|
||||
|
||||
const formatId = format.toLowerCase()
|
||||
let options: { format?: string; scale?: number; quality?: number; renderThumbnail?: boolean } | undefined
|
||||
let options:
|
||||
| { format?: string; scale?: number; quality?: number; renderThumbnail?: boolean }
|
||||
| undefined
|
||||
if (format === 'JSX') {
|
||||
options = { format: args.style }
|
||||
} else if (format === 'FIG') {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Canvas } from 'canvaskit-wasm'
|
||||
|
||||
import { getAbsolutePosition, getWorldMatrix } from '#core/canvas/coordinate'
|
||||
import type { SkiaRenderer, RenderOverlays } from '#core/canvas/renderer'
|
||||
import {
|
||||
LABEL_OFFSET_Y,
|
||||
SIZE_PILL_PADDING_X,
|
||||
|
|
@ -12,8 +13,6 @@ import {
|
|||
import { rotatedCorners } from '#core/geometry'
|
||||
import type { SceneNode, SceneGraph } from '#core/scene-graph'
|
||||
|
||||
import type { SkiaRenderer, RenderOverlays } from '#core/canvas/renderer'
|
||||
|
||||
import { ellipsizeLabelText } from './text'
|
||||
|
||||
function getOverlayRotation(node: SceneNode, overlays?: RenderOverlays): number {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { Canvas } from 'canvaskit-wasm'
|
||||
|
||||
import { drawNodeHighlightRect } from '#core/canvas/highlight-rect'
|
||||
import type { SkiaRenderer } from '#core/canvas/renderer'
|
||||
import {
|
||||
AI_ACTIVE_COLOR,
|
||||
AI_DONE_COLOR,
|
||||
|
|
@ -8,9 +10,6 @@ import {
|
|||
} from '#core/constants'
|
||||
import type { SceneGraph } from '#core/scene-graph'
|
||||
|
||||
import { drawNodeHighlightRect } from '#core/canvas/highlight-rect'
|
||||
import type { SkiaRenderer } from '#core/canvas/renderer'
|
||||
|
||||
export function drawAiOverlays(r: SkiaRenderer, canvas: Canvas, graph: SceneGraph): void {
|
||||
const now = performance.now()
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,13 @@ function toScreenRect(r: SkiaRenderer, [x, y, width, height]: RectTuple) {
|
|||
)
|
||||
}
|
||||
|
||||
function drawStripedRect(r: SkiaRenderer, canvas: Canvas, rectTuple: RectTuple, color: Color, fill: Color) {
|
||||
function drawStripedRect(
|
||||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
rectTuple: RectTuple,
|
||||
color: Color,
|
||||
fill: Color
|
||||
) {
|
||||
const [, , width, height] = rectTuple
|
||||
if (width <= 0 || height <= 0) return
|
||||
const rect = toScreenRect(r, rectTuple)
|
||||
|
|
@ -209,13 +215,7 @@ function drawSpacingHover(
|
|||
) {
|
||||
const rects = gapRects(node, graph)
|
||||
for (const rect of rects) {
|
||||
drawStripedRect(
|
||||
r,
|
||||
canvas,
|
||||
rect,
|
||||
AUTO_LAYOUT_HOVER_MAGENTA,
|
||||
AUTO_LAYOUT_HOVER_MAGENTA_FILL
|
||||
)
|
||||
drawStripedRect(r, canvas, rect, AUTO_LAYOUT_HOVER_MAGENTA, AUTO_LAYOUT_HOVER_MAGENTA_FILL)
|
||||
}
|
||||
if (!showValue || rects.length === 0) return
|
||||
const [x, y, width, height] = rects[0]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { ImageFilter, MaskFilter, Canvas, Paint, Path } from 'canvaskit-wasm'
|
||||
|
||||
import * as AiOverlays from '#core/canvas/overlays/ai'
|
||||
import * as Effects from '#core/canvas/effects'
|
||||
import * as Fills from '#core/canvas/fills'
|
||||
import * as Labels from '#core/canvas/labels/draw'
|
||||
import * as NodeEditOverlay from '#core/canvas/node-edit-overlay'
|
||||
import type { NodeEditOverlayState } from '#core/canvas/node-edit-overlay'
|
||||
import * as Overlays from '#core/canvas/overlays'
|
||||
import * as AiOverlays from '#core/canvas/overlays/ai'
|
||||
import * as PenOverlay from '#core/canvas/pen-overlay'
|
||||
import type { SkiaRenderer } from '#core/canvas/renderer'
|
||||
import type { RenderOverlays } from '#core/canvas/renderer/types'
|
||||
|
|
@ -95,7 +95,11 @@ const rendererMethods: ThisType<SkiaRenderer> = {
|
|||
Overlays.drawLayoutInsertIndicator(this, canvas, indicator)
|
||||
},
|
||||
|
||||
drawAutoLayoutHover(canvas: Canvas, graph: SceneGraph, hover?: RenderOverlays['autoLayoutHover']) {
|
||||
drawAutoLayoutHover(
|
||||
canvas: Canvas,
|
||||
graph: SceneGraph,
|
||||
hover?: RenderOverlays['autoLayoutHover']
|
||||
) {
|
||||
Overlays.drawAutoLayoutHover(this, canvas, graph, hover)
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,11 @@ export function render(
|
|||
canvas.scale(r.dpr, r.dpr)
|
||||
|
||||
p.beginPhase('render:scene')
|
||||
if (layer === 'scene' && !hasVolatileOverlays && renderSceneBacking(r, canvas, graph, sceneVersion)) {
|
||||
if (
|
||||
layer === 'scene' &&
|
||||
!hasVolatileOverlays &&
|
||||
renderSceneBacking(r, canvas, graph, sceneVersion)
|
||||
) {
|
||||
p.setScenePictureMode('hit', 'backing')
|
||||
} else {
|
||||
canvas.translate(r.panX, r.panY)
|
||||
|
|
|
|||
|
|
@ -45,10 +45,7 @@ export function updateSceneBackingPreviewState(r: SkiaRenderer, layer: RenderLay
|
|||
if (layer !== 'scene') return
|
||||
const previous = r.lastSceneViewport
|
||||
const viewportChanged =
|
||||
!previous ||
|
||||
previous.panX !== r.panX ||
|
||||
previous.panY !== r.panY ||
|
||||
previous.zoom !== r.zoom
|
||||
!previous || previous.panX !== r.panX || previous.panY !== r.panY || previous.zoom !== r.zoom
|
||||
if (viewportChanged) {
|
||||
const timestamp = now()
|
||||
if (r.sceneBackingLastViewportEventAt > 0) {
|
||||
|
|
@ -85,7 +82,12 @@ function backingScreenCoverageContainsViewport(r: SkiaRenderer): boolean {
|
|||
const scale = r.zoom / backing.zoom
|
||||
const x = r.panX - backing.panX * scale
|
||||
const y = r.panY - backing.panY * scale
|
||||
return x <= 0 && y <= 0 && x + backing.width * scale >= r.viewportWidth && y + backing.height * scale >= r.viewportHeight
|
||||
return (
|
||||
x <= 0 &&
|
||||
y <= 0 &&
|
||||
x + backing.width * scale >= r.viewportWidth &&
|
||||
y + backing.height * scale >= r.viewportHeight
|
||||
)
|
||||
}
|
||||
|
||||
function backingWorldCoverageContainsLiveViewport(r: SkiaRenderer): boolean {
|
||||
|
|
@ -129,12 +131,7 @@ function drawSceneBacking(
|
|||
const backing = r.sceneBacking
|
||||
if (
|
||||
!backing ||
|
||||
!backingCoverageContainsLiveViewport(
|
||||
r,
|
||||
sceneVersion,
|
||||
allowStaleZoom,
|
||||
positionPreviewVersion
|
||||
)
|
||||
!backingCoverageContainsLiveViewport(r, sceneVersion, allowStaleZoom, positionPreviewVersion)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -464,4 +461,3 @@ export function renderSceneBacking(
|
|||
positionPreviewVersion
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ type SmoothCornerPathParams = {
|
|||
arcSectionLength: number
|
||||
}
|
||||
|
||||
function smoothCornerRadii(node: SceneNode, width: number, height: number, spread: number): Record<SmoothCornerKey, SmoothCorner> {
|
||||
function smoothCornerRadii(
|
||||
node: SceneNode,
|
||||
width: number,
|
||||
height: number,
|
||||
spread: number
|
||||
): Record<SmoothCornerKey, SmoothCorner> {
|
||||
const radius = (value: number) => Math.max(0, value + spread)
|
||||
const radii: Record<SmoothCornerKey, number> = node.independentCorners
|
||||
? {
|
||||
|
|
@ -63,7 +68,11 @@ function smoothCornerRadii(node: SceneNode, width: number, height: number, sprea
|
|||
bottomLeft: radius(node.cornerRadius)
|
||||
}
|
||||
|
||||
if (radii.topLeft === radii.topRight && radii.topRight === radii.bottomRight && radii.bottomRight === radii.bottomLeft) {
|
||||
if (
|
||||
radii.topLeft === radii.topRight &&
|
||||
radii.topRight === radii.bottomRight &&
|
||||
radii.bottomRight === radii.bottomLeft
|
||||
) {
|
||||
const budget = Math.min(width, height) / 2
|
||||
const clampedRadius = Math.min(radii.topLeft, budget)
|
||||
return {
|
||||
|
|
@ -80,7 +89,10 @@ function smoothCornerRadii(node: SceneNode, width: number, height: number, sprea
|
|||
bottomRight: -1,
|
||||
bottomLeft: -1
|
||||
}
|
||||
const adjacentByCorner: Record<SmoothCornerKey, Array<{ corner: SmoothCornerKey; sideLength: number }>> = {
|
||||
const adjacentByCorner: Record<
|
||||
SmoothCornerKey,
|
||||
Array<{ corner: SmoothCornerKey; sideLength: number }>
|
||||
> = {
|
||||
topLeft: [
|
||||
{ corner: 'topRight', sideLength: width },
|
||||
{ corner: 'bottomLeft', sideLength: height }
|
||||
|
|
@ -99,7 +111,9 @@ function smoothCornerRadii(node: SceneNode, width: number, height: number, sprea
|
|||
]
|
||||
}
|
||||
|
||||
for (const corner of (Object.keys(radii) as SmoothCornerKey[]).sort((a, b) => radii[b] - radii[a])) {
|
||||
for (const corner of (Object.keys(radii) as SmoothCornerKey[]).sort(
|
||||
(a, b) => radii[b] - radii[a]
|
||||
)) {
|
||||
const cornerRadius = radii[corner]
|
||||
const budget = Math.min(
|
||||
...adjacentByCorner[corner].map((adjacent) => {
|
||||
|
|
@ -154,34 +168,115 @@ function smoothCornerPathParams(corner: SmoothCorner, smoothing: number): Smooth
|
|||
}
|
||||
}
|
||||
|
||||
function drawTopRightSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
function drawTopRightSmoothCorner(
|
||||
path: Path,
|
||||
corner: SmoothCornerPathParams,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
if (corner.radius === 0) {
|
||||
path.lineTo(x + corner.p, y)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x + corner.a, y, x + corner.a + corner.b, y, x + corner.a + corner.b + corner.c, y + corner.d)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x + corner.p - corner.d, y + corner.p - corner.a - corner.b - corner.c)
|
||||
path.cubicTo(x + corner.p, y + corner.p - corner.a - corner.b, x + corner.p, y + corner.p - corner.a, x + corner.p, y + corner.p)
|
||||
path.cubicTo(
|
||||
x + corner.a,
|
||||
y,
|
||||
x + corner.a + corner.b,
|
||||
y,
|
||||
x + corner.a + corner.b + corner.c,
|
||||
y + corner.d
|
||||
)
|
||||
path.arcToRotated(
|
||||
corner.radius,
|
||||
corner.radius,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
x + corner.p - corner.d,
|
||||
y + corner.p - corner.a - corner.b - corner.c
|
||||
)
|
||||
path.cubicTo(
|
||||
x + corner.p,
|
||||
y + corner.p - corner.a - corner.b,
|
||||
x + corner.p,
|
||||
y + corner.p - corner.a,
|
||||
x + corner.p,
|
||||
y + corner.p
|
||||
)
|
||||
}
|
||||
|
||||
function drawBottomRightSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
function drawBottomRightSmoothCorner(
|
||||
path: Path,
|
||||
corner: SmoothCornerPathParams,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
if (corner.radius === 0) {
|
||||
path.lineTo(x, y + corner.p)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x, y + corner.a, x, y + corner.a + corner.b, x - corner.d, y + corner.a + corner.b + corner.c)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x - corner.p + corner.a + corner.b + corner.c, y + corner.p - corner.d)
|
||||
path.cubicTo(x - corner.p + corner.a + corner.b, y + corner.p, x - corner.p + corner.a, y + corner.p, x - corner.p, y + corner.p)
|
||||
path.cubicTo(
|
||||
x,
|
||||
y + corner.a,
|
||||
x,
|
||||
y + corner.a + corner.b,
|
||||
x - corner.d,
|
||||
y + corner.a + corner.b + corner.c
|
||||
)
|
||||
path.arcToRotated(
|
||||
corner.radius,
|
||||
corner.radius,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
x - corner.p + corner.a + corner.b + corner.c,
|
||||
y + corner.p - corner.d
|
||||
)
|
||||
path.cubicTo(
|
||||
x - corner.p + corner.a + corner.b,
|
||||
y + corner.p,
|
||||
x - corner.p + corner.a,
|
||||
y + corner.p,
|
||||
x - corner.p,
|
||||
y + corner.p
|
||||
)
|
||||
}
|
||||
|
||||
function drawBottomLeftSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
function drawBottomLeftSmoothCorner(
|
||||
path: Path,
|
||||
corner: SmoothCornerPathParams,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
if (corner.radius === 0) {
|
||||
path.lineTo(x - corner.p, y)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x - corner.a, y, x - corner.a - corner.b, y, x - corner.a - corner.b - corner.c, y - corner.d)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x - corner.p + corner.d, y - corner.p + corner.a + corner.b + corner.c)
|
||||
path.cubicTo(x - corner.p, y - corner.p + corner.a + corner.b, x - corner.p, y - corner.p + corner.a, x - corner.p, y - corner.p)
|
||||
path.cubicTo(
|
||||
x - corner.a,
|
||||
y,
|
||||
x - corner.a - corner.b,
|
||||
y,
|
||||
x - corner.a - corner.b - corner.c,
|
||||
y - corner.d
|
||||
)
|
||||
path.arcToRotated(
|
||||
corner.radius,
|
||||
corner.radius,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
x - corner.p + corner.d,
|
||||
y - corner.p + corner.a + corner.b + corner.c
|
||||
)
|
||||
path.cubicTo(
|
||||
x - corner.p,
|
||||
y - corner.p + corner.a + corner.b,
|
||||
x - corner.p,
|
||||
y - corner.p + corner.a,
|
||||
x - corner.p,
|
||||
y - corner.p
|
||||
)
|
||||
}
|
||||
|
||||
function drawTopLeftSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
|
|
@ -189,9 +284,31 @@ function drawTopLeftSmoothCorner(path: Path, corner: SmoothCornerPathParams, x:
|
|||
path.lineTo(x, y - corner.p)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x, y - corner.a, x, y - corner.a - corner.b, x + corner.d, y - corner.a - corner.b - corner.c)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x + corner.p - corner.a - corner.b - corner.c, y - corner.p + corner.d)
|
||||
path.cubicTo(x + corner.p - corner.a - corner.b, y - corner.p, x + corner.p - corner.a, y - corner.p, x + corner.p, y - corner.p)
|
||||
path.cubicTo(
|
||||
x,
|
||||
y - corner.a,
|
||||
x,
|
||||
y - corner.a - corner.b,
|
||||
x + corner.d,
|
||||
y - corner.a - corner.b - corner.c
|
||||
)
|
||||
path.arcToRotated(
|
||||
corner.radius,
|
||||
corner.radius,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
x + corner.p - corner.a - corner.b - corner.c,
|
||||
y - corner.p + corner.d
|
||||
)
|
||||
path.cubicTo(
|
||||
x + corner.p - corner.a - corner.b,
|
||||
y - corner.p,
|
||||
x + corner.p - corner.a,
|
||||
y - corner.p,
|
||||
x + corner.p,
|
||||
y - corner.p
|
||||
)
|
||||
}
|
||||
|
||||
export function makeSmoothRRectPath(
|
||||
|
|
|
|||
|
|
@ -205,7 +205,14 @@ export function drawRRectStrokeWithAlign(
|
|||
stroke: Stroke
|
||||
): void {
|
||||
if (nodeHasSmoothCorners(node)) {
|
||||
drawStrokeWithAlign(r, canvas, node, r.ck.LTRBRect(0, 0, node.width, node.height), true, stroke.align)
|
||||
drawStrokeWithAlign(
|
||||
r,
|
||||
canvas,
|
||||
node,
|
||||
r.ck.LTRBRect(0, 0, node.width, node.height),
|
||||
true,
|
||||
stroke.align
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,11 +24,7 @@ export function derivedUnderlineRect(node: Pick<SceneNode, 'width'>, baselineY:
|
|||
}
|
||||
}
|
||||
|
||||
export function drawFigmaDerivedText(
|
||||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
node: SceneNode
|
||||
): boolean {
|
||||
export function drawFigmaDerivedText(r: SkiaRenderer, canvas: Canvas, node: SceneNode): boolean {
|
||||
if (!node.figmaDerivedTextGlyphs?.length) return false
|
||||
|
||||
let underlineBaselineY = 0
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import type { Path } from 'canvaskit-wasm'
|
||||
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
import { textNodeToOutlineLayout } from '#core/text/outlines'
|
||||
import type { OutlineCommand } from '#core/text/opentype'
|
||||
import { textNodeToOutlineLayout } from '#core/text/outlines'
|
||||
|
||||
import type { SkiaRenderer } from './renderer'
|
||||
|
||||
function appendOutlineCommand(path: Path, command: OutlineCommand, xOffset: number, yOffset: number): void {
|
||||
function appendOutlineCommand(
|
||||
path: Path,
|
||||
command: OutlineCommand,
|
||||
xOffset: number,
|
||||
yOffset: number
|
||||
): void {
|
||||
switch (command.type) {
|
||||
case 'M':
|
||||
path.moveTo((command.x ?? 0) + xOffset, (command.y ?? 0) + yOffset)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { inflateSync, deflateSync } from 'fflate'
|
||||
|
||||
import { shapeTextForClipboard } from './canvas/text'
|
||||
import { initCodec, getCompiledSchema, getSchemaBytes } from './kiwi/fig/codec'
|
||||
import type { NodeChange as KiwiNodeChange } from './kiwi/fig/codec'
|
||||
import { populateAndApplyOverrides } from './kiwi/fig/instance-overrides'
|
||||
import type { InstanceNodeChange } from './kiwi/fig/instance-overrides'
|
||||
import { decodeBinarySchema, compileSchema, ByteBuffer } from './kiwi/schema-runtime'
|
||||
import { nodeChangeToProps, sortChildren } from './kiwi/fig/node-change/convert'
|
||||
import {
|
||||
sceneNodeToKiwi,
|
||||
|
|
@ -15,9 +15,9 @@ import {
|
|||
makeCanvasNodeChange,
|
||||
buildFontDigestMap
|
||||
} from './kiwi/fig/node-change/serialize'
|
||||
import { decodeBinarySchema, compileSchema, ByteBuffer } from './kiwi/schema-runtime'
|
||||
import { randomInt } from './random'
|
||||
import type { SceneGraph, SceneNode } from './scene-graph'
|
||||
import { shapeTextForClipboard } from './canvas/text'
|
||||
import { buildDerivedTextDataV4 } from './text/derived-text/clipboard'
|
||||
|
||||
interface FigmaClipboardMeta {
|
||||
|
|
@ -337,7 +337,10 @@ export async function buildFigmaClipboardHTML(
|
|||
if (!source) return
|
||||
change.textAutoResize = 'NONE'
|
||||
change.textUserLayoutVersion = 5
|
||||
change.lineHeight = { value: source.lineHeight ?? 100, units: source.lineHeight ? 'PIXELS' : 'PERCENT' }
|
||||
change.lineHeight = {
|
||||
value: source.lineHeight ?? 100,
|
||||
units: source.lineHeight ? 'PIXELS' : 'PERCENT'
|
||||
}
|
||||
const shaped = await shapeTextForClipboard(source).catch(() => null)
|
||||
change.derivedTextData = await buildDerivedTextDataV4(source, fontDigestMap, shaped, blobs)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -395,10 +395,7 @@ function applyTextStyleOverrides(props: Record<string, unknown>, o: Partial<Scen
|
|||
applyTextAlignmentOverrides(props, o)
|
||||
}
|
||||
|
||||
function applyTextAlignmentOverrides(
|
||||
props: Record<string, unknown>,
|
||||
o: Partial<SceneNode>
|
||||
): void {
|
||||
function applyTextAlignmentOverrides(props: Record<string, unknown>, o: Partial<SceneNode>): void {
|
||||
const textAlign = props.textAlign ?? props.textAlignHorizontal ?? props.textHorizontalAlignment
|
||||
if (typeof textAlign === 'string') {
|
||||
o.textAlignHorizontal = TEXT_ALIGN_ALIAS_MAP[textAlign.toLowerCase()] ?? 'LEFT'
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import type { Vector } from '#core/types'
|
|||
import { createClipboardCopyActions } from './clipboard/copy'
|
||||
import { createClipboardExportActions } from './clipboard/export'
|
||||
import { createClipboardFontActions } from './clipboard/fonts'
|
||||
import { createClipboardImageActions } from './clipboard/images'
|
||||
import { deleteIds, recreateSnapshots, restoreDeletedEntries } from './clipboard/history'
|
||||
import { createClipboardImageActions } from './clipboard/images'
|
||||
import { replaceTargetsWithCreated, selectedReplacementTargets } from './clipboard/paste-replace'
|
||||
import { resolvePasteTarget } from './clipboard/paste-target'
|
||||
import { createClipboardPlacementActions } from './clipboard/placement'
|
||||
import { replaceTargetsWithCreated, selectedReplacementTargets } from './clipboard/paste-replace'
|
||||
import { collectSubtrees, restoreSubtree, snapshotSubtree } from './clipboard/subtree-history'
|
||||
import type { EditorContext } from './types'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import type { EditorContext } from '#core/editor/types'
|
||||
import { computeAbsoluteBounds } from '#core/geometry'
|
||||
import { computeAllLayouts } from '#core/layout'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
|
||||
import type { EditorContext } from '#core/editor/types'
|
||||
import { deleteIds, type DeletedEntry, recreateSnapshots, restoreDeletedEntries } from './history'
|
||||
import { collectSubtrees, snapshotSubtree } from './subtree-history'
|
||||
import {
|
||||
deleteIds,
|
||||
type DeletedEntry,
|
||||
recreateSnapshots,
|
||||
restoreDeletedEntries
|
||||
} from './history'
|
||||
|
||||
type CenterNodesAt = (nodeIds: string[], cx: number, cy: number) => void
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { omit } from 'es-toolkit/object'
|
|||
|
||||
import type { EditorContext } from '#core/editor/types'
|
||||
import { randomHex } from '#core/random'
|
||||
import { buildVariantName, parseVariantName } from '#core/scene-graph/variant-name'
|
||||
import type {
|
||||
ComponentPropertyDefinition,
|
||||
ComponentPropertyType,
|
||||
SceneNode
|
||||
} from '#core/scene-graph'
|
||||
import { buildVariantName, parseVariantName } from '#core/scene-graph/variant-name'
|
||||
|
||||
export type VariantConflict = {
|
||||
values: Record<string, string>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ export function selectedNodesInSharedParent(ctx: EditorContext, selectedNodes: S
|
|||
if (topLevel.length === 0 || topLevel.some((node) => node.locked)) return null
|
||||
|
||||
const parentId = topLevel[0].parentId ?? ctx.state.currentPageId
|
||||
if (!topLevel.every((node) => (node.parentId ?? ctx.state.currentPageId) === parentId)) return null
|
||||
if (!topLevel.every((node) => (node.parentId ?? ctx.state.currentPageId) === parentId))
|
||||
return null
|
||||
|
||||
const parent = ctx.graph.getNode(parentId)
|
||||
return parent ? { topLevel, parentId, parent } : null
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
snapshotPage as createPageSnapshot,
|
||||
type PageSnapshot
|
||||
} from './history/snapshot'
|
||||
|
||||
import type { EditorContext } from './types'
|
||||
|
||||
export function createUndoActions(ctx: EditorContext) {
|
||||
|
|
|
|||
|
|
@ -219,7 +219,11 @@ export class FigmaAPI implements NodeProxyHost {
|
|||
return (node as BaseNode & { [INTERNAL_ID]: string })[INTERNAL_ID]
|
||||
}
|
||||
|
||||
group(nodes: ReadonlyArray<FigmaNodeProxy>, parent: FigmaNodeProxy, index?: number): FigmaGroupNode
|
||||
group(
|
||||
nodes: ReadonlyArray<FigmaNodeProxy>,
|
||||
parent: FigmaNodeProxy,
|
||||
index?: number
|
||||
): FigmaGroupNode
|
||||
group(nodes: ReadonlyArray<BaseNode>, parent: BaseNode & ChildrenMixin, index?: number): GroupNode
|
||||
group(
|
||||
nodes: ReadonlyArray<BaseNode | FigmaNodeProxy>,
|
||||
|
|
@ -424,8 +428,16 @@ export class FigmaAPI implements NodeProxyHost {
|
|||
|
||||
// --- Flatten ---
|
||||
|
||||
flatten(nodes: ReadonlyArray<FigmaNodeProxy>, parent?: FigmaNodeProxy, index?: number): FigmaVectorNode
|
||||
flatten(nodes: ReadonlyArray<BaseNode>, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode
|
||||
flatten(
|
||||
nodes: ReadonlyArray<FigmaNodeProxy>,
|
||||
parent?: FigmaNodeProxy,
|
||||
index?: number
|
||||
): FigmaVectorNode
|
||||
flatten(
|
||||
nodes: ReadonlyArray<BaseNode>,
|
||||
parent?: BaseNode & ChildrenMixin,
|
||||
index?: number
|
||||
): VectorNode
|
||||
flatten(
|
||||
nodes: ReadonlyArray<BaseNode | FigmaNodeProxy>,
|
||||
parent?: (BaseNode & ChildrenMixin) | FigmaNodeProxy,
|
||||
|
|
|
|||
|
|
@ -118,7 +118,9 @@ export function extractPaths(svgBody: string): IconPathInfo[] {
|
|||
d,
|
||||
fill: resolveAttr(attrValue(tag, 'fill'), groupAttrs.fill, 'currentColor'),
|
||||
stroke: resolveAttr(attrValue(tag, 'stroke'), groupAttrs.stroke, null),
|
||||
strokeWidth: Number.parseFloat(attrValue(tag, 'stroke-width') ?? groupAttrs.strokeWidth ?? '1'),
|
||||
strokeWidth: Number.parseFloat(
|
||||
attrValue(tag, 'stroke-width') ?? groupAttrs.strokeWidth ?? '1'
|
||||
),
|
||||
strokeCap: attrValue(tag, 'stroke-linecap') ?? groupAttrs.strokeCap ?? 'butt',
|
||||
strokeJoin: attrValue(tag, 'stroke-linejoin') ?? groupAttrs.strokeJoin ?? 'miter',
|
||||
fillRule: fillRuleAttr === 'evenodd' ? 'EVENODD' : 'NONZERO'
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { deflateSync } from 'fflate'
|
|||
import type { SkiaRenderer } from '#core/canvas'
|
||||
import { CANVAS_BG_COLOR, IS_BROWSER, IS_TAURI } from '#core/constants'
|
||||
import { renderThumbnail } from '#core/io/formats/raster'
|
||||
import { populateAllLazyFigImportRoots } from '#core/kiwi/fig/lazy-import'
|
||||
import { initCodec, getCompiledSchema, getSchemaBytes } from '#core/kiwi/fig/codec'
|
||||
import type { NodeChange } from '#core/kiwi/fig/codec'
|
||||
import { populateAllLazyFigImportRoots } from '#core/kiwi/fig/lazy-import'
|
||||
import { stringToGuid } from '#core/kiwi/fig/node-change/convert'
|
||||
import { buildFigmaPaintVariableColorMap } from '#core/kiwi/fig/node-change/export-node'
|
||||
import {
|
||||
|
|
@ -91,11 +91,17 @@ async function renderFigThumbnail(
|
|||
): Promise<Uint8Array> {
|
||||
if (!pageId) return THUMBNAIL_1X1
|
||||
if (ck && renderer) {
|
||||
return renderThumbnail(ck, renderer, graph, pageId, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) ?? THUMBNAIL_1X1
|
||||
return (
|
||||
renderThumbnail(ck, renderer, graph, pageId, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) ??
|
||||
THUMBNAIL_1X1
|
||||
)
|
||||
}
|
||||
if (!renderHeadless || IS_BROWSER || IS_TAURI) return THUMBNAIL_1X1
|
||||
const { headlessRenderThumbnail } = await import('#core/io/formats/raster')
|
||||
return (await headlessRenderThumbnail(graph, pageId, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)) ?? THUMBNAIL_1X1
|
||||
return (
|
||||
(await headlessRenderThumbnail(graph, pageId, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)) ??
|
||||
THUMBNAIL_1X1
|
||||
)
|
||||
}
|
||||
|
||||
function assignVariableGuids(
|
||||
|
|
|
|||
|
|
@ -77,11 +77,7 @@ function applyPadding(style: Record<string, string>, node: SceneNode): void {
|
|||
else style.padding = `${px(pt)} ${px(pr)} ${px(pb)} ${px(pl)}`
|
||||
}
|
||||
|
||||
function applyLayoutStyle(
|
||||
style: Record<string, string>,
|
||||
node: SceneNode,
|
||||
graph: SceneGraph
|
||||
): void {
|
||||
function applyLayoutStyle(style: Record<string, string>, node: SceneNode, graph: SceneGraph): void {
|
||||
const ctx = getNodeContext(node, graph)
|
||||
|
||||
if (ctx.isGrid) {
|
||||
|
|
@ -143,11 +139,9 @@ function applyAppearanceStyle(style: Record<string, string>, node: SceneNode): v
|
|||
function applyTextStyle(style: Record<string, string>, node: SceneNode): void {
|
||||
if (node.type !== 'TEXT') return
|
||||
style.fontSize = px(node.fontSize)
|
||||
if (node.fontFamily && node.fontFamily !== DEFAULT_FONT_FAMILY)
|
||||
style.fontFamily = node.fontFamily
|
||||
if (node.fontFamily && node.fontFamily !== DEFAULT_FONT_FAMILY) style.fontFamily = node.fontFamily
|
||||
if (node.fontWeight !== 400) style.fontWeight = String(node.fontWeight)
|
||||
if (node.textAlignHorizontal !== 'LEFT')
|
||||
style.textAlign = node.textAlignHorizontal.toLowerCase()
|
||||
if (node.textAlignHorizontal !== 'LEFT') style.textAlign = node.textAlignHorizontal.toLowerCase()
|
||||
const textColor = solidFillColor(node.fills)
|
||||
if (textColor) style.color = textColor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { polygonVertices } from '#core/geometry'
|
||||
import { nodeHasRadius } from '#core/canvas/shapes'
|
||||
import { polygonVertices } from '#core/geometry'
|
||||
import type { SceneNode, VectorNetwork, VectorSegment, VectorVertex } from '#core/scene-graph'
|
||||
|
||||
const CMD_CLOSE = 0
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { isNotNil } from 'es-toolkit/predicate'
|
||||
|
||||
import { BLACK } from '#core/constants'
|
||||
import { setLazyFigImportContext } from '#core/kiwi/fig/lazy-import'
|
||||
import type { NodeChange, VariableDataValuesEntry, Color, GUID } from '#core/kiwi/fig/codec'
|
||||
import { populateAndApplyOverrides } from '#core/kiwi/fig/instance-overrides'
|
||||
import type { InstanceNodeChange } from '#core/kiwi/fig/instance-overrides'
|
||||
import { setLazyFigImportContext } from '#core/kiwi/fig/lazy-import'
|
||||
import {
|
||||
guidToString,
|
||||
nodeChangeToProps,
|
||||
|
|
@ -19,10 +19,14 @@ import type { VariableType, VariableValue } from '#core/scene-graph'
|
|||
type AssetRef = { key: string; version?: string }
|
||||
type AliasRef = { guid?: GUID; assetRef?: AssetRef }
|
||||
|
||||
function applyImportedCanvasMetadata(page: ReturnType<SceneGraph['addPage']>, canvasNc: NodeChange) {
|
||||
function applyImportedCanvasMetadata(
|
||||
page: ReturnType<SceneGraph['addPage']>,
|
||||
canvasNc: NodeChange
|
||||
) {
|
||||
page.source.format = 'fig'
|
||||
page.source.orderKey = canvasNc.parentIndex?.position ?? null
|
||||
if (canvasNc.backgroundColor) page.source.fig.rawNodeFields.backgroundColor = structuredClone(canvasNc.backgroundColor)
|
||||
if (canvasNc.backgroundColor)
|
||||
page.source.fig.rawNodeFields.backgroundColor = structuredClone(canvasNc.backgroundColor)
|
||||
page.source.fig.rawNodeFields.strokeJoin = canvasNc.strokeJoin
|
||||
page.source.fig.rawNodeFields.strokeWeight = canvasNc.strokeWeight
|
||||
if (canvasNc.pageType) page.source.fig.rawNodeFields.pageType = canvasNc.pageType
|
||||
|
|
@ -476,7 +480,8 @@ export function importNodeChanges(
|
|||
)
|
||||
})
|
||||
|
||||
if (activeRootIds) rememberLazyFigImportContext(graph, changeMap, guidToNodeId, blobs, activeRootIds)
|
||||
if (activeRootIds)
|
||||
rememberLazyFigImportContext(graph, changeMap, guidToNodeId, blobs, activeRootIds)
|
||||
|
||||
setVariableColorResolver(null)
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ function applySwapProp(
|
|||
val: ComponentPropValue,
|
||||
modified?: Set<string>
|
||||
): void {
|
||||
const swapId = propTextCharacters(val) ?? (val.guidValue ? guidToString(val.guidValue) : undefined)
|
||||
const swapId =
|
||||
propTextCharacters(val) ?? (val.guidValue ? guidToString(val.guidValue) : undefined)
|
||||
const newCompId = swapId ? ctx.guidToNodeId.get(swapId) : undefined
|
||||
if (!newCompId) return
|
||||
applyPatchAndMark(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import {
|
||||
applyInstanceDirectAssignments,
|
||||
applyOverrideAssignments
|
||||
} from './assignments'
|
||||
import { collectAssignmentsMap, collectPropRefsMap } from './maps'
|
||||
import type { OverrideContext } from '#core/kiwi/fig/instance-overrides/types'
|
||||
|
||||
import { applyInstanceDirectAssignments, applyOverrideAssignments } from './assignments'
|
||||
import { collectAssignmentsMap, collectPropRefsMap } from './maps'
|
||||
|
||||
/**
|
||||
* Apply all component property assignments (visibility toggles, instance swaps).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import type { GUID } from '#core/kiwi/fig/codec'
|
||||
import { guidToString } from '#core/kiwi/fig/node-change/convert'
|
||||
|
||||
import type {
|
||||
ComponentPropAssignment,
|
||||
ComponentPropValue,
|
||||
OverrideContext
|
||||
} from '#core/kiwi/fig/instance-overrides/types'
|
||||
import { guidToString } from '#core/kiwi/fig/node-change/convert'
|
||||
|
||||
export function normalizePropName(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
|
|
@ -42,7 +41,8 @@ function resolveAssignmentValue(
|
|||
if (variableValue?.symbolIdValue?.guid) return { guidValue: variableValue.symbolIdValue.guid }
|
||||
if (variableValue?.boolValue !== undefined) return { boolValue: variableValue.boolValue }
|
||||
if (variableValue?.textValue !== undefined) return { textValue: variableValue.textValue }
|
||||
if (variableValue?.textDataValue !== undefined) return { textDataValue: variableValue.textDataValue }
|
||||
if (variableValue?.textDataValue !== undefined)
|
||||
return { textDataValue: variableValue.textDataValue }
|
||||
|
||||
return resolveDefaults ? (ctx.propDefaults.get(key) ?? assignment.value) : assignment.value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { resolveGeometryPaths } from '#core/kiwi/fig/node-change/convert'
|
||||
import type { DerivedSymbolOverride } from '#core/kiwi/fig/instance-overrides/types'
|
||||
import { resolveGeometryPaths } from '#core/kiwi/fig/node-change/convert'
|
||||
import type { GeometryPath, SceneNode } from '#core/scene-graph'
|
||||
|
||||
function scaleGeometryBlobs(geom: GeometryPath[], sx: number, sy: number): GeometryPath[] {
|
||||
|
|
@ -36,12 +36,20 @@ export function resolveDsdGeometry(
|
|||
|
||||
if (fg.length > 0) result.fillGeometry = fg
|
||||
else if (d.size && target.fillGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
result.fillGeometry = scaleGeometryBlobs(target.fillGeometry, d.size.x / target.width, d.size.y / target.height)
|
||||
result.fillGeometry = scaleGeometryBlobs(
|
||||
target.fillGeometry,
|
||||
d.size.x / target.width,
|
||||
d.size.y / target.height
|
||||
)
|
||||
}
|
||||
|
||||
if (sg.length > 0) result.strokeGeometry = sg
|
||||
else if (d.size && target.strokeGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
result.strokeGeometry = scaleGeometryBlobs(target.strokeGeometry, d.size.x / target.width, d.size.y / target.height)
|
||||
result.strokeGeometry = scaleGeometryBlobs(
|
||||
target.strokeGeometry,
|
||||
d.size.x / target.width,
|
||||
d.size.y / target.height
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { applyOverridePatch } from '#core/kiwi/fig/instance-overrides/patches'
|
||||
import { resolveOverrideTarget } from '#core/kiwi/fig/instance-overrides/resolve'
|
||||
import type { DerivedSymbolOverride, OverrideContext } from '#core/kiwi/fig/instance-overrides/types'
|
||||
import type {
|
||||
DerivedSymbolOverride,
|
||||
OverrideContext
|
||||
} from '#core/kiwi/fig/instance-overrides/types'
|
||||
|
||||
import { buildDsdLayoutUpdates } from './layout'
|
||||
import { propagateDsdChanges } from './propagate'
|
||||
|
|
@ -45,7 +48,8 @@ function resolveDsdUpdates(ctx: OverrideContext): { modified: Set<string>; sizeS
|
|||
const nodeId = ctx.guidToNodeId.get(ncId)
|
||||
if (!nodeId || (ctx.activeNodeIds && !ctx.activeNodeIds.has(nodeId))) continue
|
||||
|
||||
for (const d of derived) applyDsdOverride(ctx, visibleSiblingCount, nodeId, d, modified, sizeSet)
|
||||
for (const d of derived)
|
||||
applyDsdOverride(ctx, visibleSiblingCount, nodeId, d, modified, sizeSet)
|
||||
}
|
||||
|
||||
return { modified, sizeSet }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import type {
|
||||
DerivedSymbolOverride,
|
||||
OverrideContext
|
||||
} from '#core/kiwi/fig/instance-overrides/types'
|
||||
import { convertLetterSpacing, convertLineHeight } from '#core/kiwi/fig/node-change/convert'
|
||||
import { convertFigmaDerivedTextGlyphs } from '#core/kiwi/fig/node-change/derived-text-glyphs'
|
||||
import type { DerivedSymbolOverride, OverrideContext } from '#core/kiwi/fig/instance-overrides/types'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
|
||||
import { resolveDsdGeometry } from './geometry'
|
||||
|
||||
function getVisibleSiblingCount(ctx: OverrideContext, cache: Map<string, number>, parentId: string): number {
|
||||
function getVisibleSiblingCount(
|
||||
ctx: OverrideContext,
|
||||
cache: Map<string, number>,
|
||||
parentId: string
|
||||
): number {
|
||||
const cached = cache.get(parentId)
|
||||
if (cached !== undefined) return cached
|
||||
const count = ctx.graph.getChildren(parentId).filter((child) => child.visible).length
|
||||
|
|
@ -18,12 +25,21 @@ function resolveSizeOnlyPosition(
|
|||
visibleSiblingCount: Map<string, number>,
|
||||
node: SceneNode
|
||||
): Pick<SceneNode, 'x' | 'y'> | null {
|
||||
if (!node.parentId || getVisibleSiblingCount(ctx, visibleSiblingCount, node.parentId) !== 1 || !node.componentId) return null
|
||||
if (
|
||||
!node.parentId ||
|
||||
getVisibleSiblingCount(ctx, visibleSiblingCount, node.parentId) !== 1 ||
|
||||
!node.componentId
|
||||
)
|
||||
return null
|
||||
const source = ctx.graph.getNode(node.componentId)
|
||||
if (!source) return null
|
||||
const sourceParent = source.parentId ? ctx.graph.getNode(source.parentId) : null
|
||||
if (!sourceParent) return { x: source.x, y: source.y }
|
||||
const withinParent = source.x >= 0 && source.y >= 0 && source.x + source.width <= sourceParent.width + 0.01 && source.y + source.height <= sourceParent.height + 0.01
|
||||
const withinParent =
|
||||
source.x >= 0 &&
|
||||
source.y >= 0 &&
|
||||
source.x + source.width <= sourceParent.width + 0.01 &&
|
||||
source.y + source.height <= sourceParent.height + 0.01
|
||||
return withinParent ? { x: source.x, y: source.y } : { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
|
|
@ -35,9 +51,13 @@ function buildDsdTextUpdates(
|
|||
const updates: Partial<SceneNode> = {}
|
||||
if (d.fontSize !== undefined) updates.fontSize = d.fontSize
|
||||
if (d.lineHeight !== undefined) updates.lineHeight = convertLineHeight(d.lineHeight, d.fontSize)
|
||||
if (d.letterSpacing !== undefined) updates.letterSpacing = convertLetterSpacing(d.letterSpacing, d.fontSize)
|
||||
if (d.letterSpacing !== undefined)
|
||||
updates.letterSpacing = convertLetterSpacing(d.letterSpacing, d.fontSize)
|
||||
if (d.strokeWeight !== undefined && target.strokes.length > 0) {
|
||||
updates.strokes = target.strokes.map((stroke) => ({ ...stroke, weight: d.strokeWeight as number }))
|
||||
updates.strokes = target.strokes.map((stroke) => ({
|
||||
...stroke,
|
||||
weight: d.strokeWeight as number
|
||||
}))
|
||||
}
|
||||
const figmaDerivedTextGlyphs = convertFigmaDerivedTextGlyphs(d.derivedTextData, blobs)
|
||||
if (figmaDerivedTextGlyphs.length > 0) updates.figmaDerivedTextGlyphs = figmaDerivedTextGlyphs
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ function buildCloneUpdates(
|
|||
if (source.x !== clone.x) updates.x = source.x
|
||||
if (source.y !== clone.y) updates.y = source.y
|
||||
if (!ctx.geometryOverrideNodes.has(cloneId)) {
|
||||
if (source.fillGeometry !== clone.fillGeometry) updates.fillGeometry = copyGeometryPaths(source.fillGeometry)
|
||||
if (source.strokeGeometry !== clone.strokeGeometry) updates.strokeGeometry = copyGeometryPaths(source.strokeGeometry)
|
||||
if (source.fillGeometry !== clone.fillGeometry)
|
||||
updates.fillGeometry = copyGeometryPaths(source.fillGeometry)
|
||||
if (source.strokeGeometry !== clone.strokeGeometry)
|
||||
updates.strokeGeometry = copyGeometryPaths(source.strokeGeometry)
|
||||
}
|
||||
if (source.text === clone.text && source.figmaDerivedTextGlyphs) {
|
||||
updates.figmaDerivedTextGlyphs = structuredClone(source.figmaDerivedTextGlyphs)
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import { guidToString } from '#core/kiwi/fig/node-change/convert'
|
|||
import type { SceneGraph, SceneNode } from '#core/scene-graph'
|
||||
import { copyFills, copyStyleRuns } from '#core/scene-graph/copy'
|
||||
|
||||
import { applyComponentProperties } from './component-props'
|
||||
import { applyConstraintScaling } from './constraints'
|
||||
import { applyDerivedSymbolData } from './derived-symbol-data'
|
||||
import { populateInstances } from './populate'
|
||||
import { applyComponentProperties } from './component-props'
|
||||
import { preComputeRoots } from './resolve'
|
||||
import { applySymbolOverrides } from './symbol/overrides'
|
||||
import { propagateOverridesTransitively } from './sync'
|
||||
|
|
@ -100,7 +100,11 @@ function propagateResolvedChildPlacementClones(graph: SceneGraph): void {
|
|||
const sourceChild = graph.getNode(source.childIds[i])
|
||||
const child = graph.getNode(node.childIds[i])
|
||||
if (!sourceChild || !child) continue
|
||||
if (sourceChild.overrideKey && child.overrideKey && sourceChild.overrideKey !== child.overrideKey) {
|
||||
if (
|
||||
sourceChild.overrideKey &&
|
||||
child.overrideKey &&
|
||||
sourceChild.overrideKey !== child.overrideKey
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const updates: Partial<SceneNode> = {}
|
||||
|
|
|
|||
|
|
@ -325,7 +325,10 @@ export function resolveOverrideTarget(
|
|||
* Only renames when the current name matches the root component name (preserves
|
||||
* user-given names). Clears the componentIdRoot cache after changing the tree.
|
||||
*/
|
||||
function collectStyledStrokeDescendants(ctx: OverrideContext, nodeId: string): SceneNode['strokes'][] {
|
||||
function collectStyledStrokeDescendants(
|
||||
ctx: OverrideContext,
|
||||
nodeId: string
|
||||
): SceneNode['strokes'][] {
|
||||
const result: SceneNode['strokes'][] = []
|
||||
const visit = (id: string) => {
|
||||
const node = ctx.graph.getNode(id)
|
||||
|
|
@ -337,7 +340,11 @@ function collectStyledStrokeDescendants(ctx: OverrideContext, nodeId: string): S
|
|||
return result
|
||||
}
|
||||
|
||||
function applyStrokeDescendants(ctx: OverrideContext, nodeId: string, strokes: SceneNode['strokes'][]): void {
|
||||
function applyStrokeDescendants(
|
||||
ctx: OverrideContext,
|
||||
nodeId: string,
|
||||
strokes: SceneNode['strokes'][]
|
||||
): void {
|
||||
let index = 0
|
||||
const visit = (id: string) => {
|
||||
const node = ctx.graph.getNode(id)
|
||||
|
|
|
|||
|
|
@ -27,12 +27,24 @@ function assignDirectUpdate(
|
|||
updates: Partial<SceneNode>
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'text': updates.text = source.text; break
|
||||
case 'visible': updates.visible = source.visible; break
|
||||
case 'opacity': updates.opacity = source.opacity; break
|
||||
case 'locked': updates.locked = source.locked; break
|
||||
case 'layoutGrow': updates.layoutGrow = source.layoutGrow; break
|
||||
case 'textAutoResize': updates.textAutoResize = source.textAutoResize; break
|
||||
case 'text':
|
||||
updates.text = source.text
|
||||
break
|
||||
case 'visible':
|
||||
updates.visible = source.visible
|
||||
break
|
||||
case 'opacity':
|
||||
updates.opacity = source.opacity
|
||||
break
|
||||
case 'locked':
|
||||
updates.locked = source.locked
|
||||
break
|
||||
case 'layoutGrow':
|
||||
updates.layoutGrow = source.layoutGrow
|
||||
break
|
||||
case 'textAutoResize':
|
||||
updates.textAutoResize = source.textAutoResize
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,10 +71,18 @@ function assignCopiedUpdate(
|
|||
updates: Partial<SceneNode>
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'fills': updates.fills = copyFills(source.fills); break
|
||||
case 'strokes': updates.strokes = copyStrokes(source.strokes); break
|
||||
case 'effects': updates.effects = copyEffects(source.effects); break
|
||||
case 'styleRuns': updates.styleRuns = copyStyleRuns(source.styleRuns); break
|
||||
case 'fills':
|
||||
updates.fills = copyFills(source.fills)
|
||||
break
|
||||
case 'strokes':
|
||||
updates.strokes = copyStrokes(source.strokes)
|
||||
break
|
||||
case 'effects':
|
||||
updates.effects = copyEffects(source.effects)
|
||||
break
|
||||
case 'styleRuns':
|
||||
updates.styleRuns = copyStyleRuns(source.styleRuns)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export function guidToString(guid: GUID): string {
|
|||
|
||||
export function stringToGuid(str: string): GUID {
|
||||
const match = str.match(/^(?:VariableID:|VariableCollectionId:)?(\d+):(\d+)$/)
|
||||
if (match) return { sessionID: Number.parseInt(match[1], 10), localID: Number.parseInt(match[2], 10) }
|
||||
if (match)
|
||||
return { sessionID: Number.parseInt(match[1], 10), localID: Number.parseInt(match[2], 10) }
|
||||
const [session, local] = str.split(':')
|
||||
return { sessionID: Number.parseInt(session, 10), localID: Number.parseInt(local, 10) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getLazyFigImportContext, setLazyFigImportContext } from '#core/kiwi/fig/lazy-import'
|
||||
import type { InstanceNodeChange } from '#core/kiwi/fig/instance-overrides'
|
||||
import { getLazyFigImportContext, setLazyFigImportContext } from '#core/kiwi/fig/lazy-import'
|
||||
import { SceneGraph } from '#core/scene-graph'
|
||||
import type { SceneNode, Variable, VariableCollection, DocumentColorSpace } from '#core/scene-graph'
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ self.onmessage = (e: MessageEvent<ArrayBuffer | WorkerParseRequest>) => {
|
|||
const graph = importNodeChanges(nodeChanges, blobs, new Map(images), request.options)
|
||||
graph.figKiwiVersion = figKiwiVersion
|
||||
const serialized = serializeSceneGraph(graph)
|
||||
;(self as WorkerScope).postMessage({ graph: serialized }, serializedSceneGraphTransferList(serialized))
|
||||
;(self as WorkerScope).postMessage(
|
||||
{ graph: serialized },
|
||||
serializedSceneGraphTransferList(serialized)
|
||||
)
|
||||
} catch (err) {
|
||||
self.postMessage({ error: err instanceof Error ? err.message : String(err) })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,10 @@ export { ByteBuffer } from './bb'
|
|||
export { compileSchema } from './js'
|
||||
export { decodeBinarySchema, encodeBinarySchema } from './binary'
|
||||
export { parseSchema } from './parser'
|
||||
export { validateSchema, expectFieldNumber, expectEnumValue, findDefinition, findField } from './validate'
|
||||
export {
|
||||
validateSchema,
|
||||
expectFieldNumber,
|
||||
expectEnumValue,
|
||||
findDefinition,
|
||||
findField
|
||||
} from './validate'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ export function findDefinition(schema: Schema, name: string): Definition | null
|
|||
}
|
||||
|
||||
export function findField(schema: Schema, definitionName: string, fieldName: string): Field | null {
|
||||
return findDefinition(schema, definitionName)?.fields.find((field) => field.name === fieldName) ?? null
|
||||
return (
|
||||
findDefinition(schema, definitionName)?.fields.find((field) => field.name === fieldName) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
export function expectFieldNumber(
|
||||
|
|
|
|||
|
|
@ -36,8 +36,14 @@ function updateChildFromYoga(graph: SceneGraph, child: SceneNode, yogaChild: Yog
|
|||
|
||||
const derived = child.figmaDerivedLayout
|
||||
graph.updateNode(child.id, {
|
||||
x: child.type === 'INSTANCE' ? yogaChild.getComputedLeft() : (derived?.x ?? yogaChild.getComputedLeft()),
|
||||
y: child.type === 'INSTANCE' ? yogaChild.getComputedTop() : (derived?.y ?? yogaChild.getComputedTop()),
|
||||
x:
|
||||
child.type === 'INSTANCE'
|
||||
? yogaChild.getComputedLeft()
|
||||
: (derived?.x ?? yogaChild.getComputedLeft()),
|
||||
y:
|
||||
child.type === 'INSTANCE'
|
||||
? yogaChild.getComputedTop()
|
||||
: (derived?.y ?? yogaChild.getComputedTop()),
|
||||
width: derived?.width ?? yogaChild.getComputedWidth(),
|
||||
height: derived?.height ?? yogaChild.getComputedHeight()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -155,8 +155,16 @@ export class HudRenderer {
|
|||
|
||||
const pictureLabel = stats.scenePictureMode === 'record' ? 'record' : 'picture'
|
||||
const pictureTime =
|
||||
stats.scenePictureMode === 'record' ? stats.scenePictureRecordTime : stats.scenePictureDrawTime
|
||||
canvas.drawText(`${pictureLabel}: ${pictureTime.toFixed(1)}ms`, col1, y, this.textPaint, this.hudFont)
|
||||
stats.scenePictureMode === 'record'
|
||||
? stats.scenePictureRecordTime
|
||||
: stats.scenePictureDrawTime
|
||||
canvas.drawText(
|
||||
`${pictureLabel}: ${pictureTime.toFixed(1)}ms`,
|
||||
col1,
|
||||
y,
|
||||
this.textPaint,
|
||||
this.hudFont
|
||||
)
|
||||
canvas.drawText(`flush: ${stats.flushTime.toFixed(1)}ms`, col2, y, this.textPaint, this.hudFont)
|
||||
|
||||
if (visiblePhases.length > 0) {
|
||||
|
|
|
|||
|
|
@ -325,8 +325,6 @@ export class SceneGraph {
|
|||
'maxHeight'
|
||||
])
|
||||
|
||||
|
||||
|
||||
runPreviewUpdates(fn: () => void): void {
|
||||
this.previewMutationDepth++
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { normalizeVectorNetwork } from './vector-network'
|
||||
|
||||
import type { SceneNode } from './types'
|
||||
import { normalizeVectorNetwork } from './vector-network'
|
||||
|
||||
type PreviewGraph = {
|
||||
nodes: Map<string, SceneNode>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
|
||||
|
||||
import type { NodeChange } from '#core/kiwi/fig/codec'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
|
||||
import { encodePathCommandsBlob } from '#core/kiwi/fig/node-change/path-commands'
|
||||
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
import { normalizeFontFamily, weightToFigmaStyle, weightToStyle } from '#core/text/fonts'
|
||||
import { type GlyphOutlineMetrics, getGlyphOutlineMetricsSync } from '#core/text/opentype'
|
||||
|
||||
|
|
@ -136,7 +134,8 @@ export async function buildDerivedTextDataV4(
|
|||
const normalizedFamily = normalizeFontFamily(node.fontFamily)
|
||||
const key = `${normalizedFamily}|${style}`
|
||||
const lineHeightFallback = node.lineHeight ?? Math.ceil(node.fontSize * 1.2)
|
||||
const glyphMetrics = getGlyphOutlineMetricsSync(node.fontFamily, style, node.text, node.fontSize) ?? []
|
||||
const glyphMetrics =
|
||||
getGlyphOutlineMetricsSync(node.fontFamily, style, node.text, node.fontSize) ?? []
|
||||
|
||||
const fallbackAdvance = node.text.length > 0 ? node.width / Math.max(node.text.length, 1) : 0
|
||||
const textGlyphs = buildTextGlyphs(node.text, glyphMetrics, fallbackAdvance, node.fontSize)
|
||||
|
|
@ -144,7 +143,10 @@ export async function buildDerivedTextDataV4(
|
|||
const lineBreaks = computeLineBreaks(node, glyphMetrics, textGlyphs, fallbackAdvance, shaped)
|
||||
const lineBreakSet = new Set(lineBreaks)
|
||||
|
||||
const shapedByChar = new Map<number, (typeof shaped extends null | undefined ? never : NonNullable<typeof shaped>)['glyphs'][number]>()
|
||||
const shapedByChar = new Map<
|
||||
number,
|
||||
(typeof shaped extends null | undefined ? never : NonNullable<typeof shaped>)['glyphs'][number]
|
||||
>()
|
||||
if (shaped) {
|
||||
for (const g of shaped.glyphs) shapedByChar.set(g.firstCharacter, g)
|
||||
}
|
||||
|
|
@ -207,7 +209,11 @@ export async function buildDerivedTextDataV4(
|
|||
glyphs,
|
||||
fontMetaData: [
|
||||
{
|
||||
key: { family: normalizedFamily, style: weightToFigmaStyle(node.fontWeight, node.italic), postscript: '' },
|
||||
key: {
|
||||
family: normalizedFamily,
|
||||
style: weightToFigmaStyle(node.fontWeight, node.italic),
|
||||
postscript: ''
|
||||
},
|
||||
fontLineHeight: 1.2,
|
||||
fontDigest: digestMap.get(key),
|
||||
fontStyle: node.italic ? 'ITALIC' : 'NORMAL',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
|
|||
|
||||
import type { CharacterStyleOverride, SceneNode } from '#core/scene-graph'
|
||||
import { fontManager, weightToStyle } from '#core/text/fonts'
|
||||
import { fontHasGlyphSync, getGlyphOutlineMetricsSync, type OutlineCommand } from '#core/text/opentype'
|
||||
import {
|
||||
fontHasGlyphSync,
|
||||
getGlyphOutlineMetricsSync,
|
||||
type OutlineCommand
|
||||
} from '#core/text/opentype'
|
||||
|
||||
export type TextOutlineUnsupportedReason =
|
||||
| 'not-text'
|
||||
|
|
@ -29,7 +33,12 @@ export interface TextOutlineLayout {
|
|||
|
||||
const COMPLEX_SCRIPT_PATTERN = /[\u0590-\u08ff\u0900-\u0dff\ufb1d-\ufdff\ufe70-\ufeff]/
|
||||
|
||||
type TextStyle = Required<Pick<CharacterStyleOverride, 'fontFamily' | 'fontSize' | 'fontWeight' | 'italic' | 'letterSpacing'>>
|
||||
type TextStyle = Required<
|
||||
Pick<
|
||||
CharacterStyleOverride,
|
||||
'fontFamily' | 'fontSize' | 'fontWeight' | 'italic' | 'letterSpacing'
|
||||
>
|
||||
>
|
||||
|
||||
function baseTextStyle(node: SceneNode): TextStyle {
|
||||
return {
|
||||
|
|
@ -66,7 +75,10 @@ function resolvedGlyphStyle(style: TextStyle, char: string): TextStyle | null {
|
|||
if (fontHasGlyphSync(style.fontFamily, styleName(style), char)) return style
|
||||
const family = fallbackFamilies().find((candidate) => {
|
||||
const next = fallbackStyle(style, candidate)
|
||||
return fontManager.loadedData(next.fontFamily, styleName(next)) && fontHasGlyphSync(next.fontFamily, styleName(next), char)
|
||||
return (
|
||||
fontManager.loadedData(next.fontFamily, styleName(next)) &&
|
||||
fontHasGlyphSync(next.fontFamily, styleName(next), char)
|
||||
)
|
||||
})
|
||||
return family ? fallbackStyle(style, family) : null
|
||||
}
|
||||
|
|
@ -130,7 +142,12 @@ function glyphAdvance(node: SceneNode, absoluteIndex: number): number | null {
|
|||
const char = node.text[absoluteIndex]
|
||||
const style = resolvedGlyphStyle(textStyleAt(node, absoluteIndex), char)
|
||||
if (!style) return null
|
||||
const metrics = getGlyphOutlineMetricsSync(style.fontFamily, styleName(style), char, style.fontSize)
|
||||
const metrics = getGlyphOutlineMetricsSync(
|
||||
style.fontFamily,
|
||||
styleName(style),
|
||||
char,
|
||||
style.fontSize
|
||||
)
|
||||
const glyph = metrics?.[0]
|
||||
return glyph ? glyph.advance + style.letterSpacing : null
|
||||
}
|
||||
|
|
@ -211,7 +228,12 @@ function verticalOffset(node: SceneNode, contentHeight: number): number {
|
|||
}
|
||||
}
|
||||
|
||||
function lineGlyphs(node: SceneNode, line: TextLine, baseline: number, xOffset: number): { glyphs: TextOutlineGlyph[]; width: number } | null {
|
||||
function lineGlyphs(
|
||||
node: SceneNode,
|
||||
line: TextLine,
|
||||
baseline: number,
|
||||
xOffset: number
|
||||
): { glyphs: TextOutlineGlyph[]; width: number } | null {
|
||||
const glyphs: TextOutlineGlyph[] = []
|
||||
let cursorX = xOffset
|
||||
let index = 0
|
||||
|
|
@ -229,7 +251,12 @@ function lineGlyphs(node: SceneNode, line: TextLine, baseline: number, xOffset:
|
|||
}
|
||||
|
||||
const segment = line.text.slice(index, end)
|
||||
const metrics = getGlyphOutlineMetricsSync(style.fontFamily, styleName(style), segment, style.fontSize)
|
||||
const metrics = getGlyphOutlineMetricsSync(
|
||||
style.fontFamily,
|
||||
styleName(style),
|
||||
segment,
|
||||
style.fontSize
|
||||
)
|
||||
if (!metrics) return null
|
||||
|
||||
for (const glyph of metrics) {
|
||||
|
|
@ -258,7 +285,10 @@ export function textNodeToOutlineLayout(node: SceneNode): TextOutlineLayout | nu
|
|||
if (!measured) return null
|
||||
const xOffset = lineOffsetX(node, measured.width)
|
||||
maxWidth = Math.max(maxWidth, measured.width)
|
||||
const placed = xOffset === 0 ? measured.glyphs : measured.glyphs.map((glyph) => ({ ...glyph, x: glyph.x + xOffset }))
|
||||
const placed =
|
||||
xOffset === 0
|
||||
? measured.glyphs
|
||||
: measured.glyphs.map((glyph) => ({ ...glyph, x: glyph.x + xOffset }))
|
||||
glyphs.push(...placed)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@ export const analyzeClusters = defineTool({
|
|||
[...signatureMap.entries()]
|
||||
.filter(([, nodes]) => nodes.length >= minCount)
|
||||
.map(([signature, nodes]) => {
|
||||
const avgWidth = nodes.reduce((sum, node) => sum + node.width, 0) / nodes.length
|
||||
const avgHeight = nodes.reduce((sum, node) => sum + node.height, 0) / nodes.length
|
||||
const widths = nodes.map((node) => node.width)
|
||||
const heights = nodes.map((node) => node.height)
|
||||
const widthRange = Math.max(...widths) - Math.min(...widths)
|
||||
const heightRange = Math.max(...heights) - Math.min(...heights)
|
||||
const confidence = calcClusterConfidence(nodes)
|
||||
const avgWidth = nodes.reduce((sum, node) => sum + node.width, 0) / nodes.length
|
||||
const avgHeight = nodes.reduce((sum, node) => sum + node.height, 0) / nodes.length
|
||||
const widths = nodes.map((node) => node.width)
|
||||
const heights = nodes.map((node) => node.height)
|
||||
const widthRange = Math.max(...widths) - Math.min(...widths)
|
||||
const heightRange = Math.max(...heights) - Math.min(...heights)
|
||||
const confidence = calcClusterConfidence(nodes)
|
||||
|
||||
return {
|
||||
signature,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { extractPaths } from '#core/icons/svg'
|
|||
import type { IconPathInfo } from '#core/icons/types'
|
||||
import { parseSVGPath } from '#core/io/formats/svg/parse-path'
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
|
||||
import type { Rect } from '#core/types'
|
||||
|
||||
function parseSvgViewBox(svg: string): Rect | null {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { CanvasKit, Path } from 'canvaskit-wasm'
|
||||
|
||||
import { addOpenSegmentsToPath } from './path-helpers'
|
||||
|
||||
import type { VectorNetwork } from '#core/scene-graph'
|
||||
import type { Vector } from '#core/types'
|
||||
|
||||
import { addOpenSegmentsToPath } from './path-helpers'
|
||||
|
||||
export function fitCircleArc(
|
||||
pts: Vector[]
|
||||
): { cx: number; cy: number; r: number; startAngleDeg: number; sweepDeg: number } | null {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ export {
|
|||
|
||||
import type { CanvasKit, Path } from 'canvaskit-wasm'
|
||||
|
||||
import { addOpenSegmentsToPath, addSegmentDirected } from './path-helpers'
|
||||
|
||||
import type {
|
||||
HandleMirroring,
|
||||
VectorNetwork,
|
||||
|
|
@ -22,6 +20,8 @@ import type {
|
|||
VectorVertex,
|
||||
WindingRule
|
||||
} from '#core/scene-graph'
|
||||
|
||||
import { addOpenSegmentsToPath, addSegmentDirected } from './path-helpers'
|
||||
export { vectorNetworkToCenterlinePath, fitCircleArc, isClosedThinCrescent } from './centerline'
|
||||
|
||||
// --- vectorNetworkBlob binary format ---
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import { serve } from '@hono/node-server'
|
|||
import { startServer } from './server.js'
|
||||
|
||||
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||
process.stdout.write(`openpencil-mcp-http\n\nStart the OpenPencil MCP HTTP and WebSocket server.\n\nOptions:\n --help, -h Show this help message\n`)
|
||||
process.stdout.write(
|
||||
`openpencil-mcp-http\n\nStart the OpenPencil MCP HTTP and WebSocket server.\n\nOptions:\n --help, -h Show this help message\n`
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import { MCP_VERSION, registerTools } from './server.js'
|
|||
import { createStdioRpcBridge } from './stdio-bridge.js'
|
||||
|
||||
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||
process.stdout.write(`openpencil-mcp\n\nStart the OpenPencil MCP stdio bridge.\n\nOptions:\n --help, -h Show this help message\n`)
|
||||
process.stdout.write(
|
||||
`openpencil-mcp\n\nStart the OpenPencil MCP stdio bridge.\n\nOptions:\n --help, -h Show this help message\n`
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,12 +69,15 @@ export function registerTools(mcpServer: McpServer, options: RegisterToolsOption
|
|||
? `Save the current document to disk. If path is provided, it must be inside ${resolvedRoot}.`
|
||||
: 'Save the current document to disk. Uses the existing file path if available, otherwise prompts for a location.',
|
||||
inputSchema: resolvedRoot
|
||||
? z.object({ path: z.string().describe('Optional absolute path for the .fig file').optional() })
|
||||
? z.object({
|
||||
path: z.string().describe('Optional absolute path for the .fig file').optional()
|
||||
})
|
||||
: z.object({})
|
||||
},
|
||||
async (args: { path?: string }) => {
|
||||
try {
|
||||
const safePath = args.path && resolvedRoot ? resolveSafePath(args.path, resolvedRoot) : undefined
|
||||
const safePath =
|
||||
args.path && resolvedRoot ? resolveSafePath(args.path, resolvedRoot) : undefined
|
||||
const result = await sendRpc({ command: 'save_file', args: { path: safePath } })
|
||||
const res = result as { ok?: boolean; error?: string }
|
||||
if (res.ok === false) return fail(new Error(res.error))
|
||||
|
|
|
|||
|
|
@ -46,7 +46,18 @@ export function handleToolMouseDown({
|
|||
}
|
||||
|
||||
if (tool === 'SELECT') {
|
||||
handleSelectDown(event, cx, cy, sx, sy, editor, hitFns, tryStartRotation, handleTextEditClick, setDrag)
|
||||
handleSelectDown(
|
||||
event,
|
||||
cx,
|
||||
cy,
|
||||
sx,
|
||||
sy,
|
||||
editor,
|
||||
hitFns,
|
||||
tryStartRotation,
|
||||
handleTextEditClick,
|
||||
setDrag
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import { createCanvasPointer } from '#vue/canvas/pointer/use'
|
|||
import { createTextEditInput } from '#vue/canvas/text-edit/input'
|
||||
import { handleToolMouseDown } from '#vue/canvas/tool-input/use'
|
||||
import { createCanvasTransformInput } from '#vue/canvas/transform-input/use'
|
||||
import { createClickCounter } from '#vue/shared/input/click-count'
|
||||
import { resolveAutoLayoutHover } from '#vue/shared/input/auto-layout-hover'
|
||||
import { createClickCounter } from '#vue/shared/input/click-count'
|
||||
import { handleDrawMove, handleDrawUp } from '#vue/shared/input/draw'
|
||||
import { handleMoveMove, handleMoveUp } from '#vue/shared/input/move'
|
||||
import { handleNodeEditMove } from '#vue/shared/input/node-edit'
|
||||
|
|
|
|||
|
|
@ -252,7 +252,8 @@ export function createLayoutActions({
|
|||
updateProp('primaryAxisSizing', 'HUG')
|
||||
} else {
|
||||
if (n.primaryAxisSizing === 'HUG') updateProp('primaryAxisSizing', 'FIXED')
|
||||
if (isInAutoLayout.value) updateProp('layoutAlignSelf', sizing === 'FILL' ? 'STRETCH' : 'AUTO')
|
||||
if (isInAutoLayout.value)
|
||||
updateProp('layoutAlignSelf', sizing === 'FILL' ? 'STRETCH' : 'AUTO')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { Ref } from 'vue'
|
||||
|
||||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import {
|
||||
getFillOkHCL,
|
||||
getStrokeOkHCL,
|
||||
|
|
@ -10,6 +9,7 @@ import {
|
|||
setNodeStrokeOkHCL
|
||||
} from '@open-pencil/core/color'
|
||||
import type { OkHCLColor } from '@open-pencil/core/color'
|
||||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import type { Editor } from '@open-pencil/core/editor'
|
||||
import type { SceneNode } from '@open-pencil/core/scene-graph'
|
||||
|
||||
|
|
@ -30,17 +30,11 @@ export function getStrokeOkHCLColor(node: SceneNode | null, index: number): OkHC
|
|||
}
|
||||
|
||||
function fallbackFillOkHCL(node: SceneNode, index: number) {
|
||||
return (
|
||||
getFillOkHCLColor(node, index) ??
|
||||
rgbaToOkHCL(node.fills[index]?.color ?? BLACK)
|
||||
)
|
||||
return getFillOkHCLColor(node, index) ?? rgbaToOkHCL(node.fills[index]?.color ?? BLACK)
|
||||
}
|
||||
|
||||
function fallbackStrokeOkHCL(node: SceneNode, index: number) {
|
||||
return (
|
||||
getStrokeOkHCLColor(node, index) ??
|
||||
rgbaToOkHCL(node.strokes[index]?.color ?? BLACK)
|
||||
)
|
||||
return getStrokeOkHCLColor(node, index) ?? rgbaToOkHCL(node.strokes[index]?.color ?? BLACK)
|
||||
}
|
||||
|
||||
export function createOkHCLActions(editor: Editor) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ function isNear(value: number, target: number, tolerance = AUTO_LAYOUT_HOVER_TIC
|
|||
return Math.abs(value - target) <= tolerance
|
||||
}
|
||||
|
||||
function resolvePaddingHover(node: SceneNode, localX: number, localY: number): AutoLayoutHover | null {
|
||||
function resolvePaddingHover(
|
||||
node: SceneNode,
|
||||
localX: number,
|
||||
localY: number
|
||||
): AutoLayoutHover | null {
|
||||
const centerX = node.width / 2
|
||||
const centerY = node.height / 2
|
||||
|
||||
|
|
@ -40,7 +44,10 @@ function resolvePaddingHover(node: SceneNode, localX: number, localY: number): A
|
|||
if (isNear(localX, centerX) && isNear(localY, tickY)) {
|
||||
return { nodeId: node.id, kind: 'padding-value', side: 'bottom' }
|
||||
}
|
||||
if (localY >= node.height - Math.min(node.paddingBottom, AUTO_LAYOUT_HOVER_PADDING_REGION_TOLERANCE)) {
|
||||
if (
|
||||
localY >=
|
||||
node.height - Math.min(node.paddingBottom, AUTO_LAYOUT_HOVER_PADDING_REGION_TOLERANCE)
|
||||
) {
|
||||
return { nodeId: node.id, kind: 'padding', side: 'bottom' }
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +67,10 @@ function resolvePaddingHover(node: SceneNode, localX: number, localY: number): A
|
|||
if (isNear(localX, tickX) && isNear(localY, centerY)) {
|
||||
return { nodeId: node.id, kind: 'padding-value', side: 'right' }
|
||||
}
|
||||
if (localX >= node.width - Math.min(node.paddingRight, AUTO_LAYOUT_HOVER_PADDING_REGION_TOLERANCE)) {
|
||||
if (
|
||||
localX >=
|
||||
node.width - Math.min(node.paddingRight, AUTO_LAYOUT_HOVER_PADDING_REGION_TOLERANCE)
|
||||
) {
|
||||
return { nodeId: node.id, kind: 'padding', side: 'right' }
|
||||
}
|
||||
}
|
||||
|
|
@ -124,7 +134,11 @@ function resolveChildrenHover(
|
|||
return null
|
||||
}
|
||||
|
||||
export function resolveAutoLayoutHover(cx: number, cy: number, editor: Editor): AutoLayoutHover | null {
|
||||
export function resolveAutoLayoutHover(
|
||||
cx: number,
|
||||
cy: number,
|
||||
editor: Editor
|
||||
): AutoLayoutHover | null {
|
||||
if (editor.state.selectedIds.size !== 1) return null
|
||||
const nodeId = [...editor.state.selectedIds][0]
|
||||
const node = editor.graph.getNode(nodeId)
|
||||
|
|
@ -139,7 +153,6 @@ export function resolveAutoLayoutHover(cx: number, cy: number, editor: Editor):
|
|||
return (
|
||||
resolveSpacingHover(node, children, localX, localY) ??
|
||||
resolvePaddingHover(node, localX, localY) ??
|
||||
resolveChildrenHover(node, children, localX, localY) ??
|
||||
{ nodeId: node.id, kind: 'frame' }
|
||||
resolveChildrenHover(node, children, localX, localY) ?? { nodeId: node.id, kind: 'frame' }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ export function createSelectionMoveDrag(
|
|||
editor: Editor,
|
||||
duplicate: boolean
|
||||
): DragState {
|
||||
if (duplicate && editor.state.selectedIds.size > 0) return duplicateAndDrag(cx, cy, sx, sy, editor).drag
|
||||
if (duplicate && editor.state.selectedIds.size > 0)
|
||||
return duplicateAndDrag(cx, cy, sx, sy, editor).drag
|
||||
|
||||
const originals = collectMoveOriginals(editor)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Ref } from 'vue'
|
||||
|
||||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import { colorToHexRaw, parseColor } from '@open-pencil/core/color'
|
||||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import type { Editor } from '@open-pencil/core/editor'
|
||||
import { randomHex } from '@open-pencil/core/random'
|
||||
import type {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ function checkRuntimePath(packageName: string, field: string, value: string): vo
|
|||
function walkExports(packageName: string, value: unknown, path: string[] = []): void {
|
||||
if (typeof value === 'string') {
|
||||
const key = path.at(-1)
|
||||
if (key !== 'types' && key !== 'bun') checkRuntimePath(packageName, `exports.${path.join('.')}`, value)
|
||||
if (key !== 'types' && key !== 'bun')
|
||||
checkRuntimePath(packageName, `exports.${path.join('.')}`, value)
|
||||
return
|
||||
}
|
||||
if (!value || typeof value !== 'object') return
|
||||
|
|
@ -59,7 +60,10 @@ for (const packageDir of publicPackages) {
|
|||
|
||||
walkExports(pkg.name, pkg.exports)
|
||||
|
||||
if (pkg.publishConfig && ('exports' in pkg.publishConfig || 'main' in pkg.publishConfig || 'types' in pkg.publishConfig)) {
|
||||
if (
|
||||
pkg.publishConfig &&
|
||||
('exports' in pkg.publishConfig || 'main' in pkg.publishConfig || 'types' in pkg.publishConfig)
|
||||
) {
|
||||
errors.push(`${pkg.name}: publishConfig must not rewrite runtime entrypoints`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ try {
|
|||
|
||||
const tarballs: string[] = []
|
||||
for (const packageDir of packageDirs) {
|
||||
const output = run(['bun', 'pm', 'pack', '--destination', tempDir, '--quiet'], join(rootDir, packageDir))
|
||||
const output = run(
|
||||
['bun', 'pm', 'pack', '--destination', tempDir, '--quiet'],
|
||||
join(rootDir, packageDir)
|
||||
)
|
||||
const filename = output
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import type { StepBudget, ToolLogEntry } from '@open-pencil/core/tools'
|
|||
|
||||
import { makeFigmaFromStore } from '@/app/automation/bridge/figma-factory'
|
||||
import { getActiveEditorStore } from '@/app/editor/active-store'
|
||||
import { ensureGraphFonts } from '@/app/editor/fonts'
|
||||
import type { EditorStore } from '@/app/editor/active-store'
|
||||
import { ensureGraphFonts } from '@/app/editor/fonts'
|
||||
|
||||
export const MAX_AGENT_STEPS = 50
|
||||
|
||||
|
|
|
|||
|
|
@ -413,7 +413,9 @@ export function createEffectsSection(store: EditorStore) {
|
|||
cornerRadius: 22,
|
||||
cornerSmoothing: 0.85,
|
||||
fills: [solid({ r: 0.58, g: 0.27, b: 0.95, a: 0.7 })],
|
||||
effects: [{ ...dropShadow(0, 0, 28, 0, { r: 0.56, g: 0.33, b: 1, a: 0.72 }), blendMode: 'SCREEN' }]
|
||||
effects: [
|
||||
{ ...dropShadow(0, 0, 28, 0, { r: 0.56, g: 0.33, b: 1, a: 0.72 }), blendMode: 'SCREEN' }
|
||||
]
|
||||
})
|
||||
const effectBlendText = store.createShape('TEXT', 184, 34, 88, 34, effectBlendCard)
|
||||
graph.updateNode(effectBlendText, {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ function copyAction(
|
|||
): () => void {
|
||||
switch (id) {
|
||||
case 'copy-as-text':
|
||||
return runAsync(() => actions.clipboardWrite(editor.copySelectionAsText(actions.ids()), 'text'))
|
||||
return runAsync(() =>
|
||||
actions.clipboardWrite(editor.copySelectionAsText(actions.ids()), 'text')
|
||||
)
|
||||
case 'copy-as-svg':
|
||||
return runAsync(() => actions.clipboardWrite(editor.copySelectionAsSVG(actions.ids()), 'SVG'))
|
||||
case 'copy-as-png':
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import { editorCommandMetadata } from '@open-pencil/vue'
|
|||
import type { EditorCommandId } from '@open-pencil/vue'
|
||||
|
||||
import { TOOL_SHORTCUTS } from '@/app/editor/session'
|
||||
import { appMenuTinykeysShortcut } from '@/app/shell/menu/shortcut'
|
||||
import { isEditing } from '@/app/shell/keyboard/focus'
|
||||
import { bindSpaceHandTool } from '@/app/shell/keyboard/space-tool'
|
||||
import type {
|
||||
KeyboardShortcutOptions,
|
||||
KeyboardShortcutRunOptions
|
||||
} from '@/app/shell/keyboard/types'
|
||||
import { appMenuTinykeysShortcut } from '@/app/shell/menu/shortcut'
|
||||
|
||||
type ShortcutAction = (options: KeyboardShortcutRunOptions) => void
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ type TextFormatUpdates = {
|
|||
|
||||
const store = useEditorStore()
|
||||
|
||||
export function alignSelected(axis: 'horizontal' | 'vertical', align: 'min' | 'center' | 'max'): void {
|
||||
export function alignSelected(
|
||||
axis: 'horizontal' | 'vertical',
|
||||
align: 'min' | 'center' | 'max'
|
||||
): void {
|
||||
store.alignNodes([...store.state.selectedIds], axis, align)
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +44,9 @@ export function toggleSelectedTextUnderline(): void {
|
|||
})
|
||||
}
|
||||
|
||||
export function createSharedEditorMenuActions(setTheme: (theme: 'light' | 'dark' | 'auto') => void) {
|
||||
export function createSharedEditorMenuActions(
|
||||
setTheme: (theme: 'light' | 'dark' | 'auto') => void
|
||||
) {
|
||||
return {
|
||||
'zoom-in': () => store.applyZoom(-100, window.innerWidth / 2, window.innerHeight / 2),
|
||||
'zoom-out': () => store.applyZoom(100, window.innerWidth / 2, window.innerHeight / 2),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ function isActionItem(entry: AppMenuEntry): entry is AppMenuActionItem {
|
|||
function findShortcutInEntries(entries: readonly AppMenuEntry[], id: string): string | undefined {
|
||||
for (const entry of entries) {
|
||||
if (!isActionItem(entry)) continue
|
||||
if (entry.id === id) return entry.shortcut ?? (entry.command ? editorCommandMetadata(entry.command).shortcut : undefined)
|
||||
if (entry.id === id)
|
||||
return (
|
||||
entry.shortcut ??
|
||||
(entry.command ? editorCommandMetadata(entry.command).shortcut : undefined)
|
||||
)
|
||||
const shortcut = entry.sub ? findShortcutInEntries(entry.sub, id) : undefined
|
||||
if (shortcut) return shortcut
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,13 @@ const ctx = useColorPickerPanelContext()
|
|||
:min="0"
|
||||
:max="0.4"
|
||||
:step="0.001"
|
||||
:display="{ value: toPercent(ctx.okhcl.okhcl.c), min: 0, max: 40, step: 1, parse: fromPercent }"
|
||||
:display="{
|
||||
value: toPercent(ctx.okhcl.okhcl.c),
|
||||
min: 0,
|
||||
max: 40,
|
||||
step: 1,
|
||||
parse: fromPercent
|
||||
}"
|
||||
:gradient-style="ctx.okhclSliderGradient?.okhclChroma ?? undefined"
|
||||
:thumb-fill="colorToCSS(ctx.okhclSliderPreview?.okhclChroma ?? ctx.color)"
|
||||
test-id="color-slider-okhcl-c"
|
||||
|
|
@ -42,7 +48,13 @@ const ctx = useColorPickerPanelContext()
|
|||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:display="{ value: toPercent(ctx.okhcl.okhcl.l), min: 0, max: 100, step: 1, parse: fromPercent }"
|
||||
:display="{
|
||||
value: toPercent(ctx.okhcl.okhcl.l),
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
parse: fromPercent
|
||||
}"
|
||||
:gradient-style="ctx.okhclSliderGradient?.okhclLightness ?? undefined"
|
||||
:thumb-fill="colorToCSS(ctx.okhclSliderPreview?.okhclLightness ?? ctx.color)"
|
||||
test-id="color-slider-okhcl-l"
|
||||
|
|
@ -55,7 +67,13 @@ const ctx = useColorPickerPanelContext()
|
|||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:display="{ value: toPercent(ctx.okhcl.okhcl.a ?? 1), min: 0, max: 100, step: 1, parse: fromPercent }"
|
||||
:display="{
|
||||
value: toPercent(ctx.okhcl.okhcl.a ?? 1),
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
parse: fromPercent
|
||||
}"
|
||||
checkerboard
|
||||
:gradient-style="`background: linear-gradient(to right, transparent, ${colorToCSS(ctx.color)})`"
|
||||
:thumb-fill="colorToCSS(ctx.color)"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ import { useAttrs, watch } from 'vue'
|
|||
import { templateRef } from '@vueuse/core'
|
||||
import { TreeItem, ContextMenuRoot, ContextMenuTrigger, ContextMenuPortal } from 'reka-ui'
|
||||
|
||||
import {
|
||||
LayerTreeRoot,
|
||||
LayerTreeItem,
|
||||
useI18n,
|
||||
useInlineRename
|
||||
} from '@open-pencil/vue'
|
||||
import { LayerTreeRoot, LayerTreeItem, useI18n, useInlineRename } from '@open-pencil/vue'
|
||||
import { useEditorStore } from '@/app/editor/active-store'
|
||||
import { nodeIcon, COMPONENT_TYPES } from '@/app/editor/icons'
|
||||
import CanvasMenu from './CanvasMenu.vue'
|
||||
|
|
@ -53,165 +48,165 @@ function onTreeSelect(e: CustomEvent, select: (additive: boolean) => void) {
|
|||
<div v-bind="attrs" class="relative min-h-0 flex-1 overflow-hidden">
|
||||
<ContextMenuTrigger as-child @contextmenu="onLayerRightClick">
|
||||
<div data-test-id="layers-scroll" class="scrollbar-thin h-full overflow-y-auto px-1">
|
||||
<template v-if="flattenItems">
|
||||
<LayerTreeItem
|
||||
v-for="item in flattenItems"
|
||||
:key="item._id"
|
||||
v-slot="{ node, isSelected, padLeft, actions }"
|
||||
:node="item.value"
|
||||
:level="item.level"
|
||||
:has-children="item.hasChildren"
|
||||
>
|
||||
<TreeItem
|
||||
v-slot="{ isExpanded }"
|
||||
:value="item.value"
|
||||
<template v-if="flattenItems">
|
||||
<LayerTreeItem
|
||||
v-for="item in flattenItems"
|
||||
:key="item._id"
|
||||
v-slot="{ node, isSelected, padLeft, actions }"
|
||||
:node="item.value"
|
||||
:level="item.level"
|
||||
as-child
|
||||
@select="(e: CustomEvent) => onTreeSelect(e, actions.select)"
|
||||
@toggle="
|
||||
(e: CustomEvent) => {
|
||||
if (e.detail.originalEvent?.type === 'click') e.preventDefault()
|
||||
}
|
||||
"
|
||||
:has-children="item.hasChildren"
|
||||
>
|
||||
<!-- Rename mode -->
|
||||
<div
|
||||
v-if="rename.editingId.value === node.id"
|
||||
class="flex w-full items-center gap-1 py-1"
|
||||
:style="{ paddingLeft: padLeft }"
|
||||
<TreeItem
|
||||
v-slot="{ isExpanded }"
|
||||
:value="item.value"
|
||||
:level="item.level"
|
||||
as-child
|
||||
@select="(e: CustomEvent) => onTreeSelect(e, actions.select)"
|
||||
@toggle="
|
||||
(e: CustomEvent) => {
|
||||
if (e.detail.originalEvent?.type === 'click') e.preventDefault()
|
||||
}
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-if="item.hasChildren"
|
||||
class="flex w-4 shrink-0 cursor-pointer items-center justify-center text-muted transition-transform hover:text-surface"
|
||||
:class="isExpanded ? 'rotate-90' : 'rotate-0'"
|
||||
@click.stop="actions.toggleExpand"
|
||||
>
|
||||
<icon-lucide-chevron-right class="size-3" />
|
||||
</span>
|
||||
<span v-else class="w-4 shrink-0" />
|
||||
<component :is="nodeIcon(node)" class="size-3 shrink-0 opacity-70" />
|
||||
<input
|
||||
ref="renameInput"
|
||||
data-layer-edit
|
||||
data-test-id="layers-item-input"
|
||||
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none"
|
||||
:value="node.name"
|
||||
@blur="rename.commit(node.id, $event)"
|
||||
@keydown.stop="rename.onKeydown"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Normal row -->
|
||||
<button
|
||||
v-else
|
||||
data-test-id="layers-item"
|
||||
class="group/row relative flex w-full cursor-pointer items-center gap-1 rounded border-none py-1 pr-1 text-left text-xs"
|
||||
:class="[
|
||||
isSelected
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-transparent text-surface hover:bg-hover',
|
||||
draggingId === node.id ? 'opacity-30' : '',
|
||||
instructionTargetId === node.id && instruction?.type === 'make-child'
|
||||
? 'bg-accent/15 text-surface outline-2 outline-accent outline-offset-[-2px]'
|
||||
: '',
|
||||
!node.visible ? 'opacity-50' : ''
|
||||
]"
|
||||
:style="{ paddingLeft: padLeft }"
|
||||
@dblclick="rename.start(node.id, node.name)"
|
||||
>
|
||||
<span
|
||||
v-if="item.hasChildren"
|
||||
class="flex w-4 shrink-0 cursor-pointer items-center justify-center text-muted transition-transform hover:text-surface"
|
||||
:class="isExpanded ? 'rotate-90' : 'rotate-0'"
|
||||
@click.stop="actions.toggleExpand"
|
||||
>
|
||||
<icon-lucide-chevron-right class="size-3" />
|
||||
</span>
|
||||
<span v-else class="w-4 shrink-0" />
|
||||
|
||||
<component
|
||||
:is="nodeIcon(node)"
|
||||
class="size-3 shrink-0"
|
||||
:class="
|
||||
COMPONENT_TYPES.has(node.type) ? 'text-component opacity-100' : 'opacity-70'
|
||||
"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate">{{ node.name }}</span>
|
||||
|
||||
<span
|
||||
class="flex shrink-0 items-center gap-0.5"
|
||||
:class="
|
||||
!node.locked && node.visible ? 'opacity-0 group-hover/row:opacity-100' : ''
|
||||
"
|
||||
>
|
||||
<Tip :label="node.locked ? t.unlock : t.lock">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
@click.stop="actions.toggleLock"
|
||||
>
|
||||
<icon-lucide-lock
|
||||
v-if="node.locked"
|
||||
class="size-3"
|
||||
:class="isSelected ? 'text-white' : 'text-surface'"
|
||||
/>
|
||||
<icon-lucide-unlock
|
||||
v-else
|
||||
class="size-3 opacity-0 group-hover/row:opacity-100"
|
||||
:class="isSelected ? 'text-white/80' : 'text-surface/70'"
|
||||
/>
|
||||
</span>
|
||||
</Tip>
|
||||
<Tip :label="node.visible ? t.hide : t.show">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
@click.stop="actions.toggleVisibility"
|
||||
>
|
||||
<icon-lucide-eye-off
|
||||
v-if="!node.visible"
|
||||
class="size-3"
|
||||
:class="isSelected ? 'text-white' : 'text-surface'"
|
||||
/>
|
||||
<icon-lucide-eye
|
||||
v-else
|
||||
class="size-3 opacity-0 group-hover/row:opacity-100"
|
||||
:class="isSelected ? 'text-white/80' : 'text-surface/70'"
|
||||
/>
|
||||
</span>
|
||||
</Tip>
|
||||
</span>
|
||||
|
||||
<!-- Rename mode -->
|
||||
<div
|
||||
v-if="instructionTargetId === node.id && instruction?.type === 'make-child'"
|
||||
class="pointer-events-none absolute inset-y-1 rounded border border-accent bg-accent/10"
|
||||
:style="{
|
||||
left: `${item.level * INDENT}px`,
|
||||
right: '4px'
|
||||
}"
|
||||
/>
|
||||
v-if="rename.editingId.value === node.id"
|
||||
class="flex w-full items-center gap-1 py-1"
|
||||
:style="{ paddingLeft: padLeft }"
|
||||
>
|
||||
<span
|
||||
v-if="item.hasChildren"
|
||||
class="flex w-4 shrink-0 cursor-pointer items-center justify-center text-muted transition-transform hover:text-surface"
|
||||
:class="isExpanded ? 'rotate-90' : 'rotate-0'"
|
||||
@click.stop="actions.toggleExpand"
|
||||
>
|
||||
<icon-lucide-chevron-right class="size-3" />
|
||||
</span>
|
||||
<span v-else class="w-4 shrink-0" />
|
||||
<component :is="nodeIcon(node)" class="size-3 shrink-0 opacity-70" />
|
||||
<input
|
||||
ref="renameInput"
|
||||
data-layer-edit
|
||||
data-test-id="layers-item-input"
|
||||
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none"
|
||||
:value="node.name"
|
||||
@blur="rename.commit(node.id, $event)"
|
||||
@keydown.stop="rename.onKeydown"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- DnD reorder indicator -->
|
||||
<div
|
||||
v-if="
|
||||
instructionTargetId === node.id &&
|
||||
instruction &&
|
||||
instruction.type !== 'make-child'
|
||||
"
|
||||
class="pointer-events-none absolute h-0.5 bg-accent"
|
||||
:class="{
|
||||
'bottom-0': instruction.type === 'reorder-below',
|
||||
'top-0': instruction.type === 'reorder-above'
|
||||
}"
|
||||
:style="{
|
||||
left: `${(item.level - 1) * INDENT}px`,
|
||||
width: `calc(100% - ${(item.level - 1) * INDENT}px)`
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</TreeItem>
|
||||
</LayerTreeItem>
|
||||
</template>
|
||||
<!-- Normal row -->
|
||||
<button
|
||||
v-else
|
||||
data-test-id="layers-item"
|
||||
class="group/row relative flex w-full cursor-pointer items-center gap-1 rounded border-none py-1 pr-1 text-left text-xs"
|
||||
:class="[
|
||||
isSelected
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-transparent text-surface hover:bg-hover',
|
||||
draggingId === node.id ? 'opacity-30' : '',
|
||||
instructionTargetId === node.id && instruction?.type === 'make-child'
|
||||
? 'bg-accent/15 text-surface outline-2 outline-accent outline-offset-[-2px]'
|
||||
: '',
|
||||
!node.visible ? 'opacity-50' : ''
|
||||
]"
|
||||
:style="{ paddingLeft: padLeft }"
|
||||
@dblclick="rename.start(node.id, node.name)"
|
||||
>
|
||||
<span
|
||||
v-if="item.hasChildren"
|
||||
class="flex w-4 shrink-0 cursor-pointer items-center justify-center text-muted transition-transform hover:text-surface"
|
||||
:class="isExpanded ? 'rotate-90' : 'rotate-0'"
|
||||
@click.stop="actions.toggleExpand"
|
||||
>
|
||||
<icon-lucide-chevron-right class="size-3" />
|
||||
</span>
|
||||
<span v-else class="w-4 shrink-0" />
|
||||
|
||||
<component
|
||||
:is="nodeIcon(node)"
|
||||
class="size-3 shrink-0"
|
||||
:class="
|
||||
COMPONENT_TYPES.has(node.type) ? 'text-component opacity-100' : 'opacity-70'
|
||||
"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate">{{ node.name }}</span>
|
||||
|
||||
<span
|
||||
class="flex shrink-0 items-center gap-0.5"
|
||||
:class="
|
||||
!node.locked && node.visible ? 'opacity-0 group-hover/row:opacity-100' : ''
|
||||
"
|
||||
>
|
||||
<Tip :label="node.locked ? t.unlock : t.lock">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
@click.stop="actions.toggleLock"
|
||||
>
|
||||
<icon-lucide-lock
|
||||
v-if="node.locked"
|
||||
class="size-3"
|
||||
:class="isSelected ? 'text-white' : 'text-surface'"
|
||||
/>
|
||||
<icon-lucide-unlock
|
||||
v-else
|
||||
class="size-3 opacity-0 group-hover/row:opacity-100"
|
||||
:class="isSelected ? 'text-white/80' : 'text-surface/70'"
|
||||
/>
|
||||
</span>
|
||||
</Tip>
|
||||
<Tip :label="node.visible ? t.hide : t.show">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
@click.stop="actions.toggleVisibility"
|
||||
>
|
||||
<icon-lucide-eye-off
|
||||
v-if="!node.visible"
|
||||
class="size-3"
|
||||
:class="isSelected ? 'text-white' : 'text-surface'"
|
||||
/>
|
||||
<icon-lucide-eye
|
||||
v-else
|
||||
class="size-3 opacity-0 group-hover/row:opacity-100"
|
||||
:class="isSelected ? 'text-white/80' : 'text-surface/70'"
|
||||
/>
|
||||
</span>
|
||||
</Tip>
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="instructionTargetId === node.id && instruction?.type === 'make-child'"
|
||||
class="pointer-events-none absolute inset-y-1 rounded border border-accent bg-accent/10"
|
||||
:style="{
|
||||
left: `${item.level * INDENT}px`,
|
||||
right: '4px'
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- DnD reorder indicator -->
|
||||
<div
|
||||
v-if="
|
||||
instructionTargetId === node.id &&
|
||||
instruction &&
|
||||
instruction.type !== 'make-child'
|
||||
"
|
||||
class="pointer-events-none absolute h-0.5 bg-accent"
|
||||
:class="{
|
||||
'bottom-0': instruction.type === 'reorder-below',
|
||||
'top-0': instruction.type === 'reorder-above'
|
||||
}"
|
||||
:style="{
|
||||
left: `${(item.level - 1) * INDENT}px`,
|
||||
width: `calc(100% - ${(item.level - 1) * INDENT}px)`
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</TreeItem>
|
||||
</LayerTreeItem>
|
||||
</template>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,45 +53,48 @@ function handlePageDblClick(
|
|||
</Tip>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
<div data-test-id="pages-scroll" class="scrollbar-thin h-full overflow-x-hidden overflow-y-auto px-1 pb-1">
|
||||
<div v-for="pg in pages" :key="pg.id">
|
||||
<div
|
||||
v-if="rename.editingId.value === pg.id"
|
||||
class="flex w-full items-center gap-1.5 rounded px-2 py-1"
|
||||
>
|
||||
<icon-lucide-file class="size-3 shrink-0 opacity-70" />
|
||||
<input
|
||||
ref="pageInput"
|
||||
data-test-id="pages-item-input"
|
||||
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none"
|
||||
:value="pg.name"
|
||||
@blur="rename.commit(pg.id, $event)"
|
||||
@keydown.stop="rename.onKeydown"
|
||||
/>
|
||||
<div
|
||||
data-test-id="pages-scroll"
|
||||
class="scrollbar-thin h-full overflow-x-hidden overflow-y-auto px-1 pb-1"
|
||||
>
|
||||
<div v-for="pg in pages" :key="pg.id">
|
||||
<div
|
||||
v-if="rename.editingId.value === pg.id"
|
||||
class="flex w-full items-center gap-1.5 rounded px-2 py-1"
|
||||
>
|
||||
<icon-lucide-file class="size-3 shrink-0 opacity-70" />
|
||||
<input
|
||||
ref="pageInput"
|
||||
data-test-id="pages-item-input"
|
||||
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0 text-xs text-surface outline-none"
|
||||
:value="pg.name"
|
||||
@blur="rename.commit(pg.id, $event)"
|
||||
@keydown.stop="rename.onKeydown"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isDivider(pg)"
|
||||
class="my-1 flex items-center px-2"
|
||||
@dblclick="startRename(pg)"
|
||||
>
|
||||
<div class="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
data-test-id="pages-item"
|
||||
class="flex w-full cursor-pointer items-center gap-1.5 rounded border-none px-2 py-1 text-left text-xs"
|
||||
:class="
|
||||
pg.id === currentPageId
|
||||
? 'bg-hover text-surface'
|
||||
: 'bg-transparent text-muted hover:bg-hover hover:text-surface'
|
||||
"
|
||||
@click="actions.switch(pg.id)"
|
||||
@dblclick="handlePageDblClick(pg, actions.rename)"
|
||||
>
|
||||
<icon-lucide-file class="size-3 shrink-0" />
|
||||
<span class="truncate">{{ pg.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isDivider(pg)"
|
||||
class="my-1 flex items-center px-2"
|
||||
@dblclick="startRename(pg)"
|
||||
>
|
||||
<div class="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
data-test-id="pages-item"
|
||||
class="flex w-full cursor-pointer items-center gap-1.5 rounded border-none px-2 py-1 text-left text-xs"
|
||||
:class="
|
||||
pg.id === currentPageId
|
||||
? 'bg-hover text-surface'
|
||||
: 'bg-transparent text-muted hover:bg-hover hover:text-surface'
|
||||
"
|
||||
@click="actions.switch(pg.id)"
|
||||
@dblclick="handlePageDblClick(pg, actions.rename)"
|
||||
>
|
||||
<icon-lucide-file class="size-3 shrink-0" />
|
||||
<span class="truncate">{{ pg.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,7 @@ interface ToolButtonProps extends RequiredTestIdProps {
|
|||
mobile?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
icon,
|
||||
active = false,
|
||||
mobile = false,
|
||||
testId
|
||||
} = defineProps<ToolButtonProps>()
|
||||
const { icon, active = false, mobile = false, testId } = defineProps<ToolButtonProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: []
|
||||
|
|
|
|||
|
|
@ -27,12 +27,7 @@ interface AppSelectProps<TValue extends string | number> extends TestIdProps {
|
|||
ui?: AppSelectUi
|
||||
}
|
||||
|
||||
const {
|
||||
options,
|
||||
placeholder,
|
||||
ui,
|
||||
testId = 'app-select-trigger'
|
||||
} = defineProps<AppSelectProps<T>>()
|
||||
const { options, placeholder, ui, testId = 'app-select-trigger' } = defineProps<AppSelectProps<T>>()
|
||||
|
||||
const modelValue = defineModel<T>({ required: true })
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||
|
||||
import { getSelectedNodes } from '#tests/helpers/store'
|
||||
|
||||
const editor = useEditorSetup()
|
||||
|
|
@ -20,7 +19,6 @@ function getSelectedCount() {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
test('copy + paste via store duplicates a shape', async () => {
|
||||
await editor.canvas.drawRect(100, 100, 120, 80)
|
||||
await editor.canvas.waitForRender()
|
||||
|
|
|
|||
|
|
@ -91,5 +91,10 @@ test('selecting a frame shows Frame in JSX', async () => {
|
|||
test('switching back to Design tab works', async () => {
|
||||
await designTab().click()
|
||||
|
||||
await expect(editor.page.getByTestId('design-panel-single').or(editor.page.getByTestId('design-panel-empty')).first()).toBeVisible()
|
||||
await expect(
|
||||
editor.page
|
||||
.getByTestId('design-panel-single')
|
||||
.or(editor.page.getByTestId('design-panel-empty'))
|
||||
.first()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -140,9 +140,7 @@ test('assets panel groups component sets and inserts the default variant', async
|
|||
await expect(details.getByTestId('asset-details-description')).toContainText(
|
||||
'Reusable button component'
|
||||
)
|
||||
await expect(details.getByTestId('asset-details-library')).toContainText(
|
||||
'lk-test-library'
|
||||
)
|
||||
await expect(details.getByTestId('asset-details-library')).toContainText('lk-test-library')
|
||||
await expect(details.getByTestId('asset-details-docs')).toBeVisible()
|
||||
await expect(details.getByTestId('asset-details-property')).toContainText('Type')
|
||||
await page.getByTestId('asset-details-close').click()
|
||||
|
|
@ -233,7 +231,9 @@ test('assets insertion accounts for entered container coordinates', async ({ pag
|
|||
const selected = selectedId ? store.graph.getNode(selectedId) : null
|
||||
if (!selected) return null
|
||||
const abs = store.graph.getAbsolutePosition(selected.id)
|
||||
const center = store.screenToCanvas(...Object.values(store.viewportCanvasCenter()) as [number, number])
|
||||
const center = store.screenToCanvas(
|
||||
...(Object.values(store.viewportCanvasCenter()) as [number, number])
|
||||
)
|
||||
return {
|
||||
parentId: selected.parentId,
|
||||
centerX: abs.x + selected.width / 2,
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ test('outline stroke is disabled for fill-only shapes', async () => {
|
|||
await editor.page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
test('Copy/Paste as submenu exists', async () => {
|
||||
test('Copy/Paste as submenu exists', async () => {
|
||||
await rightClickShape(130, 130)
|
||||
|
||||
const submenuTrigger = contextItem('context-copy-paste-as')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import { expect, test } from '@playwright/test'
|
|||
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
|
||||
test('dragging selected nested instance content reorders its auto-layout item', async ({ page }) => {
|
||||
test('dragging selected nested instance content reorders its auto-layout item', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/')
|
||||
const canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
|
|
@ -83,7 +85,9 @@ test('dragging selected nested instance content reorders its auto-layout item',
|
|||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('auto-layout drag does not show an insert indicator before order changes', async ({ page }) => {
|
||||
test('auto-layout drag does not show an insert indicator before order changes', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/')
|
||||
const canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
|
|
|
|||
|
|
@ -75,7 +75,9 @@ test('dragging a layer into a container expands it and shows the child', async (
|
|||
await source.dragTo(target, { targetPosition: { x: 70, y: 12 } })
|
||||
await canvas.waitForRender()
|
||||
|
||||
await expect(page.locator(`[data-node-id="${ids.rect}"]`).getByTestId('layers-item')).toBeVisible()
|
||||
await expect(
|
||||
page.locator(`[data-node-id="${ids.rect}"]`).getByTestId('layers-item')
|
||||
).toBeVisible()
|
||||
expect(await layerOrder(page, ids.frame)).toEqual([ids.rect])
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -78,14 +78,16 @@ test('stroke sides toggle shows per-side weight inputs', async () => {
|
|||
await expect(toggle).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const sectionInputsBefore = await page
|
||||
.getByTestId('stroke-section').getByTestId('scrub-input')
|
||||
.getByTestId('stroke-section')
|
||||
.getByTestId('scrub-input')
|
||||
.count()
|
||||
|
||||
await toggle.click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const sectionInputsAfter = await page
|
||||
.getByTestId('stroke-section').getByTestId('scrub-input')
|
||||
.getByTestId('stroke-section')
|
||||
.getByTestId('scrub-input')
|
||||
.count()
|
||||
expect(sectionInputsAfter).toBeGreaterThan(sectionInputsBefore)
|
||||
|
||||
|
|
@ -93,7 +95,8 @@ test('stroke sides toggle shows per-side weight inputs', async () => {
|
|||
await canvas.waitForRender()
|
||||
|
||||
const sectionInputsFinal = await page
|
||||
.getByTestId('stroke-section').getByTestId('scrub-input')
|
||||
.getByTestId('stroke-section')
|
||||
.getByTestId('scrub-input')
|
||||
.count()
|
||||
expect(sectionInputsFinal).toBe(sectionInputsBefore)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -148,7 +148,9 @@ test('blend modes', async () => {
|
|||
width: 324,
|
||||
height: 180,
|
||||
cornerRadius: 20,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.97, a: 1 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.97, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('RECTANGLE', pageId, {
|
||||
name: 'Multiply Base',
|
||||
|
|
@ -157,7 +159,9 @@ test('blend modes', async () => {
|
|||
width: 96,
|
||||
height: 96,
|
||||
cornerRadius: 20,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 0.23, b: 0.2, a: 0.95 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 1, g: 0.23, b: 0.2, a: 0.95 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('RECTANGLE', pageId, {
|
||||
name: 'Multiply Layer',
|
||||
|
|
@ -167,7 +171,9 @@ test('blend modes', async () => {
|
|||
height: 96,
|
||||
cornerRadius: 20,
|
||||
blendMode: 'MULTIPLY',
|
||||
fills: [{ type: 'SOLID', color: { r: 0.12, g: 0.45, b: 1, a: 0.92 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.12, g: 0.45, b: 1, a: 0.92 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('ELLIPSE', pageId, {
|
||||
name: 'Screen Layer',
|
||||
|
|
@ -176,7 +182,9 @@ test('blend modes', async () => {
|
|||
width: 104,
|
||||
height: 104,
|
||||
blendMode: 'SCREEN',
|
||||
fills: [{ type: 'SOLID', color: { r: 0.05, g: 0.75, b: 0.45, a: 0.8 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.05, g: 0.75, b: 0.45, a: 0.8 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('RECTANGLE', pageId, {
|
||||
name: 'Overlay Multi Fill',
|
||||
|
|
@ -215,7 +223,9 @@ test('alpha mask stack', async () => {
|
|||
width: 324,
|
||||
height: 180,
|
||||
cornerRadius: 20,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.08, g: 0.1, b: 0.18, a: 1 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.08, g: 0.1, b: 0.18, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('RECTANGLE', frame.id, {
|
||||
name: 'Unmasked Baseline',
|
||||
|
|
@ -224,7 +234,9 @@ test('alpha mask stack', async () => {
|
|||
width: 58,
|
||||
height: 132,
|
||||
cornerRadius: 12,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.98, g: 0.75, b: 0.18, a: 1 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.98, g: 0.75, b: 0.18, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('ELLIPSE', frame.id, {
|
||||
name: 'Alpha Mask',
|
||||
|
|
@ -243,7 +255,9 @@ test('alpha mask stack', async () => {
|
|||
width: 220,
|
||||
height: 38,
|
||||
rotation: -12,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.08, g: 0.73, b: 0.73, a: 1 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.08, g: 0.73, b: 0.73, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('RECTANGLE', frame.id, {
|
||||
name: 'Masked Purple Stripe',
|
||||
|
|
@ -252,7 +266,9 @@ test('alpha mask stack', async () => {
|
|||
width: 222,
|
||||
height: 42,
|
||||
rotation: -12,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.58, g: 0.27, b: 0.95, a: 1 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.58, g: 0.27, b: 0.95, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('RECTANGLE', frame.id, {
|
||||
name: 'Masked Pink Stripe',
|
||||
|
|
@ -282,7 +298,9 @@ test('smoothed corners with blended shadow', async () => {
|
|||
width: 272,
|
||||
height: 168,
|
||||
cornerRadius: 20,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.08, g: 0.1, b: 0.18, a: 1 }, visible: true, opacity: 1 }]
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.08, g: 0.1, b: 0.18, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('RECTANGLE', pageId, {
|
||||
name: 'Uniform Smooth Radius',
|
||||
|
|
@ -292,7 +310,9 @@ test('smoothed corners with blended shadow', async () => {
|
|||
height: 88,
|
||||
cornerRadius: 28,
|
||||
cornerSmoothing: 0.75,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.58, g: 0.27, b: 0.95, a: 1 }, visible: true, opacity: 1 }],
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.58, g: 0.27, b: 0.95, a: 1 }, visible: true, opacity: 1 }
|
||||
],
|
||||
effects: [
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
|
|
@ -317,7 +337,9 @@ test('smoothed corners with blended shadow', async () => {
|
|||
bottomRightRadius: 34,
|
||||
bottomLeftRadius: 10,
|
||||
cornerSmoothing: 1,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.08, g: 0.73, b: 0.73, a: 1 }, visible: true, opacity: 1 }],
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.08, g: 0.73, b: 0.73, a: 1 }, visible: true, opacity: 1 }
|
||||
],
|
||||
strokes: [
|
||||
{
|
||||
color: { r: 1, g: 1, b: 1, a: 0.8 },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { getPageChildren, getSelectedNode } from '#tests/helpers/store'
|
||||
|
||||
|
|
@ -11,9 +10,7 @@ test('ScrubInput drag changes X position', async () => {
|
|||
const before = await getSelectedNode(editor.page)
|
||||
const initialX = expectDefined(before, 'selected rectangle before drag').x
|
||||
|
||||
const xScrub = editor.page
|
||||
.getByTestId('position-section').getByTestId('scrub-input')
|
||||
.first()
|
||||
const xScrub = editor.page.getByTestId('position-section').getByTestId('scrub-input').first()
|
||||
await editor.canvas.dragScrubInput(xScrub, 50)
|
||||
|
||||
const after = await getSelectedNode(editor.page)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,40 @@
|
|||
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { getSelectedNode } from '#tests/helpers/store'
|
||||
|
||||
const editor = useEditorSetup()
|
||||
|
||||
|
||||
test('fill visibility supports repeat click and undo redo', async () => {
|
||||
await editor.canvas.drawRect(120, 120, 120, 80)
|
||||
await editor.canvas.waitForRender()
|
||||
|
||||
const fillButton = editor.page.getByTestId('fill-visibility-0')
|
||||
await expect(fillButton).toBeVisible()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
await fillButton.click()
|
||||
await editor.canvas.waitForRender()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(
|
||||
false
|
||||
)
|
||||
|
||||
await fillButton.click()
|
||||
await editor.canvas.waitForRender()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
await editor.canvas.undo()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(false)
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(
|
||||
false
|
||||
)
|
||||
|
||||
await editor.canvas.redo()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(true)
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').fills[0]?.visible).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test('stroke visibility supports repeat click and undo redo', async () => {
|
||||
|
|
@ -35,21 +43,31 @@ test('stroke visibility supports repeat click and undo redo', async () => {
|
|||
|
||||
const strokeButton = editor.page.getByTestId('stroke-visibility-0')
|
||||
await expect(strokeButton).toBeVisible()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible
|
||||
).toBe(true)
|
||||
|
||||
await strokeButton.click()
|
||||
await editor.canvas.waitForRender()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible
|
||||
).toBe(false)
|
||||
|
||||
await strokeButton.click()
|
||||
await editor.canvas.waitForRender()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible
|
||||
).toBe(true)
|
||||
|
||||
await editor.canvas.undo()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible).toBe(false)
|
||||
expect(
|
||||
expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible
|
||||
).toBe(false)
|
||||
|
||||
await editor.canvas.redo()
|
||||
expect(expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible).toBe(true)
|
||||
expect(
|
||||
expectDefined(await getSelectedNode(editor.page), 'selected node').strokes[0]?.visible
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('appearance visibility supports repeat click and undo redo in one step', async () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
import { getSelectedNode } from '#tests/helpers/store'
|
||||
|
||||
const editor = useEditorSetup()
|
||||
|
||||
|
||||
function getPageChildren() {
|
||||
return editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||
|
||||
import { getPageChildren } from '#tests/helpers/store'
|
||||
import { toolbarFlyoutItemTestId, toolbarFlyoutTestId } from '#tests/helpers/test-ids'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
|
||||
|
||||
import { variablesAddTestId } from '#tests/helpers/test-ids'
|
||||
|
||||
const editor = useEditorSetup()
|
||||
|
|
@ -57,9 +56,9 @@ test('add variable menu creates non-color variable types', async () => {
|
|||
|
||||
await editor.page.getByTestId('variables-add-variable').click()
|
||||
await editor.page.getByTestId(variablesAddTestId('STRING')).click()
|
||||
await expect(
|
||||
editor.page.getByTestId('variable-row').filter({ hasText: 'New text' })
|
||||
).toHaveCount(1)
|
||||
await expect(editor.page.getByTestId('variable-row').filter({ hasText: 'New text' })).toHaveCount(
|
||||
1
|
||||
)
|
||||
|
||||
await editor.page.getByTestId('variables-add-variable').click()
|
||||
await editor.page.getByTestId(variablesAddTestId('BOOLEAN')).click()
|
||||
|
|
@ -91,10 +90,7 @@ test('color swatch opens color picker', async () => {
|
|||
await editor.page.getByTestId('variables-section-open').click()
|
||||
await expect(editor.page.getByTestId('variables-dialog')).toBeVisible({ timeout: 3000 })
|
||||
|
||||
const swatch = editor.page
|
||||
.getByTestId('variable-row')
|
||||
.first()
|
||||
.getByTestId('color-picker-swatch')
|
||||
const swatch = editor.page.getByTestId('variable-row').first().getByTestId('color-picker-swatch')
|
||||
await expect(swatch).toBeVisible({ timeout: 3000 })
|
||||
await swatch.click()
|
||||
await expect(editor.page.getByTestId('color-picker-popover')).toBeVisible({ timeout: 5000 })
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ function actionItems(entries: readonly AppMenuEntry[]): AppMenuEntry[] {
|
|||
describe('APP_MENU_SCHEMA', () => {
|
||||
test('does not duplicate shortcuts for command-backed entries', () => {
|
||||
const duplicated = APP_MENU_SCHEMA.flatMap((group) =>
|
||||
actionItems(group.items).filter((entry) => !('type' in entry) && entry.command && entry.shortcut)
|
||||
actionItems(group.items).filter(
|
||||
(entry) => !('type' in entry) && entry.command && entry.shortcut
|
||||
)
|
||||
)
|
||||
|
||||
expect(duplicated).toEqual([])
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import {
|
|||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
|
||||
function expectFigmaEditableTextDefaults(textNode: NonNullable<Awaited<ReturnType<typeof parseFigmaClipboard>>>['nodes'][number]) {
|
||||
function expectFigmaEditableTextDefaults(
|
||||
textNode: NonNullable<Awaited<ReturnType<typeof parseFigmaClipboard>>>['nodes'][number]
|
||||
) {
|
||||
expect(textNode.textUserLayoutVersion).toBe(5)
|
||||
expect(textNode.textExplicitLayoutVersion).toBe(1)
|
||||
expect(textNode.textBidiVersion).toBe(1)
|
||||
|
|
@ -73,7 +75,9 @@ describe('buildFigmaClipboardHTML', () => {
|
|||
expectFigmaEditableTextDefaults(textNode)
|
||||
expect(textNode.derivedTextData?.glyphs).toBeDefined()
|
||||
expect(textNode.derivedTextData?.baselines?.length).toBeGreaterThan(0)
|
||||
expect(textNode.derivedTextData?.logicalIndexToCharacterOffsetMap?.length).toBe(text.text.length + 1)
|
||||
expect(textNode.derivedTextData?.logicalIndexToCharacterOffsetMap?.length).toBe(
|
||||
text.text.length + 1
|
||||
)
|
||||
expect(textNode.derivedTextData?.derivedLines).toEqual([{ directionality: 'LTR' }])
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { buildOpenPencilClipboardHTML, FigmaAPI, parseOpenPencilClipboard, SceneGraph } from '@open-pencil/core'
|
||||
import {
|
||||
buildOpenPencilClipboardHTML,
|
||||
FigmaAPI,
|
||||
parseOpenPencilClipboard,
|
||||
SceneGraph
|
||||
} from '@open-pencil/core'
|
||||
import type { SceneNode } from '@open-pencil/core'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
|
||||
import type { SceneNode } from '@open-pencil/core'
|
||||
|
||||
describe('clipboard roundtrip with images', () => {
|
||||
function graphWithImageNode(): {
|
||||
graph: SceneGraph
|
||||
|
|
@ -159,4 +163,3 @@ describe('clipboard roundtrip with images', () => {
|
|||
expect(clipboard.images.get(hash)).toEqual(bytes)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ describe('editor scoped hit testing', () => {
|
|||
})
|
||||
|
||||
editor.state.enteredContainerId = frame.id
|
||||
editor.setCanvasKit({} as Parameters<typeof editor.setCanvasKit>[0], {} as Parameters<typeof editor.setCanvasKit>[1])
|
||||
editor.setCanvasKit(
|
||||
{} as Parameters<typeof editor.setCanvasKit>[0],
|
||||
{} as Parameters<typeof editor.setCanvasKit>[1]
|
||||
)
|
||||
|
||||
expect(editor.hitTestAtPoint(130, 130)?.id).toBe(child.id)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ describe('booleanOperationSelected', () => {
|
|||
test('wraps selected nodes in a boolean operation container', () => {
|
||||
const editor = createEditor()
|
||||
const pageId = editor.state.currentPageId
|
||||
const first = editor.graph.createNode('RECTANGLE', pageId, { x: 10, y: 20, width: 30, height: 40 })
|
||||
const second = editor.graph.createNode('ELLIPSE', pageId, { x: 80, y: 90, width: 20, height: 10 })
|
||||
const first = editor.graph.createNode('RECTANGLE', pageId, {
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 30,
|
||||
height: 40
|
||||
})
|
||||
const second = editor.graph.createNode('ELLIPSE', pageId, {
|
||||
x: 80,
|
||||
y: 90,
|
||||
width: 20,
|
||||
height: 10
|
||||
})
|
||||
|
||||
editor.select([first.id, second.id])
|
||||
editor.booleanOperationSelected('UNION')
|
||||
|
|
@ -26,7 +36,10 @@ describe('booleanOperationSelected', () => {
|
|||
test('does not wrap unsupported text nodes', () => {
|
||||
const editor = createEditor()
|
||||
const pageId = editor.state.currentPageId
|
||||
const first = editor.graph.createNode('TEXT', pageId, { text: 'Nope', fontFamily: 'Definitely Missing Font' })
|
||||
const first = editor.graph.createNode('TEXT', pageId, {
|
||||
text: 'Nope',
|
||||
fontFamily: 'Definitely Missing Font'
|
||||
})
|
||||
const second = editor.graph.createNode('RECTANGLE', pageId)
|
||||
|
||||
editor.select([first.id, second.id])
|
||||
|
|
@ -57,7 +70,16 @@ describe('booleanOperationSelected', () => {
|
|||
const editor = createEditor()
|
||||
const pageId = editor.state.currentPageId
|
||||
const first = editor.graph.createNode('RECTANGLE', pageId, {
|
||||
fills: [{ type: 'IMAGE', imageHash: 'image', imageScaleMode: 'FILL', color: TRANSPARENT, opacity: 1, visible: true }]
|
||||
fills: [
|
||||
{
|
||||
type: 'IMAGE',
|
||||
imageHash: 'image',
|
||||
imageScaleMode: 'FILL',
|
||||
color: TRANSPARENT,
|
||||
opacity: 1,
|
||||
visible: true
|
||||
}
|
||||
]
|
||||
})
|
||||
const second = editor.graph.createNode('RECTANGLE', pageId)
|
||||
|
||||
|
|
@ -82,7 +104,12 @@ describe('booleanOperationSelected', () => {
|
|||
expect(editor.graph.getNode(pageId)?.childIds).toEqual([before.id, booleanId, after.id])
|
||||
|
||||
editor.undo.undo()
|
||||
expect(editor.graph.getNode(pageId)?.childIds).toEqual([before.id, first.id, second.id, after.id])
|
||||
expect(editor.graph.getNode(pageId)?.childIds).toEqual([
|
||||
before.id,
|
||||
first.id,
|
||||
second.id,
|
||||
after.id
|
||||
])
|
||||
expect(editor.state.selectedIds).toEqual(new Set([first.id, second.id]))
|
||||
|
||||
editor.undo.redo()
|
||||
|
|
|
|||
|
|
@ -45,8 +45,18 @@ describe('frameSelection', () => {
|
|||
test('undo and redo restore frame selection state', () => {
|
||||
const editor = createEditor()
|
||||
const pageId = editor.state.currentPageId
|
||||
const first = editor.graph.createNode('RECTANGLE', pageId, { x: 20, y: 30, width: 40, height: 50 })
|
||||
const second = editor.graph.createNode('ELLIPSE', pageId, { x: 100, y: 120, width: 30, height: 20 })
|
||||
const first = editor.graph.createNode('RECTANGLE', pageId, {
|
||||
x: 20,
|
||||
y: 30,
|
||||
width: 40,
|
||||
height: 50
|
||||
})
|
||||
const second = editor.graph.createNode('ELLIPSE', pageId, {
|
||||
x: 100,
|
||||
y: 120,
|
||||
width: 30,
|
||||
height: 20
|
||||
})
|
||||
|
||||
editor.select([first.id, second.id])
|
||||
editor.frameSelection()
|
||||
|
|
|
|||
|
|
@ -46,4 +46,3 @@ describe('FigmaAPI.createImage', () => {
|
|||
expect(hash).toMatch(/^[0-9a-f]{40}$/)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -21,4 +21,3 @@ describe('degToRad / radToDeg', () => {
|
|||
expect(radToDeg(degToRad(-90))).toBeCloseTo(-90, 10)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -59,4 +59,3 @@ describe('computeAbsoluteBounds', () => {
|
|||
expect(result).toEqual({ x: 10, y: 20, width: 60, height: 60 })
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -92,4 +92,3 @@ describe('rotatedBBox', () => {
|
|||
expect(pos.bottom - pos.top).toBeCloseTo(neg.bottom - neg.top, 5)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ describe('text node export', () => {
|
|||
await initCodec()
|
||||
|
||||
const { unzipSync, inflateSync } = await import('fflate')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } = await import('#core/kiwi/schema-runtime')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } =
|
||||
await import('#core/kiwi/schema-runtime')
|
||||
const { parseFigKiwiChunks } = await import('@open-pencil/core')
|
||||
|
||||
const graph = new SceneGraph()
|
||||
|
|
@ -123,7 +124,8 @@ describe('text node export', () => {
|
|||
await initCodec()
|
||||
|
||||
const { unzipSync, inflateSync } = await import('fflate')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } = await import('#core/kiwi/schema-runtime')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } =
|
||||
await import('#core/kiwi/schema-runtime')
|
||||
const { parseFigKiwiChunks } = await import('@open-pencil/core')
|
||||
|
||||
const graph = new SceneGraph()
|
||||
|
|
@ -148,7 +150,10 @@ describe('text node export', () => {
|
|||
}
|
||||
const message = compiled.decodeMessage(inflateSync(chunks?.[1] ?? new Uint8Array()))
|
||||
const nodeChanges = message.nodeChanges as Array<Record<string, unknown>>
|
||||
const textNc = expectDefined(nodeChanges.find((nc) => nc.type === 'TEXT'), 'text node change')
|
||||
const textNc = expectDefined(
|
||||
nodeChanges.find((nc) => nc.type === 'TEXT'),
|
||||
'text node change'
|
||||
)
|
||||
const derivedTextData = textNc.derivedTextData as Record<string, unknown>
|
||||
const fontMetaData = expectDefined(
|
||||
derivedTextData.fontMetaData as Array<Record<string, unknown>> | undefined,
|
||||
|
|
@ -163,7 +168,8 @@ describe('text node export', () => {
|
|||
await initCodec()
|
||||
|
||||
const { unzipSync, inflateSync } = await import('fflate')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } = await import('#core/kiwi/schema-runtime')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } =
|
||||
await import('#core/kiwi/schema-runtime')
|
||||
const { parseFigKiwiChunks } = await import('@open-pencil/core')
|
||||
|
||||
const graph = new SceneGraph()
|
||||
|
|
@ -195,7 +201,10 @@ describe('text node export', () => {
|
|||
}
|
||||
const message = compiled.decodeMessage(inflateSync(chunks?.[1] ?? new Uint8Array()))
|
||||
const nodeChanges = message.nodeChanges as Array<Record<string, unknown>>
|
||||
const textNc = expectDefined(nodeChanges.find((nc) => nc.type === 'TEXT'), 'text node change')
|
||||
const textNc = expectDefined(
|
||||
nodeChanges.find((nc) => nc.type === 'TEXT'),
|
||||
'text node change'
|
||||
)
|
||||
|
||||
expect(textNc.textAutoResize).toBe('HEIGHT')
|
||||
expect(textNc.lineHeight).toBeUndefined()
|
||||
|
|
@ -206,7 +215,8 @@ describe('text node export', () => {
|
|||
await initCodec()
|
||||
|
||||
const { unzipSync, inflateSync } = await import('fflate')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } = await import('#core/kiwi/schema-runtime')
|
||||
const { decodeBinarySchema, compileSchema, ByteBuffer } =
|
||||
await import('#core/kiwi/schema-runtime')
|
||||
const { parseFigKiwiChunks } = await import('@open-pencil/core')
|
||||
|
||||
const graph = new SceneGraph()
|
||||
|
|
@ -253,20 +263,23 @@ describe('text node export', () => {
|
|||
expect(families).toContain('Regular')
|
||||
})
|
||||
|
||||
test.if(runsHeavyTests)('material3.fig text nodes have derivedTextData after round-trip', async () => {
|
||||
const original = await parseFixture('material3.fig')
|
||||
test.if(runsHeavyTests)(
|
||||
'material3.fig text nodes have derivedTextData after round-trip',
|
||||
async () => {
|
||||
const original = await parseFixture('material3.fig')
|
||||
|
||||
const textNodes = [...original.getAllNodes()].filter((n) => n.type === 'TEXT')
|
||||
expect(textNodes.length).toBeGreaterThan(0)
|
||||
const textNodes = [...original.getAllNodes()].filter((n) => n.type === 'TEXT')
|
||||
expect(textNodes.length).toBeGreaterThan(0)
|
||||
|
||||
const exported = await exportFigFile(original)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
const exported = await exportFigFile(original)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const reimportedText = [...reimported.getAllNodes()].filter((n) => n.type === 'TEXT')
|
||||
expect(reimportedText.length).toBe(textNodes.length)
|
||||
const reimportedText = [...reimported.getAllNodes()].filter((n) => n.type === 'TEXT')
|
||||
expect(reimportedText.length).toBe(textNodes.length)
|
||||
|
||||
for (const node of reimportedText.slice(0, 10)) {
|
||||
expect(node.text.length).toBeGreaterThan(0)
|
||||
for (const node of reimportedText.slice(0, 10)) {
|
||||
expect(node.text.length).toBeGreaterThan(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { importNodeChanges } from '#core/kiwi/fig/import'
|
||||
import type { NodeChange } from '#core/kiwi/fig/codec'
|
||||
import { importNodeChanges } from '#core/kiwi/fig/import'
|
||||
|
||||
const documentGuid = { sessionID: 0, localID: 0 }
|
||||
const pageGuid = { sessionID: 0, localID: 1 }
|
||||
|
|
@ -39,7 +39,13 @@ describe('Figma component property import', () => {
|
|||
test('applies text data component prop assignments', () => {
|
||||
const nodeChanges: NodeChange[] = [
|
||||
{ guid: documentGuid, phase: 'CREATED', type: 'DOCUMENT', name: 'Document' },
|
||||
{ guid: pageGuid, phase: 'CREATED', parentIndex: { guid: documentGuid, position: '!' }, type: 'CANVAS', name: 'Page' },
|
||||
{
|
||||
guid: pageGuid,
|
||||
phase: 'CREATED',
|
||||
parentIndex: { guid: documentGuid, position: '!' },
|
||||
type: 'CANVAS',
|
||||
name: 'Page'
|
||||
},
|
||||
{
|
||||
guid: componentGuid,
|
||||
phase: 'CREATED',
|
||||
|
|
@ -80,7 +86,13 @@ describe('Figma component property import', () => {
|
|||
test('propagates nested instance swaps through clone chains', () => {
|
||||
const nodeChanges: NodeChange[] = [
|
||||
{ guid: documentGuid, phase: 'CREATED', type: 'DOCUMENT', name: 'Document' },
|
||||
{ guid: pageGuid, phase: 'CREATED', parentIndex: { guid: documentGuid, position: '!' }, type: 'CANVAS', name: 'Page' },
|
||||
{
|
||||
guid: pageGuid,
|
||||
phase: 'CREATED',
|
||||
parentIndex: { guid: documentGuid, position: '!' },
|
||||
type: 'CANVAS',
|
||||
name: 'Page'
|
||||
},
|
||||
{
|
||||
guid: strokeStyleGuid,
|
||||
phase: 'CREATED',
|
||||
|
|
@ -177,9 +189,7 @@ describe('Figma component property import', () => {
|
|||
symbolData: { symbolID: mailIconGuid },
|
||||
size: { x: 16, y: 16 },
|
||||
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
||||
componentPropRefs: [
|
||||
{ defID: iconPropGuid, componentPropNodeField: 'OVERRIDDEN_SYMBOL_ID' }
|
||||
]
|
||||
componentPropRefs: [{ defID: iconPropGuid, componentPropNodeField: 'OVERRIDDEN_SYMBOL_ID' }]
|
||||
},
|
||||
{
|
||||
guid: sourceInstanceGuid,
|
||||
|
|
@ -220,7 +230,9 @@ describe('Figma component property import', () => {
|
|||
|
||||
const graph = importNodeChanges(nodeChanges, [], undefined, { populate: 'all' })
|
||||
const clone = Array.from(graph.getAllNodes()).find((node) => node.name === 'Menu item clone')
|
||||
const icon = clone?.childIds.map((id) => graph.getNode(id)).find((node) => node?.type === 'INSTANCE')
|
||||
const icon = clone?.childIds
|
||||
.map((id) => graph.getNode(id))
|
||||
.find((node) => node?.type === 'INSTANCE')
|
||||
const iconChild = icon?.childIds.map((id) => graph.getNode(id)).find(Boolean)
|
||||
expect(icon?.name).toBe('icon/user')
|
||||
expect(iconChild?.name).toBe('user-path')
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue