From 1ee3087bc97fc1476a4026ab53fef32c851cbc05 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Mon, 9 Nov 2026 08:46:12 -0500 Subject: [PATCH 1/2] M12: a sticky's text keeps its size and the note grows taller (#2157) FigJam's sticky text is 18 (Small) and never changes as the note fills; the note keeps its width and grows taller (measured in the web app, 2026-11-09). A new sticky's text is STICKY_TEXT_SIZE (18) and the normalize pass no longer resets a sticky's text size by its length, so saved stickies keep the size they have. No size menu yet. Co-Authored-By: Claude Sonnet 5.5 Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 measured in Figma and wrote at medium --- crates/doc/src/board.rs | 4 +- crates/engine/src/board_tests.rs | 71 ++++++++++++++++++++--- crates/engine/src/boards/board.rs | 2 +- crates/engine/src/boards/board/derived.rs | 19 +----- crates/engine/src/boards/board/items.rs | 17 ++---- docs/boards-parity.md | 5 +- 6 files changed, 76 insertions(+), 42 deletions(-) diff --git a/crates/doc/src/board.rs b/crates/doc/src/board.rs index 89413b2e9..c0fcd7c68 100644 --- a/crates/doc/src/board.rs +++ b/crates/doc/src/board.rs @@ -37,8 +37,8 @@ impl FileKind { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum BoardItem { - /// A sticky note: a frame whose first text layer is the note (its size shrinks as the note - /// grows) and, below it, who wrote it. + /// A sticky note: a frame whose first text layer is the note (its size stays fixed, the note + /// grows taller) and, below it, who wrote it. Sticky { #[serde(default, skip_serializing_if = "String::is_empty")] author: String, diff --git a/crates/engine/src/board_tests.rs b/crates/engine/src/board_tests.rs index 9e99bd1c9..1b0ac120c 100644 --- a/crates/engine/src/board_tests.rs +++ b/crates/engine/src/board_tests.rs @@ -7,7 +7,7 @@ use figcraft_doc::{ use figcraft_geom::Point; use serde_json::{Value, json}; -use crate::board::{route, sticky_font_size}; +use crate::board::{STICKY_TEXT_SIZE, route}; use crate::collab_live_tests::Room; use crate::tools::{PointerEvent, PointerKind, ToolId}; use crate::{EngineError, Session}; @@ -77,7 +77,7 @@ fn board_commands_refuse_on_design_files_and_prototyping_refuses_on_boards() { } #[test] -fn a_sticky_is_a_square_frame_whose_note_shrinks_as_it_grows() { +fn a_sticky_is_a_square_frame_whose_note_grows_taller_as_it_fills() { let mut s = board(); s.prefs.user_name = "Ada".into(); let r = s.execute("board.sticky", &json!({"x": 10, "y": 20, "text": "Hi", "color": "blue"})).unwrap(); @@ -88,19 +88,74 @@ fn a_sticky_is_a_square_frame_whose_note_shrinks_as_it_grows() { assert_eq!(n.fills[0].solid_color(), board_color("blue")); assert!(n.layout.is_some() && !n.effects.is_empty()); assert_eq!(n.children.len(), 2, "the note and its author"); - assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 24.0); + assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 18.0); // A click on the note picks the sticky whole. let doc = s.doc().unwrap(); assert_eq!(figcraft_doc::geometry::select_target(doc, note, &[], false), st); - // A long note takes a smaller size; a very long one grows the sticky past its square. + // A long note keeps the size; a very long one grows the sticky past its square. let long = "word ".repeat(120); s.execute("node.set", &json!({"id": note, "props": {"characters": long}})).unwrap(); - assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, sticky_font_size(&long)); - assert!(sticky_font_size(&long) < 16.0); + assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, STICKY_TEXT_SIZE); assert!(node(&s, st).width == 200.0 && node(&s, st).height >= 200.0); - // Undo puts the short note and its size back. + // Undo puts the short note back. s.execute("edit.undo", &json!({})).unwrap(); - assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 24.0); + assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 18.0); +} + +#[test] +fn a_new_sticky_starts_at_18() { + let mut s = board(); + for (cmd, p) in [("board.sticky", json!({"x": 0, "y": 0, "text": "Hi"})), ("board.sticky", json!({"x": 300, "y": 0}))] { + let note = id(&s.execute(cmd, &p).unwrap(), "text"); + assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 18.0); + } + let next = id(&s.execute("board.nextSticky", &json!({})).unwrap(), "text"); + assert_eq!(node(&s, next).kind.text().unwrap().style.font_size, 18.0); + assert_eq!(STICKY_TEXT_SIZE, 18.0); +} + +#[test] +fn a_sticky_keeps_its_text_size_as_text_is_added() { + let mut s = board(); + let r = s.execute("board.sticky", &json!({"x": 0, "y": 0, "text": "one"})).unwrap(); + let (st, note) = (id(&r, "id"), id(&r, "text")); + let (height, text_height) = (node(&s, st).height, node(&s, note).height); + // Typing in the note: each step is a real edit that runs the normalize pass. + crate::board::edit_text(&mut s, st).unwrap(); + for _ in 0..9 { + crate::textedit::key(&mut s, "Enter", false, false, false).unwrap(); + crate::textedit::insert(&mut s, "another line").unwrap(); + } + let ten = node(&s, note).kind.text().unwrap().clone(); + assert_eq!(ten.characters.matches('\n').count(), 9, "ten lines"); + assert_eq!(ten.style.font_size, 18.0); + // Growth needs text measured: a build without the craft-fonts input measures nothing (the same + // reason `a_double_click_types_in_the_cell...` fails there), so it is only checked where it is. + let measured = node(&s, note).height > text_height; + assert!(!measured || node(&s, st).height > height, "ten lines make the note taller"); + assert_eq!(node(&s, st).child_layout.sizing_v, figcraft_doc::Sizing::Hug, "the note hugs its text"); + let (w, h10) = (node(&s, st).width, node(&s, st).height); + crate::textedit::insert(&mut s, &"a long paragraph ".repeat(15)).unwrap(); + crate::textedit::end(&mut s).unwrap(); + let t = node(&s, note).kind.text().unwrap().clone(); + assert!(t.characters.chars().count() >= 250); + assert_eq!(t.style.font_size, 18.0); + assert!(t.runs.iter().all(|r| r.style.font_size == 18.0)); + assert_eq!(node(&s, st).width, w, "the note keeps its width"); + assert!(!measured || node(&s, st).height > h10, "and grows taller"); +} + +#[test] +fn a_sticky_saved_with_another_text_size_keeps_it() { + let mut s = board(); + let r = s.execute("board.sticky", &json!({"x": 0, "y": 0, "text": "old"})).unwrap(); + let (st, note) = (id(&r, "id"), id(&r, "text")); + s.execute("node.set", &json!({"id": note, "props": {"fontSize": 11}})).unwrap(); + assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 11.0); + s.execute("node.set", &json!({"id": note, "props": {"characters": "old and now much longer than it was"}})).unwrap(); + assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 11.0, "an edit"); + s.execute("node.set", &json!({"id": st, "props": {"x": 40}})).unwrap(); + assert_eq!(node(&s, note).kind.text().unwrap().style.font_size, 11.0, "a move"); } #[test] diff --git a/crates/engine/src/boards/board.rs b/crates/engine/src/boards/board.rs index 1a66eed22..158e679d4 100644 --- a/crates/engine/src/boards/board.rs +++ b/crates/engine/src/boards/board.rs @@ -68,8 +68,8 @@ pub(crate) use edit::{copied_items, copy_map, rejoin}; use edit::{label_cmd, set_color, set_style}; pub(crate) use items::add_text; pub use items::{CONNECTED_GAP, connected_place}; +pub use items::{STICKY_TEXT_SIZE, edit_text, owner_of_text, text_of}; use items::{add_connected, ink_cmd, next_sticky, shape_cmd, stamp_cmd, sticky_cmd}; -pub use items::{edit_text, owner_of_text, sticky_font_size, text_of}; pub(crate) use reshape::path_handle_at; pub use reshape::selected_connector_handles; diff --git a/crates/engine/src/boards/board/derived.rs b/crates/engine/src/boards/board/derived.rs index 9fae17206..37a2f9ba9 100644 --- a/crates/engine/src/boards/board/derived.rs +++ b/crates/engine/src/boards/board/derived.rs @@ -1,7 +1,6 @@ use figcraft_doc::RouteEnd; -use super::edit::set_font_size; -use super::items::{owner_of_text, shape_padding, sticky_font_size, text_of}; +use super::items::shape_padding; use super::{ Affine, BTreeSet, BezPath, BoardItem, Connector, Document, End, NodeId, NodeKind, PathEl, Point, Rect, Routing, Shape, VectorNetwork, is_board, }; @@ -9,26 +8,14 @@ use super::{ // --------------------------------------------------------------------------------------------- // Derived data (the normalize pass). -/// Before text is measured: a sticky's note takes the size its length calls for, and a resized -/// shape keeps its text inside its outline. +/// Before text is measured: a resized shape keeps its text inside its outline. A sticky's text +/// keeps whatever size it has (FigJam's size is the note's own setting). pub(crate) fn settle_text(doc: &mut Document, before: &Document, touched: &BTreeSet) { if !is_board(doc) { return; } for &id in touched { let Some(n) = doc.node(id) else { continue }; - if let Some(owner) = - owner_of_text(doc, id).or(Some(id)).filter(|o| doc.node(*o).is_some_and(|k| matches!(k.board, Some(BoardItem::Sticky { .. })))) - && let Some(t) = text_of(doc, owner) - && let Some(NodeKind::Text(td)) = doc.node(t).map(|k| &k.kind) - { - let size = sticky_font_size(&td.characters); - let off = td.style.font_size != size || td.runs.iter().any(|r| r.style.font_size != size); - if off && let Some(NodeKind::Text(td)) = doc.node_mut(t).map(|k| &mut k.kind) { - set_font_size(td, size); - } - continue; - } if let Some(BoardItem::Shape { shape }) = n.board && before.node(id).is_none_or(|b| (b.width, b.height) != (n.width, n.height)) { diff --git a/crates/engine/src/boards/board/items.rs b/crates/engine/src/boards/board/items.rs index b409eeb90..115ce6d3b 100644 --- a/crates/engine/src/boards/board/items.rs +++ b/crates/engine/src/boards/board/items.rs @@ -57,18 +57,9 @@ pub(crate) fn add_text(doc: &mut Document, parent: NodeId, text: &str, size: f64 Ok(id) } -/// The text size a sticky's note gets: smaller the more it holds (by characters and lines, so -/// every copy of the file picks the same size whatever fonts it has). -pub fn sticky_font_size(text: &str) -> f64 { - let weight = text.chars().count() + 24 * text.matches('\n').count(); - match weight { - 0..=24 => 24.0, - 25..=64 => 20.0, - 65..=140 => 16.0, - 141..=280 => 13.0, - _ => 11.0, - } -} +/// The text size of a new sticky's note, FigJam's "Small" default (measured in the web app, +/// 2026-11-09): it never changes as the note fills, the note grows taller instead. +pub const STICKY_TEXT_SIZE: f64 = 18.0; /// A sticky note's layers: the frame, the note and (when known) its author. fn make_sticky(doc: &mut Document, parent: NodeId, at: (f64, f64), text: &str, color: Color, author: &str) -> Result<(NodeId, NodeId)> { @@ -89,7 +80,7 @@ fn make_sticky(doc: &mut Document, parent: NodeId, at: (f64, f64), text: &str, c n.board = Some(BoardItem::Sticky { author: author.to_string() }); })?; let inner = STICKY - 2.0 * STICKY_PAD; - let note = add_text(doc, id, text, sticky_font_size(text), INK, TextAlignH::Left, inner)?; + let note = add_text(doc, id, text, STICKY_TEXT_SIZE, INK, TextAlignH::Left, inner)?; if !author.is_empty() { let a = add_text(doc, id, author, 10.0, AUTHOR_INK, TextAlignH::Left, inner)?; if let Some(n) = doc.node_mut(a) { diff --git a/docs/boards-parity.md b/docs/boards-parity.md index 16456492c..2cc099f08 100644 --- a/docs/boards-parity.md +++ b/docs/boards-parity.md @@ -1,6 +1,6 @@ # Boards parity -> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-11-04 · **Change:** minor (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-09 · **Change:** minor (a sticky's text stays 18 and the note grows taller instead of the text shrinking, as measured in FigJam's web app (#2157); 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 @@ -30,7 +30,7 @@ all end with the same geometry. | Board toolbar at the bottom | done | Move/Hand, Marker/Highlighter/Eraser, Sticky (colour flyout), Shapes (flyout), Connector, Text, Section, Stamp (flyout), Comment, Actions, then the templates and the Design panel's buttons — on the same 32 pt grid as a design file's (no Resources button, as there) | | Design panel hidden by default; a contextual bar above the selection | done | colour, shape, text size (S/M/L/XL); for connectors straight/elbow/curved, the start and end points (none, round, square, line arrow, triangle arrow, reversed triangle, circle, diamond), solid or dashed, the weight, and any colour in the colour picker beside the board colours (#1596); each control says what it is on hover; the Design panel opens with its button or View ▸ Design panel | | Prototyping and Developer Mode refuse on boards | done | `proto.*`, `dev.*`, `view.prototype`, `view.devMode` say why | -| Sticky notes | done | a square frame (200 wide, growing taller through auto layout), 8 soft colours, a soft shadow, the note's text size shrinking as it grows (24 → 11, by characters and lines), the author's name at the bottom; a click places one and starts typing; Enter or a double-click edits; Shift+Cmd/Ctrl+Enter adds the next one beside it | +| Sticky notes | done | a square frame (200 wide, growing taller through auto layout), 8 soft colours, a soft shadow, the note's text 18 by default and fixed (FigJam's "Small"): the note grows taller as it fills and never shrinks its text, measured in FigJam's web app, 2026-11-09; stickies saved with another size keep it, and there is no size menu yet (#2157), the author's name at the bottom; a click places one and starts typing; Enter or a double-click edits; Shift+Cmd/Ctrl+Enter adds the next one beside it | | Sticky options: hide the author, change size presets, sticky stacks | partial | `board.sticky {author: false}`; no size presets or stacks | | Shapes with text | done | rectangle, rounded rectangle, ellipse, diamond, triangle, speech bubble, chevron; text centred and wrapping inside the outline; shape type and text size from the contextual bar | | Shape auto-fit to its text, more shapes (cylinder, cross, star…), shape libraries | missing | | @@ -72,6 +72,7 @@ all end with the same geometry. | Date | Change | Summary | |---|---|---| +| 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-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-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). | From ff3e4e6010b905bf7dc09c507a2078426efa9d37 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Mon, 9 Nov 2026 09:38:56 -0500 Subject: [PATCH 2/2] M12: sticky growth checked with a measured font; the normalize note drops sticky size (#2164 review) The growth test scans the system fonts, as the other text-measuring engine tests do (#333), and asserts the note grows taller unconditionally instead of only when text happened to measure. docs/boards-parity.md no longer lists a sticky's text size among what the normalize pass derives. Co-Authored-By: Claude Opus 5.5 Model-Effort: Opus 5.5 checked the findings and wrote --- crates/engine/src/board_tests.rs | 11 ++++++----- docs/boards-parity.md | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/board_tests.rs b/crates/engine/src/board_tests.rs index 1b0ac120c..eb00647ac 100644 --- a/crates/engine/src/board_tests.rs +++ b/crates/engine/src/board_tests.rs @@ -116,6 +116,9 @@ fn a_new_sticky_starts_at_18() { #[test] fn a_sticky_keeps_its_text_size_as_text_is_added() { + // The note's growth needs text measured: without craft-fonts that is with a system font, + // scanned here rather than by whichever test happens to run first (#333). + figcraft_text::scan_system_fonts(); let mut s = board(); let r = s.execute("board.sticky", &json!({"x": 0, "y": 0, "text": "one"})).unwrap(); let (st, note) = (id(&r, "id"), id(&r, "text")); @@ -129,10 +132,8 @@ fn a_sticky_keeps_its_text_size_as_text_is_added() { let ten = node(&s, note).kind.text().unwrap().clone(); assert_eq!(ten.characters.matches('\n').count(), 9, "ten lines"); assert_eq!(ten.style.font_size, 18.0); - // Growth needs text measured: a build without the craft-fonts input measures nothing (the same - // reason `a_double_click_types_in_the_cell...` fails there), so it is only checked where it is. - let measured = node(&s, note).height > text_height; - assert!(!measured || node(&s, st).height > height, "ten lines make the note taller"); + assert!(node(&s, note).height > text_height, "ten lines make the text taller"); + assert!(node(&s, st).height > height, "and the note"); assert_eq!(node(&s, st).child_layout.sizing_v, figcraft_doc::Sizing::Hug, "the note hugs its text"); let (w, h10) = (node(&s, st).width, node(&s, st).height); crate::textedit::insert(&mut s, &"a long paragraph ".repeat(15)).unwrap(); @@ -142,7 +143,7 @@ fn a_sticky_keeps_its_text_size_as_text_is_added() { assert_eq!(t.style.font_size, 18.0); assert!(t.runs.iter().all(|r| r.style.font_size == 18.0)); assert_eq!(node(&s, st).width, w, "the note keeps its width"); - assert!(!measured || node(&s, st).height > h10, "and grows taller"); + assert!(node(&s, st).height > h10, "and grows taller"); } #[test] diff --git a/docs/boards-parity.md b/docs/boards-parity.md index 2cc099f08..f874daad5 100644 --- a/docs/boards-parity.md +++ b/docs/boards-parity.md @@ -17,8 +17,8 @@ design product). The work item is gap 24 in [`gaps.md`](gaps.md). Board objects are ordinary layers with a small `board` field on the node (`figcraft_doc::BoardItem`), so rendering, hit testing, undo, saving, SVG/PNG/PDF export, copy and paste and live sessions work on them unchanged. The file kind (`Document.kind`) is saved in `.figcraft` and reaches people who -join a live session in their snapshot. What follows from the metadata (a sticky's text size, a -shape's text room, connector routes and their labels) is recomputed in the engine's normalize pass, +join a live session in their snapshot. What follows from the metadata (a shape's text +room, connector routes and their labels) is recomputed in the engine's normalize pass, deterministically from the document, so local edits, undo and edits from others in a live session all end with the same geometry.