Queued sync messages wait in a list the session loop drains, so Leave, a lost
connection, presence and pings are never held up behind them. A text frame from
the host stays final. A session that ended stays on its tab after a Save as:
ending it there could turn a tab into a plain local file with no word about it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Joining again after a session couldn't go on opened your offline changes in a
copy beside the file. They now go on top as one undoable edit, through the
three-way merge branches use. The rejoin asks to resume as the same client, so
layers made offline are yours to send. What can't go in (the same thing changed
both ways, or layers a new client can't make) leaves the server's version, and
your whole version opens beside it so nothing is lost. A server that no longer
knows you merges the same way instead of discarding unconfirmed work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session you joined could end for reasons that pass, or after about 85 s of
retries, and its tab then looked like an unsaved local file. Work done there
reached no one, with only a 3 s toast to say so.
A live connection now reconnects until you leave, pausing a minute after each
12 attempts. Only refusals that are for good stop it, so "too many messages",
"busy" and the like retry. Messages queued while offline go out paced so a long
queue can't trip the server's rate limit. A session that ended stays on its tab:
the tab shows a red mark and a strip across the canvas offers Retry now or Try
again, Save a copy and Dismiss. Joining again uses the same tab, and what changed
offline opens beside it as an unsaved copy. A session that gets out of step
joins again by itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A loop whose segments are the chain between the two ends but are listed out of walking order has
no place for the new segment that makes it continuous; inserting it at the first gap would leave
the other gaps. Such a loop is left as it was (the walkers' bridging still draws it), as a loop
broken in two places is. Test: the chain A-B-C-D-E with its loop listed out of order, joined A to E.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cause: `ops::connect` rebuilt the broken loop along the path from the second end to the first, so
connecting the ends in the other order reversed the loop's walk; a hole wound against its outline
then wound with it and the non-zero rule filled it.
Fix: the new segment goes into the loop at its gap, in the loop's own order. Test: a ring whose
hole loses a corner with Delete and is connected again, either end first (red before for one
order).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cause: `visual_gaps` mapped a gap at a run boundary to the gap before the later run's first item,
which for a run nested inside another (a Chinese word in a Hebrew phrase in Chinese text, made
with embedding marks) is a gap inside the embedding: one boundary took it, another got none, and
the line came up a gap short of the box's edge.
Fix: each gap is reordered with the line as a glyph of no width at the lower of its two
neighbours' levels (rule N1 for a space between them), through the same `visual_order`: items and
gaps keep alternating and each gap lands once at the boundary it belongs to. Test row with explicit
embedding marks (red before: "char 8 moved 2 (want 1)").
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cause: deleting a point with Delete (not Shift+Delete) opens the path and leaves its region's
loop with a gap, filled closed by a straight line across it; joining the two ends again (the pen
onto the other end, Connect, Join) added the segment to the network but not to the loop, which
still spanned the gap. Unrounded, the straight line and the segment coincide; once the points are
rounded, the fill and the selection outline run straight across while the stroke, walked from the
network itself, follows the segment and its arcs (#1506's picture).
Fix: `ops::connect` splices the new segment into the loop whose segments are the chain between
the two ends, so the loop closes through it; a chain no loop fills still becomes a region of its
own. Tests: the chamfer made by Shift+Delete, by Delete then Connect, and drawn with the pen, each
rounded, with the fill tracing the stroke (engine); every loop order and segment direction of a
chamfered rectangle (doc).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cause: justified lines only widened their spaces, so a line of ideographs, written without any,
kept its natural width and its right edge never reached the box's.
Fix: the room on a line (but a paragraph's last) also goes into the gaps next to an ideograph,
kana, Hangul or full-width form wherever a line could break there (`breaks::justify_between`,
beside `break_between`): each is a word of its own, as for line breaking, and no gap opens before a
closing mark or after an opening one. Spaces widen as before; mixed text widens both.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: the first and last runs were checked for lying at right angles to their sides, not for
going out of the object, so a path whose first run went up through the start's box (or whose last
arrived from inside the end's) was kept and drawn through it. Both runs now have to go the way the
side faces; one along the side or back into the object leaves the connector to the router. Two more
cases in the routed-here test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `.jam` importer routed every elbowed connector itself, so one drawn around a layer by hand in
FigJam came in routed through it. FigJam saves the path as `connectorControlPoints`: one per run
between the first and the last, each a point on its run (in the connector's parent's coordinates)
with the run's axis; the first run leaves the start's side and the last arrives at the end's. They
become the `ConnectorPath::Runs` a board connector reshaped by hand has, walked by
`figcraft_doc::route_along`, so the path is kept exactly. Points that don't make such a path (off
both axes, two runs the same way in a row, a first or last run sideways to its side, more runs than
a hand could make) leave the connector to the router as before, and only connectors routed here
count in the "own routing" note. `figcraft_doc::attach_points` and `MAX_RUNS` are exported for it.
Docs: file-format-parity.md, boards-parity.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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.
An end whose magnet is AUTO has no fixed point: FigJam picks the side when it draws, and the file
keeps whatever relativePosition the end last had, so the importer sent every automatic end out of
the stale side (the left one in the reporter's file). AUTO and CENTER ends now attach to the side
facing the other end, as a board connector drawn here does, for straight, elbowed and curved
connectors alike; named magnets and ends without one are unchanged.
Test: connectors_with_an_automatic_side_leave_the_side_facing_the_target (the fixture schema gains
the ConnectorMagnet enum). Docs: file-format-parity .jam row, status line and revision history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: Noto Sans Hebrew, Devanagari and Georgian join Arabic and Thai in FALLBACK_FAMILIES, so a
web build that embeds one draws that script in an Inter document; with Arab embedded the picker
offers the Arabic UI catalog, as cjk::can_show intends (the docs said otherwise); the docs
example lists one tag per script.
Test: every_embeddable_script_face_is_a_document_fallback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Layers panel cut a name that didn't fit with an ellipsis and had no horizontal scrollbar, so
an imported component set's variants all read "Size=small, Type=prim..." and a deeply nested name
lost its room to the indent. The row cache now also keeps the width the widest row needs (a few
names measured per rebuild, picked by a bound on their width), the list takes that width and
scrolls both ways, revealing a layer scrolls up or down only, and a name past 1200 pt still
elides (#1650).
Tests: layers_wide_names_tests. Docs: ui-parity Layers panel row, status line and revision
history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Layers panel drew component, variant and instance names and icons in the canvas outline
purple whatever the theme (3.1:1 on the dark panel, 2.7:1 on the selected row), and the rows
inside a selected component in the plain selection's green. They take the theme's component
token (the dark theme's lilac; the light theme's a shade darker than the canvas purple, so it
reads at AA on a selected and a hovered row too), and a new component_row_child token paints the
rows inside a selected component in a tone of its row. The canvas keeps its purple.
Test: layers_component_rows_tests (both themes). Docs: ui-parity themes row, status line and
revision history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The browser has no system fonts and the text crate's build script keeps the web catalog to Inter,
Noto Sans Georgian, the wordmark and (with FIGCRAFT_EMBED_CJK) Noto Sans CJK, so craft-fonts'
Noto Sans Arabic never reached a web build and Persian or Arabic text drew as missing-glyph
boxes. FIGCRAFT_EMBED_SCRIPTS names script tags as the manifest lists them (Arab, Hebr, ...);
a web build keeps the faces of those scripts as well. The default build is unchanged.
Test: web_embeds_other_scripts_faces_only_when_asked (build script). Docs: localization-parity
(web fonts paragraph, Arabic row, status line, revision history), translating.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
In the baseline branch of the flow layout only an item with a text baseline got one; any other
layer stayed at the top of the line. A layer without a text baseline now uses its bottom edge, as
the help center's figure shows ("the bottoms of the icon and the word home are aligned on the red
line"): an icon next to a text sits on the text's baseline, a layer reaching below it moves the
text down, and a line with no text lines the layers' bottoms up (CSS does the same).
Tests: alignment_rules (three cases), baseline_alignment_horizontal, engine
baseline_alignment_puts_an_icons_bottom_on_the_text_baseline. Docs: auto-layout-parity row,
status line and revision history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The merge with main kept both versions of the row; they are combined, the Bend click sentence and the region fill sentences together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both redirect the Fill section's reads and writes: the selected regions of a vector come first (#1878), else a text's characters' shared fills (#1857), in the Fill section, the colour picker, the canvas handles, fill.copy, fill.set and fill.move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pick select changes the same ui_key and key_event paths as the crosshair and the pan; stacked on them as the maintainers asked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both change number_field_impl's click and drag handling; stacked so the combination is reviewed and tested together, as the maintainers asked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handles read the paint list the Fill section and the colour picker show (character_fills), so with the characters' image already in Crop mode the crop handles show and edit it, not the layer's own fill at that index; writes still go through fill.set, which moves the characters' list to the layer first. No undo step on entering crop mode. Test canvas::handles::character_fill_tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: wrap style on a text layer: Auto, Balance and Pretty decide where its lines break (#1827)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Docs: no table pipe inside the wrap-style row's code span
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Wrap styles: no new emergency breaks, hanging punctuation in Pretty's width, wrap style as an override
Balance and Pretty reject a re-break with more mid-word breaks than the greedy lines; Pretty narrows a line by its text less its hanging marks; wrapStyle joins the textStyle override group; docs wording. Tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M8: a click with the Bend tool curves a sharp point, its handles along the line between its neighbours (#1875)
A release without movement on a sharp point between two segments runs ops::auto_smooth instead of cancelling; a curved point still sharpens, a drag still sets the handles by hand, an endpoint is left alone. Tests; docs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Vector edit hit test: the nearer of a handle's end and a point wins
A short path's handles end within the point's hit tolerance; a click on the point is on the point, one on the handle's end still takes the handle. Test short_handles_leave_their_point_clickable.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Vector edit hit test: the nearer of a handle's end and a point is judged on the page
Candidates stay within the tolerance as before; ranking by page distance keeps a non-uniform scale from favouring the wrong one. Test a_handle_and_a_point_are_compared_on_the_page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Vector edit hit test: a handle wins a tie with a point, as before
A handle snapped onto another point stays selectable there. Test a_handle_on_another_point_is_still_the_handles (fails with the strict comparison).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
fill.pattern sets the layer's fill as its spec says. The gradient and crop handles read the paint from the stack fill.set writes (the selected regions' while there are any); for a region's paint, Upload from computer and image presets write the paint like any edit, and a drop on the picker goes to the canvas (image.replace and image.adjust edit layer paints). Tests in regions_tests, canvas/handles/region_tests and colorpicker image tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Named regions are deduplicated; an inheriting region stays inheriting when an edit leaves its stack as it was, and its own stack is only replaced once the edit succeeds; fill.copy reads the selected regions' paint; the Fill section leaves the fill style, a variable bound to the layer's fill and the image crop off region rows. Tests in regions_tests and region_fill_rows_tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tab or the chord again switches the box to a pick cursor; Tab/Shift+Tab move between sibling layers, Enter/Shift+Enter into and out of a layer, Space selects, Escape or a canvas press ends it; ui.key takes the same keys. Gesture and parity rows, catalog, test, docs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fill.set/add/remove/move/paste on the edited layer go to the selected regions' own fill stacks (or those named by regions), the layer's fills untouched; the Fill section and the colour picker list them. Test regions_tests::the_fill_commands_edit_the_selected_regions; docs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>