apply_order gave fillStyle, strokeStyle, effectStyle, gridStyle and
textStyle the rank of plain values, so the JSON map's byte order decided:
fills, strokes, effects, grids, layoutGrids and variationSettings sort
after their link and overrode it (node.set then unlinked the style;
node.create kept a link its values contradicted). Style links now apply
after the values and x/y, just before boundVariables. A deviation set in a
later call still unlinks.
Found while testing crates/doc/src/proto_expr_parse.rs:462 (variable lookup before the true/false literals at :466) with crates/doc/src/proto_expr.rs:151/186 (display writes the literal as bare `true` and var_ref writes such a variable bare)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A third row in open_subpaths.svg: a filled and stroked path that is only
0.001 short stays one layer, and an open triangle with
paint-order="stroke" puts its stroke layer under its fill layer.
A file whose font family variables alias each other in several modes made
the missing-fonts walk branch in every mode, billions of steps before the
depth limit stopped it, freezing the app on open and `figcraft-cli info`.
The walk now visits each variable once.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SVG fills every subpath as if it were closed; the import filled only
subpaths that close themselves (a z, or an end within 1e-6 of the
start). Logos exported from PDF round relative coordinates to three
decimals, so their outlines end about 0.001 short of where they started
and most of their letters came in empty.
A filled path now has its open subpaths closed before it becomes a
vector network: an end within 1/1000 of the path's size snaps onto the
start, any other open subpath of two or more segments gets a closing
segment. Stroke-only paths are unchanged. A filled and stroked path
with a really open subpath becomes a fill layer (closed) under a stroke
layer (open), so the stroke stays open as SVG draws it.
SaveFiles named each test's folder after the process id and the
clock in nanoseconds. The tests run in parallel and the macOS clock
counts microseconds, so two of them often got the same folder and one
failed with AlreadyExists (14 of 30 runs here). A counter now keeps
the names apart.
Switching an instance to another variant copied the old root's sizing
and min/max back over the new variant's on the rebuild, so an instance
switched from a hugging variant to a fixed-width one whose children
fill kept hugging at the old width. The next edit then turned those
children Fixed, as a child can't fill an axis its parent hugs.
Resizing still equal to the old variant's was never the instance's own,
so on a sibling-variant switch it now follows the new variant; sizing or
min/max the instance set itself stays.
- --no-system-fonts holds for .sketch imports too: the CLI opts the
process out, so the import's font matching doesn't scan.
- Shared text styles get the installed family and platform tracking
their linked layers get.
- Each text run writes its letter spacing, line height and font axes,
neutral when it has none, instead of taking the first run's.
- The unsupported-drop hint and its catalogs list .sketch.
- A test sends damaged .sketch data through file.open and file.import.
- Stack items are matched to their source layers through a map.
- Docs: .sketch is a convert input; the Carbon kit's Sketch 47 edition
is refused (the 68 and 83 ones open).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- usage: a non-plain Vue argument escapes " as " in the attribute value (same rule as the connected-code path)
- usage: Vue pads with two spaces like the markup, not four
- test: an unconnected instance with a quoted title asserts inspect.componentCode and inspect.code
Dev Mode generates React, Leptos, SwiftUI and Compose; a Vue project had to translate the HTML
instead. `inspect.code {language: "vue"}` now writes a single-file component: a `<template>`, a
`<script setup>` with `defineProps` and its defaults, and a `<style scoped>`.
A Vue template holds no arbitrary expressions, so what the web targets write as a braced
expression has its own spelling. Text is `{{ prop }}`, and braces in text are escaped; a boolean
property's layer carries `v-if="prop"` rather than a wrapper element; a slot becomes Vue's own
`<slot name="…">` with its fallback content, which takes no prop, and an instance's content in one
is a `<template #name>`. Variants switch on the root's `data-*` attributes, as on the web.
`codeConnect` links a component to its code as a single-file component: a default import, and a
quoted binding, so `:size={size}` gives `:size="Large"` and a component swap passes the component
itself. A value that needs escapes keeps them inside the quotes, where the attribute's own quotes
become `"`.
`inspect.componentCode` kept a second copy of the code-language list, which is how Vue was missed
when `inspect.code` learned it; it now asks `code_connect::language`, the one list, as
`codeConnect` does.
Eleven tests in `vue_codegen_tests` and a `codeConnect` case; the Inspect panel's language picker
picks Vue up from `CodeLang::ALL`, and the language lists in the docs, the Dev Mode hint and its
thirteen catalogs name it.
A Shift-click with the Pen already put the next point at a 45° step
from the last one, but the line drawn to the pointer while moving
ignored Shift and followed the pointer, so holding Shift looked like it
did nothing. The line now ends where the click will put the point. Both
use one helper, at_45_degrees.
Texts an auto layout places keep the layout's position (the next layout
pass would take the baseline move back). A faded layer's group takes the
layer's id, so stacks, links and overrides still find it; faded masks
stay masks. figcraft-format no longer depends on figcraft-layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tinting copies a group's or master's layers; the copy is charged to the
import's work budget (past it, tints are skipped with a note). An
override handed to a nested instance costs its value's size, so deep
chains of large overrides can't be copied without end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tint or fill that isn't a JSON object is left alone instead of being
indexed. A structural fuzz replaces each value of a rich file with values
of other types and imports every variant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sketch draws an artboard's layout and square grids only while its layout
view is on; they now import hidden (still there, shown with the eye in
the Layout grid section).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alpha masks only mask (no drawn copy of their fill); a progressive
opacity (Fade) becomes an alpha mask of its gradient over the layer;
linear gradients run square in pixels as Sketch draws them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The font database knows each face's PostScript name; the Sketch import
finds the installed face a file's font names (CourierNewPSMT is Courier
New Regular) and reads the family from the name only when it isn't
installed. Text without a fixed line height takes the line Sketch draws:
ascent and descent rounded to whole pixels, the line gap left out, a
fifth more for classic Mac fonts, the baseline where Sketch puts it
(measured on sketchtool renders of 80 fonts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sketch keys a font's variation axes by the tag read as a number; they map
onto the text style's variations (a fontVariations key in the REST
mapping). Font references without font data only name the fonts a
document uses and no longer raise the embedded-font note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Groups as frames and graphics, stacks as auto layout (instances laid out
by their stack), corners and per-side borders from the style, pins and
sizing, tints (layeringType, fillColor overrides), text colour and
visibility overrides, blur lists (progressive, glass), inner shadows in
the shadow list, angular gradients starting on the right, text sizing,
and texts an override changed measured again on open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compared with Sketch's renders of a Sketch 64 file. Resized instances lay
out their layers the way Sketch does (measured: pins share the change in
proportion, a fixed size keeps its centre's share, and files before format
110 just scale); masters' backgrounds show in instances only when Sketch
includes them; swaps to a missing symbol keep the original; artboards
don't draw a style they carry. Apple's system fonts are found under the
names designs use (SF Pro, SF UI Text and Display, SF Compact), the
optical size follows the text size for named styles too, and imported
text keeps the platform's trak tracking as letter spacing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compared artboard by artboard with Sketch's renders of a real Sketch 50
file (sketchtool, black-box). Shape groups whose children have no
operation are one compound path filled by the group's winding rule (read
from the layer), not a union: rings, map streets and icons keep their
holes. Filled open paths close, as Sketch fills them. Flips apply after
rotation. Text with a fixed line height sits where Sketch's line spacing
behaviour puts it. Edited ovals and rectangles keep their points. Bitmap
colour adjustments become image filters. scale 0 means unscaled. A local
test compares sample files with sketchtool's renders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the branch review: overrides that point at junk (shared styles that
aren't objects, text runs that aren't) no longer panic; symbol expansion
spends a work budget (JSON copied, overrides handed on to nested
instances, grouped once by their first path segment), so small files
can't expand into gigabytes; far path points stay in range; notes are
capped and de-duplicated with a set; shared styles are looked up by
index; a styled second mask's drawn copy isn't clipped by the first;
JSON members are refused past 100 times their packed size or 2 GB in
all, and an unreadable document.json keeps the pages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FIGCRAFT_SKETCH_SAMPLES runs every .sketch file of a folder through
file.open (opens with linked instances and finite geometry, or is
refused as too old). File format parity gains a .sketch row; CLI, MCP,
roadmap and ROADMAP mention the import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layers linked to artboards navigate there on click (back links go back,
slide animations slide), hotspots become transparent frames with their
link, flow-home artboards start a flow; export options become export
settings (scale, width or height, suffix); artboard layout and square
grids become layout grids, which instances show like their masters.
Smart Layout, stacks and locked overrides are noted once per file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared layer styles become a paint style for their fills, a paint style
for their border colours (linked to strokes) and an effect style; shared
text styles become text styles (colour stays on the layers); local and
library copies alike, unused ones listed too. Layers link to them, keeping
their own values where they differ. Color Variables (swatches, also
library ones) become colour variables in a Color Variables collection,
with fills, borders and text colours bound to them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symbol masters become components, also those copied from libraries (on a
page of their own); instances carry their master's layers with their
overrides applied (text, nested symbol swaps and hiding, images, layer
and text styles, nested paths) and resize them by their constraints, so
editing a master still updates every instance. A symbol inside itself
stops with a note; instances of missing symbols are empty frames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text layers keep their string and styled runs (font, size, colour, line
height, letter spacing, decorations, text transform; ranges in UTF-16
units, clamped), paragraph alignment and spacing, vertical alignment and
how the box sizes. Fonts named by PostScript name become family and
style; the engine then matches families to installed ones ignoring
spaces (PingFangSC → PingFang SC). Fonts embedded in newer files are
named in the import report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sketch draws a clipping mask with its fills, borders and effects under
what it clips; a FigCraft mask only clips. A plain copy of such a mask
goes underneath it, so a light avatar circle with a white icon in it no
longer vanishes on a white artboard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sketch 51 and older files make masks from shape groups; the mask flag was
lost when the group became its shape or boolean operation, so a masked map
spilled out of its card.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Solid, gradient (linear, radial with its ellipse, angular) and image
pattern fills (tile, fill, stretch, fit); borders as strokes with the
first enabled border's width and position, dashes, caps, joins and arrow
ends; shadows, inner shadows, layer and background blurs; layer and paint
opacity and blend modes; clipping masks, wrapped in a group of their own
where Sketch breaks the mask chain; Display P3 documents. Shape groups
pass their style to the shape or boolean operation they become. Noise
fills, motion and zoom blurs are listed in the import report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paths become editable vector networks (handles, per-point corner radius,
open and closed, winding rule). A shape group with one shape becomes that
shape in the group's place; several combine as boolean operations, mixed
operations nesting from the left as Sketch applies them. Stars and
polygons stay stars and polygons while their points are the generated
ones, edited ones and triangles become vectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Artboards (and Sketch 2025 frames) become frames with their background
(white by default, as Sketch shows them), groups and graphics groups,
rectangles with their corner radii, ovals ellipses, bitmaps rectangles
with the image, slices slices. Positions accumulate through groups,
rotation and flips keep their direction, resizingConstraint bits become
constraints. Not yet mapped layer kinds are listed in the import report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.sketch archives (document.json, meta.json, pages/*.json) open through
file.open, file.import, drag and drop, the CLI and MCP; files saved by a
Sketch older than 48 are refused with a message saying how to convert
them. Layers come in the next commits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`file.saveAs {data: true}` marked the file saved before the UI handed the
bytes to `Services::download`. When the download was refused, the toast
said "Couldn't download" but the document was clean, so closing it no
longer warned and the edits could be lost.
The data result now leaves the save state alone. A new `file.markSaved`
(the bookkeeping a native save already did: the file and its branch tabs)
runs from `save_as_dialog` only once the download was delivered. It ends a
version preview like the other commands that act on the document itself.
Tests: the engine save test checks the file stays dirty until
`file.markSaved`; `a_refused_download_says_so` checks the document is
still dirty.
Clash detection compared every variable with every other, and suffixes
rescanned the taken list, so a file with very many variables made the
export quadratic. Counts in a HashMap, taken names in a HashSet and a next
suffix per base keep it linear.
Instance swap defaults that name an in-file main skipped by the layer cap
were reported as "isn't in the file": only instance targets fed the
capped-mains list. Defaults now feed it too.