* perf(vectordrawable): resume each repeated name's suffix search where it stopped
* address review: continue from a longer stem when the name suffix counter is exhausted
* perf(collab): memoise layer depths in diff and skip the sibling scan for a new child
* address review: bound the depth walk by the document's node count
* fix(engine): hold a plugin's text.preview to the permissions of the command it previews
* address review: check PREVIEWABLE before recursing into a preview's inner command
* fix(doc): bound the string bytes an expression holds and stop concatenating at the cap
* address review: count only appended characters; keep validating operands past the cap
Every shape painted with a raster pattern hashed the pattern's whole
encoded image again to find its document key, so one embedded image reused
by many shapes multiplied the hashing work without adding any image data.
The key is kept per buffer for the import.
Every missing $ref rendered the batch's whole name table into its error,
and with onError: continue each such error was kept, so a compact batch
cost its name count per failed step. The hint names twelve steps and
counts the rest.
* fix(render): check the nesting limit for mask content and bound GIF frame buffers
A mask's content is drawn straight from mask_pixels, not through
draw_layer, so masks nested inside masks recursed past the 256-level limit
and down the stack. GIF decoding budgeted frames by the logical canvas but
left the decoder's allocation limits off, so one frame descriptor could ask
for a gigabyte before the frame came back to be checked.
* address review: give the GIF decoder one frame of working room beyond the animation cap
Each argument was substituted into the already rendered text, so a value
holding placeholders was expanded again by every later replacement and a
short request could grow the prompt without bound. The template is now
scanned once and argument text is never read as syntax.
One gzip layer is unpacked within the import budget, but usvg gunzips what
it is handed again with no limit, so a doubly compressed document or
embedded image expanded outside the budget. A compressed embedded image
that failed its budget also left the shared allowance untouched, so every
sibling image could repeat the work; a refusal now spends what was left.
Each iteration froze the children whose clamping passed a 1e-9 tolerance,
but it compared the aggregate violation against the same tolerance: several
clampings each under it could add up past it, so no child froze, the
pending set never shrank and the loop never ended. An iteration that
freezes nothing now settles the remaining children as clamped.
Loading accepted a page id repeated in the pages list and repair kept the
repeats, so import, Fig and Penpot export and every page walk visited and
retained that page's subtree once per listing. validate now reports a
repeated page and repair drops the repeats.
The topic list was validated but not deduplicated, and it is scanned for
every retained event while the log's lock is held, so a request repeating
one topic thousands of times multiplied that work. Repeats collapse and a
list naming all is just all.
Review: a section moving carries its stickies and their connectors, so those connectors and
their labels are no snap targets for the section either (an end counts as carried when its node
or any ancestor is moving). The parity rows, the END_GAP doc and the test's doc no longer say
connectors from one side run together: elbows share their first run along the normal, curves
leave tangent to it, straight ones fan out from the same point.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A connector end attached to a side was routed from the middle of that side, so the stroke's
centre line started on the border and half its width plus the round cap lay on the fill, and
several connectors leaving one side piled up into a knot sticking into the card. FigJam starts
the line a few pixels outside the border and connectors leaving one side run together along the
side's normal before they fan out (the issue's screenshots). `board_route::END_GAP` (4 pt): an end
attached to a side (`RouteEnd::Box`) starts that far outside it along the side's normal; an end
placed by hand on the object (`RouteEnd::On`) keeps its exact point; a free end is unchanged.
The pinned route coordinates in the engine, format and UI tests move with it; the overlay's
attach points stay on the border. Docs: the Connectors rows, status line and history of
boards-parity.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: letting a handle go over the selection bar made the next shape behind it. The bar
hides while a "+" is dragged, as it does for any drag (board_connect::dragging), so there
is nothing to drop behind; the drop makes the next one where the bar was, and the bar is
back once typing in it ends. Test the_bar_stays_away_while_a_handle_is_dragged; the docs
rows say the toolbar and the bar.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`file.import` of a FigCraft file kept the document's style, variable or
collection for every id both files used, and the pasted layers kept
those ids. Ids are numbered per file, so two files made apart share
them, and the imported layers were bound to the document's unrelated
variables, modes and styles.
`append_pages` now gives the incoming file new ids first (`Renames`):
- an incoming style, variable or collection is shared, as before, only
when the document has the same under its id and everything it names
is shared too (the same file imported again); a collection and its
variables are shared only together; library copies keep their ids;
- every other id the document already uses gets a new one from its
allocators, which now take ids to avoid (the incoming file's own);
- layers' style links, bindings, explicit modes and prototype actions,
styles' bindings, variables' collections, aliases, calculations and
extensions, and the renamed collections' mode ids follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: a drag whose object is no longer the selected one is dropped from egui's data at once
(it lingered when the selection moved to another sticky or shape and could still act once the
first was selected again); a handle let go over the floating toolbar, which lies inside the canvas
rectangle, makes nothing, like one let go over a panel.
Tests: a_drag_ends_when_the_selection_moves_to_another_shape, letting_a_handle_go_over_the_toolbar_makes_nothing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: the drop target is the topmost sticky or shape under the pointer, sections and frames
looked into (board::sticky_or_shape_at, instead of the stamp's any-layer hit test on the page's
children); the drop point is placed in the parent's coordinates; the connector leaves by the
handle's side (as the preview draws it) and arrives by the facing side; a handle let go off the
canvas makes nothing; the parity rows on quick-create handles agree.
Tests: dropping_a_handle_on_a_shape_inside_a_frame_joins_it, letting_a_handle_go_over_a_panel_makes_nothing,
the_next_one_goes_where_it_is_dropped (frame case).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The quick-create handles only took a click. As the help says ("drag the preview to any location
on your board"), a handle can now be dragged: the preview follows the pointer and, let go on empty
board, the next sticky or shape is made there, centred on the pointer, joined by a connector whose
ends take the sides facing each other (board.addConnected gains at: [x, y]); let go on another
sticky or shape, a connector joins the two instead (board.connector), that object outlined while
over it. figcraft_engine::board::object_at exposes the hit test a stamp uses. The click is as
before.
Tests: board_connect_tests (two), board_connected_tests::the_next_one_goes_where_it_is_dropped.
Docs: boards-parity row, status line and revision history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
webbrowser on WSL hands an address to cmd.exe /c start, escaping only &
and ^, so a document link holding | (or < > " `) ran what followed it
with the viewer's authority; and an address its parser can't read is
reopened as a file path. Characters never valid in a URL unencoded are now
refused, and an http(s) authority must be made of host characters.
A read-only plugin could archive, merge, create or update a branch: branch
state lives beside the document, so neither the command's history flag nor
the document Arc check saw the write. Any plugin could read a saved version
through dev.compare's version parameter although version.* is denied, and
library.list handed plugins each library's path, which for a server library
is the share link with its token.
The confirmation showed the directory entry's access and commands, but the
downloaded bundle's manifest was registered as is, so a directory could
show a read-only plugin and install one that edits the document. A manifest
asking for more than the entry showed is refused before it is saved.
kurbo turns an arc into cubics by radius divided by tolerance, so the fixed
0.01 let an ellipse, polygon corner, rounded rectangle, per-side stroke
corner or SVG arc with a radius like 1e300 ask for more cubics than memory
holds before any size check saw the geometry. Arcs now use a tolerance
that scales with the radius, and SVG path data from documents, commands and
imports is refused before parsing when a number passes 1e9.
The assets of a plugin folder were read through whatever links they
pointed at, so a folder handed to the publisher could put any file the
publisher can read into the bundle the directory then serves to everyone.
A linked asset, or a linked assets folder, is refused by name before
anything is sent.
Review: the widest name is found by laying rows out in descending order of a bound on their width
until the bound falls under the widest found, with no cut at 48 rows; each name is laid out as it
is painted (one line, cut at NAME_ROOM); the painted room is capped at NAME_ROOM too; and a drag
over a wide row counts only inside the list's clip rect, so a layer released over the canvas drops
nowhere.
Tests: a_wide_name_among_many_long_narrow_ones_is_still_measured,
a_name_with_line_breaks_is_measured_as_painted, a_drag_released_over_the_canvas_drops_nowhere.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add a 2,647-row zh-Hant catalog (1,975 UI strings, 641 command labels, 31 menu wordings), locale mapping for zh-TW/zh-HK/zh-MO/zh-Hant, and a Noto Sans CJK TC-first fallback. The catalog is marked draft pending native review; the web picker stays hidden because craft-fonts currently ships only the Simplified CJK face.