Commit Graph

90 Commits

Author SHA1 Message Date
BFlat 5f54fecca2
M8: 50k-layer zoomed-out renders and faster save/load (#63)
Canvas previews zoomed out to 50% or less draw levels of detail
(crates/render/src/lod.rs): subtrees under 2 device px are splatted as
their averaged paint colours (box filter, no paths or effects), text
under 4 px is greeked into bars, small solid rectangles are splatted and
rectangular clip layers are composited without the blend pipeline.
Image paints and masks keep the exact path. Exports, thumbnails and
every other render stay exact (Options::lod is off by default); render
bands are assembled with a row copy instead of a pixmap draw.

.figcraft documents are serialized straight into the compressor and
parsed straight from the text (a header pass reads version and image
index without building a value tree); layer and text fields at their
default values are left out (63 -> 22 MB of JSON at 50k layers, older
files still load); JSON members use deflate level 3.

At 50k layers: zoomed-to-fit render ~58 -> ~8 ms, save ~1.0 s -> ~0.09 s,
load ~0.86 s -> ~0.16 s.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:27:51 +07:00
BFlat 1184dd4177
M4: variable width strokes (#58)
A stroke can carry a width profile: widths at positions along the path
(each subpath by length), interpolated smoothly (monotone cubic).

- doc: WidthProfile / WidthPoint / WidthPreset (uniform, wedge, taper both
  ends, eye, rounded, pinch) on StrokeProps, serde-defaulted so older files
  load; paint reach and hit testing follow the widest point.
- pathops: path measuring by length and the variable outline (offset along
  the normals, round/miter/bevel joins, caps, dashes keep their width,
  Inside/Outside clip as before; bounded samples and dashes, uniform
  fallback). Canvas, PNG, SVG (filled outline), PDF and outline stroke use it.
- engine: stroke.widthProfile {preset | points}, node.set widthProfile,
  inspect output; the Variable width tool (Shift+W) in vector edit mode:
  drag the path to add a width point, slide points, drag their handles,
  Delete removes the selected one; vector.edit.widthPoint add/set/remove.
  Every gesture is one undo step.
- UI: width profile dropdown with previews drawn in code and a flip button
  in the stroke settings; the tool in the edit toolbar (new icon drawn in
  code); width handles and a hover preview on the canvas; Position/Width
  fields for the selected width point.
- Tests: profile maths, outlines, render regressions, tool gestures,
  commands, exports and junk sweeps. ROADMAP, README, docs updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:12:21 +07:00
BFlat b2738d8f0b
M5: prototype device rotation, presentation scaling, scroll momentum, mode picker (#62)
- Device rotation: phones and tablets turn to landscape (`proto.settings {rotation}`), saved in
  the file and imported from REST files (`CCW_90`); the presenter and the preview pane size the
  screen to the turned device and draw its body turned. A rotate toggle sits beside the device
  picker in the Prototype panel.
- Presentation options: fit, fit width, fill and 100% (`proto.presentOptions {scale?,
  deviceFrame?}`, an Options menu in the presenter, Z cycles them, R restarts), the device frame
  shown or hidden, kept between presentations; what overflows the window pans by wheel or drag.
  Presenter textures are capped to the GPU's limit (big frames at 100% no longer overflow it).
- Scroll momentum: drags fling and slow down exactly per frame-rate-independent sub-steps, the
  wheel coasts after it stops, a drag past the ends stretches and springs back; repaints only
  while moving, a press catches a fling.
- Variable modes in the Design panel: frames pick a mode per multi-mode collection, or Auto
  (inherited); `variable.setMode {mode: null}` sets Auto.
- Tests in new modules (engine proto_present_tests, UI present_view_tests); docs and ROADMAP.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:48:29 +07:00
BFlat b26bbb2da8
M7: pixel-exact icon set (#61)
Every UI icon redrawn on a strict 16 px grid: strokes on pixel centres, fills on pixel edges, and
consistent proportions. Painting snaps the glyph to the pixel grid, scales by whole steps above
16 px, hints straight strokes below it, and keeps strokes one physical pixel wide, so icons stay
sharp at every size and display scale. Concave fills (the move cursor) are triangulated instead of
drawn as convex polygons, which broke them. Icon buttons paint their glyph at its native 16 px.

Redrawn: hand, home, move cursor, comment, moon, undo/redo, rotate/reset, link, corner radius,
flip, eye, lock, clock and the rest of the set. `Icon::ALL` lists every icon; the control channel's
`ui.set {iconGallery: true}` shows them all at 12/16/24/32 px for review.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:21:53 +07:00
BFlat d7fd41cd13
M3: library variables (#60)
Publishing a library now includes the file's variable collections (modes,
aliases, scopes), versioned per variable and collection; variables hidden
from publishing stay out unless a published variable aliases them or a
published component binds them. Files using the library see its
collections read-only in the Variables editor and the variable pickers;
binding, aliasing or setting a collection's mode copies the variables in
as read-only linked copies, library components keep their bindings, and
"Library updates available" covers changed variables (bound layers follow).

New commands: library.importVariable, library.bindVariable,
library.aliasVariable, library.setVariableMode; variable.update takes
hiddenFromPublishing.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:06:18 +07:00
BFlat 20ca885084
M1: custom window title bar (#59)
The tab strip is now the window's title bar instead of sitting under the system one. On Windows,
Linux and FreeBSD the window is frameless: empty bar space drags it (double-click maximises or
restores), minimise / maximise-restore / close buttons sit at the right end (close turns red on
hover), and invisible strips along the edges and corners resize it (not while maximised). On macOS
the system window buttons are inset into the strip. The browser build is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:40:38 +07:00
Brandon Thomas cf75c6e9ec Release: FreeBSD job on the org's 16-core runner (private repo's standard runner can't fit the VM)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:17:58 -04:00
Brandon Thomas 7acc224ca2 About: Contributors and Models tabs with full credits; bump version to 0.2.0 (from 0.1.0) for release
Ports PhotoCraft's credits view (craftrules standards/contributors.md): contributor table with
PR/commit/line stats and verified names, AI-model credits, generated contributors.json, and
docs/contributors.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:10:33 -04:00
BFlat 0638c98564
M3: component slots and nested override edge cases (#56)
Slots: a frame of a main component becomes a slot property
(component.makeSlot / removeSlot, preferred components, type "slot" in
component.properties). Instances show the main's default content until
the slot is edited; from then on its layers are the instance's own
(override group "slot"): free to add, remove and reorder, kept across
main updates, variant switches, copy/paste and detach, and instances
placed there stand alone. instance.slotInsert fills a slot,
instance.resetSlot brings the default content back. Structure
protection allows edits inside slots only; drops from the Assets panel,
canvas drags, drawing and paste land in slots. UI: slot icon in the
layers panel, dashed outlines and labels on the canvas, slot property
rows with a fill menu in the properties panel, Make/Remove slot on
frames inside a main.

Nested overrides: overrides inside a nested instance survive the main
moving it into another frame (layers matched by source alone when
unique); a nested instance copied out of an instance is re-linked to
its own main with its look kept (values set by component properties
aren't overrides); plain layers copied out of instances stop mirroring
them; canvas drags can no longer pull layers out of an instance or
reorder its auto layout children. Tests for swaps, variant switches,
main restructuring, detach and copy/paste.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:04:13 +07:00
BFlat ec5da02a35
M3: bind effects to variables (#55)
Shadow X, Y, blur, spread and colour, and layer/background blur radius
take variables from the Effects section like other fields (variable
button on hover, a chip to change or detach once bound). Bindings are
stored as `effects/<index>/<offsetX|offsetY|radius|spread|color>` and
applied in normalisation; pickers offer effect-scoped variables.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:52:38 +07:00
BFlat 2105faec4e
M7: README: ArtCraft logo from a public URL so it loads in every client (#57)
This repository is private, so clients that show the README without the
viewer's GitHub session (the GitHub mobile app, link previews) can't fetch
images stored here, and the ArtCraft logo showed as a broken image. Load
the identical logo and mark from the public VectorCraft repository
instead, the same files every Crafting App ships.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:51:45 +07:00
BFlat d3dbc6f495
M3: paint: colour profiles (Display P3), image adjustment presets, style picker (#54)
* M3: colour profiles: sRGB or Display P3 per file, convert or keep values, tagged exports

- color: ColorProfile (srgb, displayP3) with conversions derived from the primaries (shared D65
  white and sRGB curve; out-of-gamut channels clip), table-driven pixel conversion, a generated
  ICC v4 matrix/TRC profile and PNG cICP code points.
- doc: Document.color_profile (serde default sRGB, written only when set); every colour value
  (paints, text runs, effects, grids, page and prototype backgrounds, overlay backdrops, styles,
  colour variables, document colours) can be mapped; set_color_profile converts or reinterprets.
- engine: file.colorProfile {profile?, convert?} (File > Color profile...; without a profile it
  opens the dialog); document.inspect reports colorProfile.
- render: images are taken as sRGB and managed into the file's space; encode_export tags PNG
  (sRGB chunk, or iCCP + cICP for Display P3) and embeds the ICC profile in Display P3 JPG/WebP;
  SVG and PDF export Display P3 files converted to sRGB. The CLI exports the same way.
- ui: Color profile dialog (keep appearance / keep values) and a Color profile row on the page
  panel; the canvas, presenter, thumbnails, swatches and colour picker show a Display P3 file's
  colours converted to the screen's sRGB, and Copy as PNG converts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: image adjustment presets: built-in, saved in the file, reset

- color: ImageFilters::PRESETS (Vivid, Muted, Warm, Cool, Black & white, Fade, Reset), lookup by
  id or label, and sanitized values.
- doc: Document.image_presets (name + adjustments; written only when there are some).
- engine: image.adjust {ids?, target?, index?, preset?, filters?} sets the adjustments of image
  paints from a preset (built-in or saved) with values on top, one undo step; image.savePreset,
  image.deletePreset and image.presets.
- ui: the colour picker's image section gets a presets menu named after the preset in effect
  ("Custom" otherwise), saved presets with delete, "Save as preset...", a Reset button, and
  aligned adjustment sliders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: style picker: search, folders, list or swatch grid, edit, create, detach

The Fill, Stroke, Effects and Layout grid sections' Styles button opens a popover laid out like
the variable picker (crates/ui-egui/src/style_picker.rs):

- search over style names; styles grouped by the folders of their `/`-separated names, which
  collapse (searching opens them); colour styles as a list or a grid of swatches;
- click applies, a check marks the applied style; the row's edit button (or a right-click) edits
  the name, description and values: colour style paints open the colour picker on the style,
  effect and grid styles update from the selection; delete;
- `+` creates a style from the selection's current values and opens it for naming; detach;
- colour slots still list colour variables to bind;
- the applied style chip opens the picker; new icons Adjust, ViewList, ViewGrid (drawn in code);
- control channel: ui.set {stylePicker, stylePickerEdit, stylePickerView}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:38:17 +07:00
BFlat f2c4fb21e1
M3: bind line height and letter spacing to variables (#53)
The Typography section's line height and letter spacing fields now take
number variables (in pixels) like the other number fields: a variable
button on hover opens the picker (scoped to line height or letter
spacing), and a bound field shows the variable's chip to change or
detach it. While a text range is selected they keep taking plain values.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:24:19 +07:00
BFlat 0edb62ff7d
M3: variable groups in the Variables editor sidebar (#52)
Under the selected collection, the sidebar lists "All variables" and the
collection's groups (from `group/name` paths, nested groups indented)
with counts; picking one filters the table. Groups can be renamed
(every variable in them follows, one undo step) or deleted.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:14:03 +07:00
BFlat 5ca7742c6e
M7: ROADMAP refresh: weighted depth ~68%, updated estimates and gap list (#51)
Recompute the totals from the parity table after the day's merges
(feature depth ~58% to ~68%; remaining feature work 76-130 h for one
agent), refresh the gap-list prose (bidi text, version previews and
autosaves landed), milestones, test count, next steps, and regenerate
docs/parity.md (196/201).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:09:19 +07:00
BFlat b3203582e9
M1: Scale tool panel: scale by factor or size around an anchor (#49)
While the Scale tool (K) is active, the Design panel shows a Scale
section: width and height fields that scale proportionally, a scale
factor field and a 3 x 3 anchor picker for the point that stays put.
It goes through transform.scale, so strokes, effects, radii and text
scale with the layers.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:59:30 +07:00
BFlat b02ac0a115
M6: HTML + CSS code for whole frames (#50)
The Inspect tab's code menu gets "HTML + CSS": the selected layer and
everything under it as nested elements with a stylesheet. Auto layout
frames become flexboxes (fill children flex or stretch, hugging sides
drop their fixed size), other children are placed absolutely in their
parent, text becomes escaped paragraphs, and each layer gets a unique
class named after it. inspect.code takes `language: "html"`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:57:35 +07:00
BFlat 77a0e33b61
M5: scrolling inside device frames and a prototype preview pane (#48)
* M5: scrolling inside device frames and a prototype preview pane

On a device, the presented top-level frame is sized to the device's screen:
what doesn't fit is clipped, or scrolls (wheel and drag) when its overflow
allows it. Fixed layers stay on screen, pinned to the edges their
constraints name; "Scroll to" moves every scrolling frame around the layer;
navigating starts at the top unless the new "Preserve scroll position"
option keeps it.

The preview pane is a resizable window over the canvas running the same
player (present.rs now shares layout, drawing and input between the
full-window presenter and the pane). It follows edits live, keeps the
current screen while it exists, shows a newly selected top-level frame, and
takes the keys its prototype uses only while hovered. Opened from the play
menu, the Prototype tab (flows, settings, frame) and `proto.preview
{start?}` / `proto.closePreview`; the control channel gets `ui.set
{preview, presentScroll}` and the player's state in `ui.inspect`.
Alt+Cmd+Enter now presents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M5: reconcile with main: one device list, REST navigate keeps preserveScrollPosition

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:54:45 +07:00
BFlat a85df4d5e5
M6: Inspect tab: assets list with export (#47)
The Inspect tab lists the layers in and under the selection that have
export settings, with their formats and sizes. Click one to export it,
or Export all from the section header (one file through a save dialog,
several into a folder). New command inspect.assets.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:41:43 +07:00
BFlat 71c467879d
M2: auto layout nesting: one-pass nested hug/fill, coherent sizing, drag across nested frames (#45)
Layout (crates/layout):
- Fill children take their size before the frame is sized, so wrapping fill
  text, frames that hug it and grid rows settle in a single pass (three
  levels deep included).
- Fill children share space like CSS flexbox: min and max violations are
  resolved in turn instead of freezing both at once.
- A single child with space-between sits at the start; a wrapped row of only
  fill-height children keeps their height; grids size fill-width children
  before their rows.
- coherent_sizing: Fill only in an auto layout flow on an axis the parent
  doesn't hug, Hug only on auto layout frames and text (a text hugs what its
  resize mode grows); anything else becomes Fixed at its size.

Engine:
- Setting a child to Fill turns a hugging parent Fixed on that axis; a parent
  that starts to hug, layers leaving auto layout and layout.remove fix the
  rest (applied in normalize).
- Moving layers: the deepest auto layout frame under the pointer wins (plain
  frames in a flow are items, not destinations; the dragged layers and their
  descendants are skipped); the frame they come from keeps them near its
  edge. They take the slot (or grid cell) there with the insertion line drawn
  in the gap and the receiving frame outlined; out of auto layout they become
  positioned children of the canvas or plain frame at the drop point. One
  undo step per drag. Slots skip hidden and absolute children.

Tests: crates/layout/src/nested_tests.rs, crates/engine/src/al_nesting_tests.rs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:41:11 +07:00
BFlat 5c6fb08b49
M1: drag layers onto a page in the Pages list (#46)
While dragging layer rows, another page in the Pages list highlights as a
drop target; dropping moves the dragged layers (the selection when the
dragged row is in it) to that page through object.moveToPage.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:32:01 +07:00
BFlat 0a6d06f1e2
M4: arrow keys move visually through right-to-left text (#44)
On lines with right-to-left runs, Left and Right now move the caret one
position left or right on screen (TextLayout::visual_step) instead of one
char in logical order, so the caret no longer jumps backwards inside
Arabic or Hebrew. At a line's edge it continues to the neighbouring line
in reading order. Plain left-to-right lines are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:26:26 +07:00
BFlat b8a217ebba
M1: pixel preview (#43)
View ▸ Pixel preview now shows the canvas as it would export at 1x when
zoomed in: the view is rendered one pixel per page unit from a
whole-pixel origin and magnified without smoothing, so anti-aliasing and
sub-pixel edges show as they really are (and line up with the pixel
grid).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:22:28 +07:00
BFlat d0a21d2a7b
M6: REST import of prototypes and variables; export panel polish (#42)
* M6: REST import of prototypes and variables; export panel polish

REST API file JSON import now brings prototyping and variables along:
- interactions: on click/hover/press/drag, after delay, mouse enter/leave/down/up and key
  triggers; navigate, open/swap/close overlay, back, scroll to, change to, open link and set
  variable actions (lone unconditional blocks inlined); dissolve, smart animate, move, push and
  slide transitions with easing incl. custom bezier and spring; the legacy transitionNodeID
- flow starting points (and the deprecated start node), overflowDirection, scrollBehavior,
  prototypeDevice and prototype background
- the local variables response (collections, modes with the default first, values, aliases,
  scopes), passed as `variables` to file.open / file.import (object, JSON text or path), found
  next to the file (<name>.variables.json / variables.json) or embedded; boundVariables, paint
  and text style bindings and explicitVariableModes map onto the fields variable.bind uses
- file.import appends variables, flows and prototype settings with the pages
- unsupported parts (video, gamepad, conditionals, variable modes, library variables) warn

Export panel: rows with a typed or preset size (2x, 512w, 512h), suffix and all five formats
through the new export.update command; export.add steps up (1x, 2x @2x, ...); a preview;
"Export N layers" through the new file.exportLayers (one file per setting, unique safe names)
in the panel and the Export dialog; file.export takes `size`. MCP open_file/export pass the new
params.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M6: Inspect tab: properties with styles and variables, redlines on hover (#41)

The Inspect tab lists the selected layer's properties for handoff:
layout (size, position from the parent, rotation, opacity, radius), auto
layout (direction, gap, padding, alignment), typography, fills, strokes
and effects, each with the style or variable it comes from. Clicking a
value copies it. With the Inspect tab open, hovering measures distances
from the selection without holding Alt.

New command inspect.properties returns the same groups for agents.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:17:54 +07:00
BFlat d53cd60691
M6: Inspect tab: properties with styles and variables, redlines on hover (#41)
The Inspect tab lists the selected layer's properties for handoff:
layout (size, position from the parent, rotation, opacity, radius), auto
layout (direction, gap, padding, alignment), typography, fills, strokes
and effects, each with the style or variable it comes from. Clicking a
value copies it. With the Inspect tab open, hovering measures distances
from the selection without holding Alt.

New command inspect.properties returns the same groups for agents.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:07:48 +07:00
BFlat 476cdd95f1
M4: shape builder tool (Shift+M): merge or remove the regions of overlapping shapes (#39)
The selected shapes' outlines split the canvas into regions, computed as the faces of their
arrangement with linesweeper (curves kept, per-shape fill rules, holes and islands handled) and
cached per document version and selection.

- Engine: shapeBuilder.regions / merge / remove / mode commands, ToolId::ShapeBuilder and
  tool.shapeBuilder (catalog: Tools > Shape builder, Shift+M). Merge makes one vector from the
  picked regions styled like the top-most shape over the first; every shape covering a picked
  region loses that area (becomes a vector, or is deleted when nothing is left). One gesture or
  command is one undo step. Session::tool_key routes Enter (finish) and vector edit keys.
- Pathops: faces() (arrangement faces with their covering inputs) and union_faces().
- UI: region edges on the canvas, hovered/collected regions hatched (danger colour to remove),
  no transform box while building, a toolbar pill with Merge/Remove modes, a hint and Done; shared
  pill/tool-cell/divider/Done helpers; original merge/remove icons drawn in code.
- Tests: pathops faces; engine regions, merge, remove, curves, groups, rotation, pointer
  click/drag/Alt, modes, Escape/Enter, cache, junk parameters and degenerate geometry.
- Docs: ROADMAP, README, docs/mcp.md, docs/control-protocol.md, docs/parity.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:55:12 +07:00
BFlat e360b6fa52
M7: README: ArtCraft logo and app icon load in the private repo (#40)
GitHub doesn't render relative SVG images in a private repository's
README, and raw.githubusercontent.com doesn't serve private files, so the
ArtCraft logo, the footer mark and FigCraft's own row in the apps table
showed as broken images. Use the PNG versions of the same artwork (already
in docs/brand) and a repo-relative path for the app icon; both work for
private and public repos alike.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:52:47 +07:00
BFlat e0200acbae
M6: autosave checkpoints in version history (#38)
Every 30 minutes, each open document with unsaved changes since its last
version gets an automatic "Autosave" version (flagged `auto` in the file).
The newest 20 autosaves are kept; older ones make room and user versions
are never dropped for them. Runs of autosaves fold into one line in the
Version history panel. New command version.autosave.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:48:21 +07:00
BFlat 32dabdb5fa
M1: layers panel drag polish: edge scrolling, spring-open groups, ghost label (#37)
Dragging layers in the panel now scrolls the list near its top and bottom
edges, opens a collapsed frame or group after holding over it, shows the
dragged layer's name (or "N layers") by the pointer, and Escape cancels.
A layer can no longer be offered as a drop target inside itself.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:40:16 +07:00
BFlat ce317cdf42
M6: view a version on the canvas before restoring it (#36)
Clicking a version in Version history shows it in place of the document,
read-only, with a banner to restore it or go back to the current version
(clicking Current version or closing the panel goes back too). Edits,
undo and gestures are refused while a version is shown; saving, closing
and restoring end the preview first, so the document and its unsaved
state are never touched by it.

New commands version.preview and version.exitPreview; version.list marks
the shown version.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:34:06 +07:00
BFlat 360b6df206
M4: local libraries: publish, enable, place and apply, updates with overrides kept (#32)
- Publish (`library.publish {description?, include?, exclude?}`, Libraries > Publish library…):
  the file's components, component sets and styles (by default all but names starting with `.` or
  `_`, or what the last publish offered) and every component and style they use go into a snapshot
  with a manifest (library id, version, per-asset version stamps). An asset keeps the version it
  last changed in; publishing without changes is refused.
- .figcraft: `library/manifest.json`, `library/document.json` (the snapshot) and
  `library/images/<sha256>` for images only the library still uses. Older files and readers are
  unaffected (extra members); a damaged library is skipped and reported when the author's file
  opens. Files using a library read only these members (size-limited), never unpublished edits.
- Version history: the library entries are written and read alongside the versions (a library
  survives saving with versions and reopening); restoring a version keeps what the file published.
- Enable: `library.add {path}` / `library.remove {id}` / `library.list {assets?}`; the document
  keeps its enabled libraries (id, name, path, last seen version) in its data. The session caches
  each library file and reads it again when it changes on disk; in the browser these commands
  return a clear error (no file system).
- Use: `library.place {library, component, x?, y?}` / `library.import` copy the component (with
  the components and styles it uses) into a hidden library store (a page-like root outside the
  page list) as read-only main components and place an instance; instances behave like any other
  (overrides, properties, variants, swap, reset, detach). `library.applyStyle` copies a style in as
  a linked remote style. Editing the copies or library styles is refused; Go to main component
  names the library. Importing a file's pages brings its library copies along (shared with copies
  the document already has).
- Updates: `library.updates` and `library.acceptUpdates {ids?}` re-import changed copies in place
  (layer ids kept, so instance overrides survive, also when layers are added); unused copies are
  dropped when a library is removed or updated.
- Assets panel: each enabled library's components (thumbnails, search, click or drag to place,
  shared with the local tile code) and styles (click to apply, paint swatches), collapsible per
  library; "Library updates available" banner with Update; a libraries view (book button,
  Libraries menu) to publish this file (choose assets, description) and add or remove library
  files. Local styles in the panels list only this file's styles.
- Tests: manifest round trip and versions, add/place/update keeping overrides (incl. a structural
  update), variants/swap/undo, library styles and importing pages, missing/corrupt/truncated/
  oversized/never-published files; format tests for damaged and oversized members.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:24:46 +07:00
BFlat a5d5c265fd
M7: performance: retained canvas renders, virtual layers panel, shared text layouts, cargo xtask bench (#35)
Large documents stay interactive: the canvas re-renders only what a pan uncovers or an edit
touches, the layers panel lays out only the rows in view, and per-frame work no longer walks the
whole page.

- Benchmark: `cargo xtask bench [SIZES]` builds synthetic pages (`figcraft_engine::synthetic`:
  screens with an auto layout header, a card group, button instances and shapes/text) at
  1k/10k/50k layers and prints engine/render timings (`figcraft-cli perf --layers 1000,10000`) and
  headless whole-UI frame timings (an ignored ui-egui test).
- Retained canvas rendering (`figcraft_render::Retained`, used by the render worker): views snap
  to whole device pixels, so a pan shifts the previous image in place and renders only the
  uncovered strips; an edit renders only where changed layers were and are
  (`Document::changed_ids` diffs the two versions skipping shared structure; ancestors that paint
  from their children's pixels - booleans, effects, masks - count whole; background blur reach
  and noise are allowed for). Zoom, page, options, font or page-level changes, or large changes,
  render the whole view as before. `ui.inspect` reports `canvas.renderWork`.
- The texture image is converted on the render thread, not the UI thread.
- Layers panel: only visible rows are laid out (stable ids while scrolling); rows are cached per
  document version, page, expanded set and filter.
- Frame labels and layout grids of off-screen top-level frames are skipped; the Prototype tab's
  connection sources are cached per document version.
- Text: the layout cache keys by a cheap fingerprint confirmed by `TextData` equality (no Debug
  formatting per lookup, 32k entries) and hands out shared layouts; a layout generation marks
  renders stale when fonts arrive. Overflowing text is culled by its glyphs, so it no longer
  vanishes while its box is off screen.

Before -> after (release, Windows, 1440x900 view, ms; 10k / 50k layers):
- move one layer, re-render @100%:     7.65 / 6.49  -> 0.18 / 0.81
- move one layer, re-render zoomed out: 11.95 / 24.10 -> 0.16 / 0.77
- pan 40 px @100%:                     7.53 / 6.82  -> 0.69 / 0.81
- pan 40 px zoomed out:                12.07 / 24.30 -> 1.62 / 4.17
- full render @100% / zoomed to fit:   7.78 / 44.22 -> 6.56 / 38.83 (10k @100%, 50k fit)
- UI frame, every screen expanded in layers: 6.16 / 39.07 -> 0.24 / 0.57
- UI frame, scrolling that panel:      6.11 / 40.18 -> 0.43 / 0.40
- UI frame, layers filtered by name:   1.03 / 6.92  -> 0.17 / 0.36
- UI frame, Prototype tab:             0.89 / 6.45  -> 0.18 / 0.80
- UI frame, idle:                      0.31 / 1.43  -> 0.11 / 0.39
Unchanged and within budget: single-layer edits through the engine ~0.01 ms, normalize after a
one-node edit < 0.01 ms, hit test 26 / 93 us. Left: save/load at 50k (~0.7 s each), zoomed-out
renders at 50k (~40 ms).

Tests: retained renders match full renders after pans, moves, recolours, hide/remove/add, undo,
masks, overflowing text, edits under a background blur and an inner-shadow/noise layer, a drop-
shadowed group, and a blur panned in from the view's edge; zoom/page/options/page background
render full; junk views don't panic. Layer-row and prototype-source caches follow edits, undo,
page switches, expansion and the filter. `changed_ids`, the text cache, and the perf CLI.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:19:05 +07:00
BFlat 632c5ffc16
M4: right-to-left and mixed-direction text (Unicode bidi) (#34)
Text runs get bidi embedding levels from the Unicode bidi algorithm
(unicode-bidi, MIT/Apache-2.0). Right-to-left runs are shaped right to
left, kept in logical order for line breaking and placed in visual order
per line (rule L2, trailing spaces at the paragraph level). Glyphs and
lines know their direction, so the caret, clicks and selection
highlights follow right-to-left runs. Text without right-to-left
characters skips the analysis.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:12:14 +07:00
BFlat 1c6f04bf62
M3: Variables editor with aliases, scopes, and rename/delete for variables, modes and collections (#33)
The Local variables window becomes a table editor: collections in a
sidebar, variables grouped by name with one value column per mode, typed
value cells (colour swatch + hex + alpha, number, string, boolean), alias
chips with a variable picker and detach, double-click rename, context
menus, and an Edit variable window for description and scopes.

New commands: variable.update (name, description, scopes),
variable.duplicate, variable.renameMode, variable.removeMode,
variable.renameCollection, variable.deleteCollection. variable.set
validates aliases (same type, no loops) and stores them by id; addMode
always picks a fresh mode id. Pickers offer only variables scoped to the
property. ui.set {variables} opens the editor on a collection.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:12:08 +07:00
BFlat cb8151c999
M3: bind variables to opacity, padding, gaps, sizes, font size, text and visibility (#31)
Number fields for opacity (variables hold percentages), individual corner
radii, auto layout padding and gaps, min/max sizes and font size now take
variables like width and corner radius already did. Text content binds to
string variables and visibility to boolean ones from the Typography and
Appearance section headers; bound ones show as a variable chip that
changes or detaches the binding.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:57:36 +07:00
BFlat b572363ac8
M1: rotated handle box for layers that share a rotation (#30)
Selecting several layers with the same rotation now shows the handle box
turned with them (the size label reads the box's own width and height),
and resizing scales them along the box's axes instead of the page's.
Different or right-angle rotations keep the page-aligned box.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:57:31 +07:00
BFlat 3767b74312
M5: prototyping: edit interactions on several layers at once (#29)
- With several layers selected and the same interactions, the Prototype panel edits them all
  (add, remove, trigger, action, animation).
- With different interactions it shows "Mixed": + replaces them all with one new click
  interaction (one undo step), Remove all clears them.
- New command `proto.interaction.clear {ids?}`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:32:37 +07:00
BFlat 48d5964941
M2: auto layout drag: wrap rows and grid cells (#27)
- Dragging a child of a wrapping row layout inserts by visual row and position (rows are found
  from the flow, so the insertion line spans the right row), instead of by x alone.
- Dragging a child of a grid layout places it in the cell under the pointer (nearest cell over
  gaps), with a dashed cell highlight while dragging; one undo step.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:32:31 +07:00
BFlat b8687d7c0d
M4: component descriptions in the Design panel (#28)
- Components and component sets get a Description field (multi-line, saved when it loses
  focus; undo refreshes it). Instances show their main component's description (or the set's)
  under the swap menu.
- The frame status/resize block no longer shows for instances, where it doesn't apply.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:30:48 +07:00
BFlat 5a66859ec6
M6: paste images from other apps; the newest clipboard content wins (#26)
- Cmd/Ctrl+V pastes an image from the system clipboard (screenshots, images copied in other
  apps) as an image layer centred in the view; an image-only clipboard (no paste event) is
  handled from the key.
- FigCraft remembers what it last put on the system clipboard (SVG of the copied layers, or a
  marker): when the clipboard still holds it, paste uses our copied layers; anything newer from
  another app (image, SVG, text) is pasted instead. Cut now also mirrors to the system clipboard.
- Desktop services read clipboard images (size-capped); the browser falls back to text.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:30:32 +07:00
BFlat bf3a45d938
M1: sections and frames: resize to fit, section colours, ready-for-dev status (#23)
- Object ▸ Resize to fit (Alt+Shift+Cmd+R, `object.resizeToFit`): frames, sections and
  components shrink or grow to their visible contents without moving them on the page; auto
  layout frames switch to hug instead.
- Sections get colour swatches (light tints) in the Design panel.
- Frames and sections get a handoff status (`devStatus`: ready for dev / completed) set from
  the panel and shown as a badge next to their name on the canvas; `node.get` reports it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:20:23 +07:00
BFlat aa0e8b4a7c
M4: vector eraser in edit mode (#25)
- Eraser tool (Shift+E) in the vector edit toolbar: drag a brush over paths to remove the parts
  it covers; segments are cut exactly where the brush enters and leaves (curves stay curves),
  fully covered segments and orphaned points go, and closed shapes open while keeping their
  implied fill. A stroke is one undo step; the brush shows under the pointer.
- `vector.edit.erase {points, radius?}` for the CLI, control channel and MCP.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:16:35 +07:00
BFlat 72b8c5d154
M6: version history: versions saved in the .figcraft file, version commands and panel (#21)
- Versions now belong to the document (DocState.versions) instead of a process-global map keyed by
  document uid, so they survive closing the file. A new, renamed or deleted version marks the file
  as changed.
- .figcraft: `versions/index.json` (id, name, description, time, image hashes) plus one deflated
  `versions/<id>.json` snapshot per version; images only versions use are stored once and kept with
  them. Snapshots read from a file are parsed on first use (and written back as-is). Readers that
  don't know versions ignore the entries; files without versions load unchanged. Damaged, missing,
  oversized or duplicate entries are skipped and reported, never failing the load. At most 100
  versions per file; names and descriptions are clipped.
- Commands: `file.saveVersion {name?, description?}` returns the version; `version.list`;
  `version.restore {id | index}` (one undo step, keeps the file name, stays on the current page);
  `version.rename {id | index, name, description?}`; `version.delete`; `version.duplicate` (opens
  the snapshot in a new tab). Indexes are validated: a negative index used to restore version 0.
  `req_index` param helper shared with the guide commands.
- Version history panel in the left sidebar in place of Pages and Layers (File > Show version
  history; Save to version history opens it with the form): save form (title, description),
  "Current version" on top and the versions newest first on a timeline with name, relative time and
  description; per-version menu (also right-click): Restore, Rename (title and description inline),
  Duplicate, Delete (confirmed inline). New shared `widgets::button`.
- `figcraft-cli run --out x.figcraft` keeps the version history.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:16:18 +07:00
BFlat 15ccc379ab
M1: fix inner shadow bands: shadow from the shape, not the layer's crop; banded rendering matches one piece (#24)
The canvas renders large views in parallel horizontal bands, and each band crops a layer to its
own rows. Three effects went wrong at those crops:

- Inner shadow (the reported bug): the padded shadow buffer treated everything beyond the cropped
  layer as outside the shape, so every band edge cast shadow inwards, drawing dark bands across
  the layer (any shape and fill; also at the view's edge when a layer was partly off screen). The
  shape itself is now rasterised over the padded buffer at the shadow's offset (sub-pixel
  accurate), so only its real edges cast shadow.
- Layers starting above or left of the pixmap (inside the blur margin) were sized from the
  clamped origin and came out short by the margin: a drop-shadowed or background-blurred layer
  crossing a band edge lost a strip at its far side.
- Background blur read a backdrop that stopped at the band's edge (streaks at band edges). Bands
  now overlap by what background blurs read (3 sigma + box rounding) and keep only their own
  rows; a blur wider than a band renders the view in one piece.

Regression tests (crates/render/src/band_tests.rs): an image-filled ellipse and a rotated solid
rectangle with inner shadows (spread, offsets) under a background-blurred panel, at 1x and 2x,
rendered in 2, 3 and 7 bands, must match the one-piece render row by row; the ellipse's interior
far from its edge must equal the render without the shadow on every row; a huge backdrop blur
falls back to one piece.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:09:04 +07:00
BFlat ff0f5a40d7
M4: expose nested instances' properties on instances (#22)
- A nested instance inside a main component can be exposed (`component.exposeInstance`, an
  "Expose properties to instances" toggle in its panel); the main's instances then list it by
  name with its variant and component property controls, recursively for exposed instances
  inside it. Values set there are nested overrides and survive main edits.
- `instance.exposed {id}` lists an instance's exposed nested copies (for the CLI and MCP).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:08:48 +07:00
BFlat c54509a574
Roadmap: recompute totals after day 2 (feature depth ~58%, time to parity, next steps) (#20)
- Feature depth from the weighted table (~59%, graded down to ~58%), parity incl. fidelity,
  menu breadth 97%, remaining hours from the table's rows, next/then priorities, command and
  test counts; parity report regenerated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:00:27 +07:00
BFlat 8838515cf7
M5: prototyping: overlays animate out when closed (#19)
- Closing an overlay (Close overlay, Back, a click outside) plays its opening transition
  backwards instead of removing it at once; one closed mid-opening reverses from where it got
  to; instant overlays still go immediately.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:52:28 +07:00
BFlat b5db089db6
M4: text range fills in the Fill section (#18)
- While a range of text is selected in a text being edited, the Fill section shows that range's
  colour ("Mixed" when it varies): the swatch opens the colour picker on the range, the hex field
  sets it, and a button returns the range to the layer's fill.
- Colour picker: a text-range target that reads the range's fill and writes `text.style
  {textFills}`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:52:11 +07:00
BFlat 840d3ac5d2
M1: mask types: alpha, vector outline and luminance masks (#17)
- Layers gain `maskType` (alpha by default, so files are unchanged): a vector mask clips by the
  mask layer's outline whatever its fills and opacity; a luminance mask shows content where the
  mask is bright and hides it where it is dark.
- `node.set {maskType}` (validated), `node.get` reports it, and the Appearance section shows a
  Mask dropdown when the selection is masks.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:45:45 +07:00
BFlat 076c25f6df
M6: Inspect code in CSS, SwiftUI and Jetpack Compose (#15)
- Code generation moves from the UI into the engine (`codegen.rs`): CSS for the layer as before,
  plus SwiftUI and Compose for its subtree (stacks with spacing and padding for auto layout,
  offsets inside a ZStack/Box otherwise, text with font and colour, shapes, fills, strokes,
  shadows, opacity; other layers become image-asset placeholders). String literals are escaped
  per language; deep trees are capped.
- New command `inspect.code {id?, language?}` for the CLI, control channel and MCP.
- Inspect tab: a language picker and Copy for the shown code; Copy as CSS uses the same code.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:45:29 +07:00