M9: .jam shapes keep their fixed size and a blank record's outer text (#1808)

A native shape frame now writes fixed sizing on both axes, as a shape made here has, so the first edit no longer shrinks it to hug its text. A shape whose generated text is blank while the outer layer still carries text keeps the generated layers (#1397). The kind name in the no-outline note is clipped, the docs rows match the code, and the new key sits in order.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
Trevor Brown
2026-11-12 03:30:43 -05:00
co-authored by Claude Sonnet 5.5
parent 100a6b7e6f
commit 9e22cc7bc3
6 changed files with 33 additions and 7 deletions
@@ -566,6 +566,11 @@ fn a_jam_diamond_is_a_native_shape_drawn_by_figcraft() {
assert!(frame.fills.is_empty());
assert_eq!((frame.width, frame.height), (240.0, 240.0));
assert_eq!(frame.layout.as_ref().expect("shape auto layout").padding, [0.25 * 240.0; 4]);
assert_eq!(
(frame.child_layout.sizing_h, frame.child_layout.sizing_v),
(figcraft_doc::Sizing::Fixed, figcraft_doc::Sizing::Fixed),
"a fixed box, as a shape made here: it must not hug its text"
);
assert_eq!(frame.children.len(), 2, "the outline and the text: {warnings:?}");
let outline = doc.node(frame.children[0]).unwrap();
let NodeKind::Vector { network } = &outline.kind else { panic!("{:?}", outline.kind) };
@@ -686,3 +691,17 @@ fn a_jam_shape_without_text_is_still_a_native_shape() {
assert_eq!(frame.board, Some(figcraft_doc::BoardItem::Shape { shape: figcraft_doc::BoardShape::Rectangle }));
assert_eq!(doc.node(frame.children[1]).unwrap().kind.text().unwrap().characters, "");
}
/// Blank generated text must not hide text the outer node still carries (#1397): on a whiteboard
/// too, such a shape keeps the generated layers.
#[test]
fn a_jam_shape_whose_text_only_the_outer_node_carries_keeps_it() {
let mut layer = shape_node("SQUARE");
set(&mut layer, "textData", o(vec![("characters", s("Outer saved text"))]));
let background = record(0, vec![("fillPaints", arr(vec![solid(1.0, 1.0, 1.0)]))]);
set(&mut layer, "nodeGenerationData", o(vec![("overrides", arr(vec![background, record(1, text(" ", 24.0))]))]));
let (doc, _) = jam(layer);
let frame = named(&doc, "Board object");
assert!(frame.board.is_none(), "the generated layers keep the outer text");
assert!(frame.children.iter().filter_map(|id| doc.node(*id)?.kind.text()).any(|t| t.characters == "Outer saved text"));
}
+8 -1
View File
@@ -110,7 +110,7 @@ impl<'a> Mapper<'a> {
pub(super) fn native_shape(&mut self, o: &mut Obj, v: &View<'a>, rest_id: &str, depth: usize) -> Option<Vec<Value>> {
let kind = v.str("shapeWithTextType").unwrap_or("SQUARE");
let Some(shape) = board_shape(kind) else {
self.warn(format!("shape with text kind {kind} has no FigCraft outline; kept as a frame"));
self.warn(format!("shape with text kind {} has no FigCraft outline; kept as a frame", crate::rest::props::clip_str(kind)));
return None;
};
if v.get("nodeGenerationData").is_none() || v.get("derivedImmutableFrameData").and_then(|d| num(d, "version")) != Some(1.0) {
@@ -123,6 +123,11 @@ impl<'a> Mapper<'a> {
};
let background = record(0)?;
let body = record(1)?;
// Blank generated text must not hide text the outer node still carries (#1397): the
// generated layers keep it.
if own_text(&body).is_none() && own_text(v).is_some() {
return None;
}
if depth >= MAX_DEPTH || self.count.saturating_add(2) > self.budget() {
return None;
}
@@ -162,6 +167,8 @@ impl<'a> Mapper<'a> {
("layoutMode", json!("VERTICAL")),
("primaryAxisAlignItems", json!("CENTER")),
("counterAxisAlignItems", json!("CENTER")),
("primaryAxisSizingMode", json!("FIXED")),
("counterAxisSizingMode", json!("FIXED")),
("paddingTop", json!(top * h)),
("paddingRight", json!(right * w)),
("paddingBottom", json!(bottom * h)),
+1 -1
View File
@@ -174,7 +174,7 @@ impl<'a> Mapper<'a> {
// A label without a snapshot: the connector becomes a frame holding its line and its label.
let connector_label = own == "CONNECTOR" && !connector_snapshot && self.connector_label_fits(&view, depth);
let ty = if connector_label { "FRAME" } else { Self::mapped_type(own, &view, connector_snapshot, connector_terminal, stand)? };
// A whiteboard's sticky says it imports as a frame only when it can't be a native one.
// A whiteboard's sticky or shape with text says it imports as a frame only when it can't be a native one.
let native_board = self.board && matches!(own, "STICKY" | "SHAPE_WITH_TEXT");
if let Some((_, what)) = stand.filter(|_| own != "TEXT_PATH" && !native_board) {
self.warn(format!("{} layers import as {what}", own.to_ascii_lowercase().replace('_', " ")));
+1 -1
View File
@@ -1883,6 +1883,7 @@ name = "English"
"set-variable actions without a value were skipped" = "set-variable actions without a value were skipped"
"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped"
"shape generated outline is missing or damaged; using its rectangle" = "shape generated outline is missing or damaged; using its rectangle"
"shape with text layers import as frames with their text" = "shape with text layers import as frames with their text"
"slot content assignment target is missing or is not slot content" = "slot content assignment target is missing or is not slot content"
"slot content assignment uses the empty GUID sentinel and was skipped" = "slot content assignment uses the empty GUID sentinel and was skipped"
"slot layers without a matching slot property were imported as frames" = "slot layers without a matching slot property were imported as frames"
@@ -1891,7 +1892,6 @@ name = "English"
"stale or invalid grid track sizing entries were ignored" = "stale or invalid grid track sizing entries were ignored"
"stars and polygons without point counts or geometry use the default point count" = "stars and polygons without point counts or geometry use the default point count"
"sticky layers import as frames with their text" = "sticky layers import as frames with their text"
"shape with text layers import as frames with their text" = "shape with text layers import as frames with their text"
"style settings of some vector segments aren't supported; those segments use the layer's stroke" = "style settings of some vector segments aren't supported; those segments use the layer's stroke"
"text path underline or strikethrough decoration is not rendered" = "text path underline or strikethrough decoration is not rendered"
"text style overrides refer to missing table entries" = "text style overrides refer to missing table entries"
+2 -2
View File
@@ -1,6 +1,6 @@
# Boards parity
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-11-10 · **Change:** minor (a `.jam` file's shapes with text come in as native board shapes for the 23 kinds FigCraft draws, the other 7 as frames (#1808); resizing a board table by height changes its rows, not blank space below them: a bottom or top edge grows the last row, a corner shares the change over every row, and a table keeps at least 40 per cell, measured in FigJam's web app, 2026-11-09 (#2172); a `.jam` file's stickies come in as native board stickies, with their colour, text and author kept and the size as saved (#1808); a sticky's text stays 18 and the note grows taller instead of the text shrinking, as measured in FigJam's web app (#2157); mind maps, first step: a first node with three children on curved branches, laid out again after every edit (`board.mindMap`, `board.mindMapAdd`, #2226); a `.jam` elbowed connector saved with its path keeps it on import instead of being routed again (#1910); a "+" handle dragged carries the preview: the next shape is made where it is let go, or a connector joins the object it is let go on (#1919); a connector attached to a side starts 4 pt outside the border, so its stroke never lies on the fill (#1916); a connector end moved with Cmd/Ctrl held goes on any part of an object, and stays there as the object moves or resizes; a connector end dropped along a side of a shape attaches to that side (#1708); a board connector's path can be reshaped by dragging its path handles (#1710); boards have 16 more shapes (arrows, polygons, star, plus and flowchart symbols), in pickers that lay them out in a grid; a board keeps earlier votes' results, listed newest first in Results and deleted one at a time; a connector's bar sets its start and end points, dashes, weight and any colour (#1596); cells copied from a spreadsheet paste onto a board as a table; Shift+Cmd/Ctrl+Enter on a selected board shape adds the next shape already joined to it, and goes on typing (#1597); "+" handles beside a selected sticky or shape make the next one, joined by a connector (#1597); a FigJam `.jam` file opens as a board (#1342); board grid settings: dots, lines, graph paper or none, spacing, size and contrast; the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); templates and tables; facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-11-12 · **Change:** minor (a `.jam` file's shapes with text come in as native board shapes for the 23 kinds FigCraft draws, the other 7 as frames (#1808); resizing a board table by height changes its rows, not blank space below them: a bottom or top edge grows the last row, a corner shares the change over every row, and a table keeps at least 40 per cell, measured in FigJam's web app, 2026-11-09 (#2172); a `.jam` file's stickies come in as native board stickies, with their colour, text and author kept and the size as saved (#1808); a sticky's text stays 18 and the note grows taller instead of the text shrinking, as measured in FigJam's web app (#2157); mind maps, first step: a first node with three children on curved branches, laid out again after every edit (`board.mindMap`, `board.mindMapAdd`, #2226); a `.jam` elbowed connector saved with its path keeps it on import instead of being routed again (#1910); a "+" handle dragged carries the preview: the next shape is made where it is let go, or a connector joins the object it is let go on (#1919); a connector attached to a side starts 4 pt outside the border, so its stroke never lies on the fill (#1916); a connector end moved with Cmd/Ctrl held goes on any part of an object, and stays there as the object moves or resizes; a connector end dropped along a side of a shape attaches to that side (#1708); a board connector's path can be reshaped by dragging its path handles (#1710); boards have 16 more shapes (arrows, polygons, star, plus and flowchart symbols), in pickers that lay them out in a grid; a board keeps earlier votes' results, listed newest first in Results and deleted one at a time; a connector's bar sets its start and end points, dashes, weight and any colour (#1596); cells copied from a spreadsheet paste onto a board as a table; Shift+Cmd/Ctrl+Enter on a selected board shape adds the next shape already joined to it, and goes on typing (#1597); "+" handles beside a selected sticky or shape make the next one, joined by a connector (#1597); a FigJam `.jam` file opens as a board (#1342); board grid settings: dots, lines, graph paper or none, spacing, size and contrast; the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); templates and tables; facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
**Breadth ~47%, ready ~29% (estimated, erring low).** A board is a FigCraft file of its own kind
(`file.newBoard`) with a dot grid on the page, sticky notes, shapes with text, connectors that
@@ -76,7 +76,7 @@ all end with the same geometry.
| 2026-11-09 | patch | M12, #2157: a sticky's text is 18 and stays 18 (measured in FigJam's web app: ten lines, then a long paragraph, never changed it; the note kept its width and grew taller). The normalize pass no longer resets a sticky's text to a size by its length (24 down to 11 before), so stickies in saved files keep their size. Size presets (Small to Huge) are still missing. Tests: `a_new_sticky_starts_at_18`, `a_sticky_keeps_its_text_size_as_text_is_added`, `a_sticky_saved_with_another_text_size_keeps_it` |
| 2026-11-10 | minor | M12, #2226 (step 1 of 4): mind maps in the document and engine. A mind map node is a board object of its own (`BoardItem::MindNode`: text with no box) and a branch is a connector marked `branch`; the map's shape is read from its branches. `board.mindMap` places a first node with three children and `board.mindMapAdd` adds a child or a sibling, each one undo step. The normalize pass lays a touched map out again from its first node (`board::settle_mind_maps`, `figcraft_doc::mind_layout`): 88 between columns, 48 between siblings, each stack centred on its parent, numbers from a FigJam file saved and decoded. Tests: `board_mindmap_tests` in doc (five) and engine (eight). Numbers unchanged (no toolbar entry yet). |
| 2026-11-03 | minor | M12, #1919: as the quick create help says ("Click to select and create the next shape in that direction, or drag the preview to any location on your board"; "A new shape is created with a connector and its text field active"), a "+" beside the selected sticky or shape can be dragged: the preview follows the pointer and, let go on empty board, the next one is made there, centred on the pointer, and joined by a connector leaving by the handle's side and arriving by the side facing it (`board.addConnected` gains `at: [x, y]`, a point of the page placed in the parent's coordinates); let go on another sticky or shape, inside a section or frame too (`board::sticky_or_shape_at`), a connector joins the two instead (`board.connector`), that object outlined while the pointer is over it; let go off the canvas or over the toolbar, nothing, and the bar above the selection stays away meanwhile, as for any drag. The click is as before. A shape picker on drop and choosing the connector type are not done (the help doesn't describe them). Tests: `board_connect_tests` (the drag ones), engine `board_connected_tests::the_next_one_goes_where_it_is_dropped`. Numbers unchanged. |
| 2026-11-12 | minor | M9, #1808: `.jam` import: a FigJam shape with text comes in as a native board shape (`BoardItem::Shape`) for the 23 kinds FigCraft draws (square, rounded rectangle, ellipse, diamond, triangles, parallelograms, trapezoid, pentagon, hexagon, octagon, star, plus, arrows, chevron, speech bubble, database, document, predefined process, manual input, internal storage). The frame is empty with centred auto layout and the kind's text insets; it holds an `Outline` vector drawn by FigCraft (`BoardShape::path`, so FigJam's 3 pt corner on a square is lost, and `board.setShape` and resizing treat it as FigCraft's own) with the file's paints, weight and dashes as read, and the text, centred. The 7 kinds FigCraft has no outline for (ENG_QUEUE, ENG_FILE, ENG_FOLDER, SHIELD, DOCUMENT_MULTIPLE, SUMMING_JUNCTION, OR) stay frames with their generated layers and a warning naming the kind; so does a shape whose generated records are damaged (a shape with no text yet is still native, with an empty text layer), and the same shape in a `.fig`. Tests: `board_import_tests.rs` (five). Numbers unchanged. |
| 2026-11-12 | minor | M9, #1808: `.jam` import: a FigJam shape with text comes in as a native board shape (`BoardItem::Shape`) for the 23 kinds FigCraft draws (square, rounded rectangle, ellipse, diamond, triangles, parallelograms, trapezoid, pentagon, hexagon, octagon, star, plus, arrows, chevron, speech bubble, database, document, predefined process, manual input, internal storage). The frame is empty and a fixed size, as a shape made here, with centred auto layout and the kind's text insets; it holds an `Outline` vector drawn by FigCraft (`BoardShape::path`, so FigJam's 3 pt corner on a square is lost, and `board.setShape` and resizing treat it as FigCraft's own) with the file's paints, weight and dashes as read, and the text, centred. The 7 kinds FigCraft has no outline for (ENG_QUEUE, ENG_FILE, ENG_FOLDER, SHIELD, DOCUMENT_MULTIPLE, SUMMING_JUNCTION, OR) stay frames with their generated layers and a warning naming the kind; so does a shape whose generated records are damaged, or whose generated text is blank while the outer layer still carries text (#1397), and the same shape in a `.fig`; a shape with no text at all is still native, with an empty text layer. Tests: `board_import_tests.rs` (six). Numbers unchanged. |
| 2026-11-10 | minor | M9, #1808: a `.jam` sticky imports as a native board sticky (`BoardItem::Sticky`): its frame keeps the saved size (240 pt, not FigCraft's 200), place, opacity and shadows, the saved colour is kept as it is (not snapped to the board colours), the note and the author line are its text layers, and it lays out like one made here (vertical, space between, hugging the height down to the saved size, the saved inset as padding). The importer reads a `board` field on the REST JSON (a FigCraft extension) for this; a sticky whose generated records are damaged, or the same sticky in a `.fig`, keeps the generated layers as before. Shapes, connectors, stamps and tables are unchanged; the note's text keeps the file's size until it is edited. Tests: `board_import_tests.rs` (native sticky, no author, damaged records, `.fig` unchanged), `import_regression_tests.rs` (the `board` field and junk values). Numbers unchanged. |
| 2026-11-04 | minor | M12, #1916: a connector end attached to a side starts 4 pt outside the border (`board_route::END_GAP`), as FigJam's do (the issue's screenshots: the line starts a few pixels outside, with a round cap), so the stroke never lies on the fill; elbows leaving one side share their first run along its normal, curves leave tangent to it and straight ones fan out from the same point; the `.jam` importer's routes, re-attached ends, path handles and labels follow the route's points; an end placed by hand with Cmd/Ctrl held keeps its exact point. Tests: `board_route_tests` (three). Numbers unchanged. |
| 2026-11-04 | patch | M6, #1910: a `.jam` elbowed connector saved with its path (FigJam's `connectorControlPoints`) keeps it on import: the points become the `ConnectorPath::Runs` a connector reshaped by hand has and the line is walked along them, so a connector drawn around a layer stays around it; one whose points don't make a path from its ends' sides is routed as before. Still a vector, not a native board object. Details in [`file-format-parity.md`](file-format-parity.md). |
File diff suppressed because one or more lines are too long