diff --git a/crates/engine/src/board_mindmap_tests.rs b/crates/engine/src/board_mindmap_tests.rs index 19bb459ae..fa717b8eb 100644 --- a/crates/engine/src/board_mindmap_tests.rs +++ b/crates/engine/src/board_mindmap_tests.rs @@ -383,3 +383,41 @@ fn the_caps_refuse_what_is_too_far_and_cut_what_is_too_long() { assert_eq!(text_data(&s, id(&out, "id")).characters.chars().count(), 500); assert_eq!(text_data(&s, id(&out["children"][0], "id")).characters.chars().count(), 500); } + +/// Where a node's own origin (the place `x`/`y` name) is on the page. +fn origin(s: &Session, n: NodeId) -> Point { + s.doc().unwrap().world_transform(n) * Point::new(0.0, 0.0) +} + +#[test] +fn a_mind_map_in_a_section_without_a_place_is_centred_on_the_view() { + let mut s = board(); + let (vx, vy) = { + let v = &s.ds().unwrap().view; + (v.cx, v.cy) + }; + let sec = id(&s.execute("node.create", &json!({"type": "section", "x": 1000, "y": 500, "width": 4000, "height": 4000})).unwrap(), "id"); + let out = s.execute("board.mindMap", &json!({"parent": sec})).unwrap(); + let c = origin(&s, id(&out, "id")); + assert!(same(c.x, vx) && same(c.y, vy), "the root's place {c:?} is the view's centre ({vx}, {vy})"); +} + +#[test] +fn a_mind_map_in_a_turned_frame_without_a_place_is_centred_on_the_view_and_a_given_place_is_the_frames() { + let mut s = board(); + let (vx, vy) = { + let v = &s.ds().unwrap().view; + (v.cx, v.cy) + }; + let fr = id(&s.execute("node.create", &json!({"type": "frame", "x": 300, "y": 200, "width": 2000, "height": 2000})).unwrap(), "id"); + s.execute("node.set", &json!({"id": fr, "props": {"rotation": 30}})).unwrap(); + let out = s.execute("board.mindMap", &json!({"parent": fr})).unwrap(); + let c = origin(&s, id(&out, "id")); + assert!(same(c.x, vx) && same(c.y, vy), "the root's place {c:?} is the view's centre ({vx}, {vy})"); + + // A given place is the frame's own, as on the page: (10, 20) of the frame, carried through it. + let given = id(&s.execute("board.mindMap", &json!({"parent": fr, "x": 10, "y": 20})).unwrap(), "id"); + let want = s.doc().unwrap().world_transform(fr) * Point::new(10.0, 20.0); + let got = origin(&s, given); + assert!(same(got.x, want.x) && same(got.y, want.y), "{got:?} against {want:?}"); +} diff --git a/crates/engine/src/boards/board/mindmap.rs b/crates/engine/src/boards/board/mindmap.rs index 5e2cff1ce..656a3fa14 100644 --- a/crates/engine/src/boards/board/mindmap.rs +++ b/crates/engine/src/boards/board/mindmap.rs @@ -168,9 +168,13 @@ pub(super) fn mind_map_cmd(s: &mut Session, p: &Value) -> Result { let cmd = "board.mindMap"; require(s, cmd)?; let parent = parent_of(cmd, s, p)?; + // The view's centre is on the page, and `x`/`y` are in the parent: a map in a section or frame + // takes the centre in that parent's coordinates (a parent that can't be inverted gives a + // non-finite centre, which `coord` refuses). let (vx, vy) = { - let v = &s.ds()?.view; - (v.cx, v.cy) + let ds = s.ds()?; + let c = ds.doc.world_transform(parent).inverse() * figcraft_geom::Point::new(ds.view.cx, ds.view.cy); + (c.x, c.y) }; let (x, y) = (coord(cmd, p, "x", vx)?, coord(cmd, p, "y", vy)?); let texts = starters(cmd, p)?; diff --git a/docs/mcp.md b/docs/mcp.md index 21dfe8ca9..8d03540ba 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -498,7 +498,7 @@ one undo step and returns the ids it made: in the app does both). - `board.mindMap {x?, y?, text?, children?, parent?, edit?}` → `{id, text, children: [{id, text, connector}]}`: a mind map: a first node (`mindNode`, bold 24, no box; `text` defaults to `Topic`, `x`/`y` to the - middle of the view) with up to 20 `children` (texts; default `Idea 1`, `Idea 2`, `Idea 3`) + view's centre, in the parent's coordinates) with up to 20 `children` (texts; default `Idea 1`, `Idea 2`, `Idea 3`) on its right, each joined to it by a curved branch (a connector with `branch: true`: stroke 4, `#757575`, round caps). The map lays itself out: children stand in one column 88 units right of the widest node of their parent's column, 48 apart, centred on their parent; a node is its text,