mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-03 08:49:37 -06:00
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -227,6 +227,59 @@ fn the_connector_being_drawn_is_the_one_made() {
|
||||
assert!(the_connector(&s).is_none());
|
||||
}
|
||||
|
||||
/// A selected connector's end dropped anywhere along a side of a shape takes that side, far from
|
||||
/// its middle too; in the middle of the shape the app picks (#1708).
|
||||
#[test]
|
||||
fn an_end_dropped_along_a_side_attaches_to_that_side() {
|
||||
let mut s = board();
|
||||
let mut shape = |x: f64, y: f64| id(&s.execute("board.shape", &json!({"x": x, "y": y, "width": 320, "height": 120})).unwrap(), "id");
|
||||
let (a, b) = (shape(0.0, 0.0), shape(520.0, 660.0));
|
||||
let k = id(&s.execute("board.connector", &json!({"from": a, "to": b, "style": "elbow"})).unwrap(), "id");
|
||||
let ev = |kind, p: Point| PointerEvent::at(kind, p.x, p.y, 1.0);
|
||||
for (drop, side) in [
|
||||
// The report's: on the right edge, away from its middle.
|
||||
(Point::new(318.0, 100.0), Side::Right),
|
||||
(Point::new(318.0, 10.0), Side::Right),
|
||||
(Point::new(4.0, 20.0), Side::Left),
|
||||
(Point::new(40.0, 3.0), Side::Top),
|
||||
(Point::new(280.0, 116.0), Side::Bottom),
|
||||
// In the middle: the side facing the other end.
|
||||
(Point::new(160.0, 60.0), Side::Auto),
|
||||
(Point::new(120.0, 50.0), Side::Auto),
|
||||
] {
|
||||
s.execute("select.set", &json!({"ids": [k]})).unwrap();
|
||||
s.pointer(&ev(PointerKind::Down, drawn(&s, k)[0])).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, Point::new(200.0, 300.0))).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, drop)).unwrap();
|
||||
// The ring shows the side the drop will take.
|
||||
let lit: Vec<[f64; 2]> = s.tool.overlay.attach.iter().filter(|a| a.1).map(|a| a.0).collect();
|
||||
let bounds = s.doc().unwrap().world_bounds(a).unwrap();
|
||||
let want = (side != Side::Auto).then(|| side.point(bounds)).map(|p| [p.x, p.y]);
|
||||
assert_eq!(lit, want.into_iter().collect::<Vec<_>>(), "{drop:?}");
|
||||
s.pointer(&ev(PointerKind::Up, drop)).unwrap();
|
||||
assert_eq!(connector(&s, k).from, End::Node { node: a, side }, "{drop:?}");
|
||||
if side != Side::Auto {
|
||||
assert_eq!(drawn(&s, k)[0], side.point(bounds), "the route leaves from that side");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The connector tool chooses sides the same way, at both ends.
|
||||
#[test]
|
||||
fn the_connector_tool_takes_the_side_it_starts_and_ends_on() {
|
||||
let mut s = board();
|
||||
let mut shape = |x: f64, y: f64| id(&s.execute("board.shape", &json!({"x": x, "y": y, "width": 200, "height": 100})).unwrap(), "id");
|
||||
let (a, b) = (shape(0.0, 0.0), shape(400.0, 300.0));
|
||||
s.execute("tool.connector", &json!({"style": "elbow"})).unwrap();
|
||||
let ev = |k, x, y| PointerEvent::at(k, x, y, 1.0);
|
||||
s.pointer(&ev(PointerKind::Down, 40.0, 96.0)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, 420.0, 310.0)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, 590.0, 330.0)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, 590.0, 330.0)).unwrap();
|
||||
let k = the_connector(&s).unwrap().id;
|
||||
assert_eq!((connector(&s, k).from, connector(&s, k).to), (End::Node { node: a, side: Side::Bottom }, End::Node { node: b, side: Side::Right }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_connector_end_snaps_to_the_attach_point_under_the_pointer() {
|
||||
let mut s = board();
|
||||
|
||||
@@ -8,11 +8,26 @@ use super::{
|
||||
};
|
||||
|
||||
/// How near (screen pixels) the pointer must be to the middle of an object's side for a connector
|
||||
/// end to attach there rather than to the side facing the other end.
|
||||
/// end to attach there, however small the object is on screen.
|
||||
const ATTACH_PX: f64 = 10.0;
|
||||
/// The sides an object takes a connector end at (their middles are its attach points).
|
||||
const SIDES: [Side; 4] = [Side::Top, Side::Right, Side::Bottom, Side::Left];
|
||||
|
||||
/// The side of box `b` a connector end at `p` chooses (#1708): the one `p` is towards anywhere in
|
||||
/// the box's outer half, whatever its proportions, so a whole edge picks its side. In the middle
|
||||
/// of the box none is chosen ([`Side::Auto`]: the side facing the other end).
|
||||
fn side_at(b: figcraft_geom::Rect, p: Point) -> Side {
|
||||
let c = b.center();
|
||||
let (x, y) = ((p.x - c.x) / (b.width() / 2.0).max(1e-6), (p.y - c.y) / (b.height() / 2.0).max(1e-6));
|
||||
match (x.abs().max(y.abs()) < 0.5, x.abs() >= y.abs()) {
|
||||
(true, _) => Side::Auto,
|
||||
(false, true) if x > 0.0 => Side::Right,
|
||||
(false, true) => Side::Left,
|
||||
(false, false) if y > 0.0 => Side::Bottom,
|
||||
(false, false) => Side::Top,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The board tools.
|
||||
|
||||
@@ -77,7 +92,7 @@ impl Session {
|
||||
}
|
||||
|
||||
/// What a connector end at the pointer attaches to: the object there (at the side whose middle
|
||||
/// is within [`ATTACH_PX`], else the side facing the other end), or the point.
|
||||
/// is within [`ATTACH_PX`], else the side [`side_at`] the pointer), or the point.
|
||||
fn end_at(&self, ev: &PointerEvent) -> End {
|
||||
let zoom = ev.zoom.max(1e-6);
|
||||
let hit = (|| {
|
||||
@@ -92,7 +107,8 @@ impl Session {
|
||||
}
|
||||
let b = doc.world_bounds(t);
|
||||
let far = |s: &Side| b.map(|b| (s.point(b) - ev.p()).hypot() * zoom).filter(|d| *d <= ATTACH_PX);
|
||||
let side = SIDES.iter().filter_map(|s| Some((*s, far(s)?))).min_by(|x, y| x.1.total_cmp(&y.1)).map_or(Side::Auto, |(s, _)| s);
|
||||
let near = SIDES.iter().filter_map(|s| Some((*s, far(s)?))).min_by(|x, y| x.1.total_cmp(&y.1)).map(|(s, _)| s);
|
||||
let side = near.or(b.map(|b| side_at(b, ev.p()))).unwrap_or(Side::Auto);
|
||||
Some(End::Node { node: t, side })
|
||||
})();
|
||||
hit.unwrap_or(End::Point { x: ev.x, y: ev.y })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Boards parity
|
||||
|
||||
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-30 · **Change:** minor (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-10-30 · **Change:** minor (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
|
||||
@@ -32,6 +32,9 @@ all end with the same geometry.
|
||||
| Prototyping and Dev 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 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 | |
|
||||
| Connectors | done | from a layer's side (the facing one, or one named) to another layer or a free point, straight, elbow or curved, arrowheads (the stroke's caps), a label in the middle of the route by length (double-click to add); while one is dragged out it shows as it will be made (route, line and arrowhead), the object under the pointer outlined with its four attach points (side middles), the one the end goes to ringed; an end dropped on the outer half of an object takes the side it is towards (a drop along a whole edge chooses that edge), one in the middle takes the facing side; re-routed when either end moves or resizes; a deleted end stays where it was |
|
||||
| Shapes with text | done | 23 outlines: rectangle, rounded rectangle, ellipse, diamond, triangle up and down, parallelogram right and left, trapezoid, pentagon, hexagon, octagon, star, plus, arrow left and right, chevron, speech bubble, and the flowchart symbols database, document, predefined process, manual input and internal storage; text centred and wrapping inside the outline; shape type and text size from the contextual bar |
|
||||
| Shape auto-fit to its text, the advanced shape libraries (engineering and other icon shapes) | missing | |
|
||||
| Connectors | done | from a layer's side (the facing one, or one named) to another layer or a free point, straight, elbow or curved, arrowheads (the stroke's caps), a label in the middle of the route by length (double-click to add); while one is dragged out it shows as it will be made (route, line and arrowhead), the object under the pointer outlined with its four attach points (side middles), the one the end goes to ringed, and within 10 px of one the end keeps that side; re-routed when either end moves or resizes; a deleted end stays where it was |
|
||||
@@ -66,6 +69,7 @@ all end with the same geometry.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | patch | M12, #1708: a connector end dropped on the outer half of a shape takes the side it is towards, instead of only within 10 px of a side's middle, so a drop anywhere along an edge chooses that edge (dragging a selected connector's end and with the connector tool, ring included); a drop in the middle of a shape still takes the facing side. |
|
||||
| 2026-10-31 | minor | M12, #1710: a selected elbow or curved connector shows path handles. Dragging a run moves it and a first or last run gains an extra run to stay on its end; a curve's middle goes where it is dragged (`board.reshape`, one undo step). The shape travels with what the connector joins, is saved with the file, and goes back to routing itself when the style changes or an end moves to something else. |
|
||||
| 2026-10-31 | minor | M12: boards have 23 shapes with text instead of 7: triangle down, parallelogram right and left, trapezoid, pentagon, hexagon, octagon, star, plus, arrow left and right, and the flowchart symbols database, document, predefined process, manual input and internal storage. The toolbar flyout wraps into rows of eight and the selection bar's shape menu is a grid of outlines named on hover. The stale "Quick-create handles: missing" row now says what exists. |
|
||||
| 2026-10-30 | minor | M12, gap 24: a board keeps each ended vote's results in the file (the last 20), as the reference app keeps each voting session's results until they're deleted. A record holds the vote, its ballots, the names the voters went by and when it ended, so someone who has left a live session still shows by name. The Results popover lists them newest first, with arrows to earlier votes and Delete results (`board.vote {action: delete, index}`). Clear results still only takes the results on the board off it. `board.voteResults` lists `sessions`. The results are saved (`vote_history`; older files open without it, and a vote ended in one still shows), synced in live sessions (`DocProp::VoteHistory`), and like the vote they're not undo steps: undo and redo carry them over. Tests: `vote_history_tests`, `facilitate_tests` and `vote_history_ui_tests`. |
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user