fix(agent): give wrapped canvas rows headroom for frame labels
The canvas paints each top-level frame's name ABOVE the frame at a fixed screen-space offset that does not scale with zoom (canvas_frame_labels.rs puts the baseline at sy-18 and the label box top at sy-32). A row gap equal to the column gap therefore cannot work: at the zoom where a wrapped canvas is actually viewed, the gap is narrower than the label itself, so every second-row label sat pressed against the bottom edge of the row above. For the 3x2 deck the numbers are exact: 6000 doc px of content framed by zoom_to_fit into a 944-1424 px canvas region lands at zoom 0.14-0.22, where a 120 doc px row gap is only 16-26 screen px against a label that always occupies 32. It overlapped at every realistic window size. Add 240 doc px of headroom to the VERTICAL step only — 33-52 screen px at that zoom, clearing the label with room to spare. Columns are untouched: labels are left-aligned to their own frame and never reach sideways. The slide-deck template takes the same allowance on top of its own gap, so both the generated canvas and the shipped template space rows the same way. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
This commit is contained in:
parent
2b01566aac
commit
e5e84ea932
|
|
@ -1232,7 +1232,7 @@
|
|||
}
|
||||
],
|
||||
"x": 0,
|
||||
"y": 1200
|
||||
"y": 1440
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
|
|
@ -1575,7 +1575,7 @@
|
|||
}
|
||||
],
|
||||
"x": 2040,
|
||||
"y": 1200
|
||||
"y": 1440
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
|
|
@ -1664,7 +1664,7 @@
|
|||
}
|
||||
],
|
||||
"x": 4080,
|
||||
"y": 1200
|
||||
"y": 1440
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -462,6 +462,24 @@ pub(crate) const SCREEN_GROUP_GAP: f64 = 80.0;
|
|||
/// - 390 mobile → 17 per row, i.e. unchanged for any realistic fan-out
|
||||
const MAX_ROW_WIDTH: f64 = 8200.0;
|
||||
|
||||
/// Extra vertical clearance between rows, on top of `SCREEN_GROUP_GAP`.
|
||||
///
|
||||
/// The canvas paints each top-level frame's name ABOVE the frame at a FIXED
|
||||
/// screen-space offset that does not scale with zoom
|
||||
/// (`canvas_frame_labels.rs`: baseline at `sy - 18`, label box top at
|
||||
/// `sy - 32`). So a row gap equal to the column gap makes every second-row
|
||||
/// label collide with the bottom edge of the row above — 80 doc px is only
|
||||
/// ~11-17 screen px at the zoom where a wrapped canvas is actually viewed,
|
||||
/// well inside the label's fixed 32 px footprint (user report 2026-08-02).
|
||||
///
|
||||
/// 240 doc px is the allowance: fitting a wrapped multi-row canvas to the
|
||||
/// screen lands around zoom 0.14-0.22 (a 3-wide 1920 deck is 6000 doc px
|
||||
/// across, framed by `zoom_to_fit` into a 944-1424 px canvas region), where
|
||||
/// 240 doc px reads as 33-52 screen px — the label's 32 px plus breathing
|
||||
/// room. Horizontal spacing is untouched: labels are left-aligned to their
|
||||
/// own frame and never reach sideways.
|
||||
const ROW_LABEL_HEADROOM: f64 = 240.0;
|
||||
|
||||
/// How many screen roots fit in one row at `board_width`.
|
||||
///
|
||||
/// Always ≥ 1: a board wider than the whole row budget still has to go
|
||||
|
|
@ -527,14 +545,16 @@ pub(crate) fn build_screen_group_scaffold(
|
|||
|
||||
// Roots wrap into rows instead of one endless strip (see `MAX_ROW_WIDTH`).
|
||||
// `y` is the top of the FIRST row; each later row steps down by a board
|
||||
// height plus the same gutter the columns use. The row step reads the
|
||||
// height, the same gutter the columns use, and the label headroom the
|
||||
// columns do NOT need (see `ROW_LABEL_HEADROOM`). The row step reads the
|
||||
// height the scaffold actually builds with — a plan's `height: 0` means
|
||||
// "size me from content", and `build_root_frame_node` presets that to the
|
||||
// device-class artboard, so both must ask `resolved_root_height` or the
|
||||
// rows would overlap by exactly the amount that preset adds.
|
||||
let per_row = boards_per_row(rf.width);
|
||||
let column_step = rf.width + SCREEN_GROUP_GAP;
|
||||
let row_step = resolved_root_height(rf.height, rf.width) + SCREEN_GROUP_GAP;
|
||||
let row_step =
|
||||
resolved_root_height(rf.height, rf.width) + SCREEN_GROUP_GAP + ROW_LABEL_HEADROOM;
|
||||
|
||||
for (index, group) in groups.iter().enumerate() {
|
||||
// Placeholder id: `{root_frame.id}-{screen}` — mirrors the deleted
|
||||
|
|
|
|||
|
|
@ -520,10 +520,13 @@ fn wide_screen_group_boards_wrap_onto_a_second_row() {
|
|||
);
|
||||
|
||||
// The fifth board is the wrap: back to the left edge, one board height
|
||||
// plus a gutter further down.
|
||||
// plus a gutter plus the frame-name label's headroom further down.
|
||||
assert_eq!(
|
||||
positions[4],
|
||||
(start_x, start_y + 1080.0 + SCREEN_GROUP_GAP),
|
||||
(
|
||||
start_x,
|
||||
start_y + 1080.0 + SCREEN_GROUP_GAP + ROW_LABEL_HEADROOM
|
||||
),
|
||||
"board 5 starts a new row: {positions:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -583,7 +586,7 @@ fn content_sized_boards_still_step_down_by_the_preset_height() {
|
|||
let positions = root_positions(nodes);
|
||||
assert_eq!(
|
||||
positions[4],
|
||||
(0.0, 900.0 + SCREEN_GROUP_GAP),
|
||||
(0.0, 900.0 + SCREEN_GROUP_GAP + ROW_LABEL_HEADROOM),
|
||||
"the desktop preset height drives the row step: {positions:?}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,10 +231,23 @@ def closing():
|
|||
return s
|
||||
|
||||
|
||||
# Gap between boards on the canvas, used on both axes. Without explicit
|
||||
# positions every frame defaults to the origin and the deck stacks into one
|
||||
# visible slide — the other five are underneath it (reported 2026-08-02).
|
||||
BOARD_GAP = 120
|
||||
# Horizontal gap between boards. Without explicit positions every frame
|
||||
# defaults to the origin and the deck stacks into one visible slide — the
|
||||
# other five are underneath it (reported 2026-08-02).
|
||||
BOARD_GAP_X = 120
|
||||
|
||||
# Vertical gap. Larger than the horizontal one because the canvas paints each
|
||||
# frame's name ABOVE the frame at a fixed screen-space offset that does not
|
||||
# scale with zoom (canvas_frame_labels.rs: baseline at sy-18, label box top at
|
||||
# sy-32). At the zoom where a 3x2 deck is framed to the screen (~0.14-0.22 —
|
||||
# the deck is 6000 doc px across, fitted into a 944-1424 px canvas region), a
|
||||
# 120 doc px row gap is only ~16-26 screen px, i.e. NARROWER than the label's
|
||||
# fixed 32 px footprint, so every second-row label sat on the bottom edge of
|
||||
# the row above (reported 2026-08-02). The extra 240 doc px reads as 33-52
|
||||
# screen px there, clearing the label with room to spare. Mirrors
|
||||
# `scaffold.rs::ROW_LABEL_HEADROOM`, which fixes the generated multi-screen
|
||||
# canvas the same way.
|
||||
BOARD_GAP_Y = BOARD_GAP_X + 240
|
||||
|
||||
# Boards per row. Six 1920px slides in one strip are ~12,000px across, which
|
||||
# can only be read by panning; wrapping them into a 3x2 grid matches the
|
||||
|
|
@ -247,8 +260,8 @@ BOARDS_PER_ROW = 3
|
|||
def build():
|
||||
boards = [cover(), agenda(), points(), metrics(), chart(), closing()]
|
||||
for index, board in enumerate(boards):
|
||||
board["x"] = (index % BOARDS_PER_ROW) * (W + BOARD_GAP)
|
||||
board["y"] = (index // BOARDS_PER_ROW) * (H + BOARD_GAP)
|
||||
board["x"] = (index % BOARDS_PER_ROW) * (W + BOARD_GAP_X)
|
||||
board["y"] = (index // BOARDS_PER_ROW) * (H + BOARD_GAP_Y)
|
||||
return boards
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue