Composite renders (multiple boards)

Some games need more than one independent board in a single SVG—for example chess-like variants with two play areas. These games emit a composite document (kind: "composite") instead of a plain APRenderRep.

Single-board games are unchanged: keep producing a normal render rep and call APRender.render().

Document shape

There is no composite-level legend; duplicate legend / glyphDefs on each panel as needed—the compositor prefixes SVG ids per panel to avoid collisions.

Cross-panel annotations

Optional top-level annotations draw move arrows, polylines, dots, or outlines in composite SVG space (layer #ap-composite-annotations, above all panels). Each target is a compositeCellRef: { "panel": "<panels[].id>", "row", "col" } (logical cell indices on that panel’s board, respecting panels[].rotate plus panels[].render.board.rotate — same combined rotation as a single-board render()).

Intent Where to author
Arrow entirely on one board panels[i].render.annotations (unchanged)
Arrow between panels top-level annotations

Supported types today: move, line, dots, outline. Square boards only (same constraint as composite boardClick). Requires showAnnotations: true on renderComposite (includes per-panel render.annotations).

Example: docs/samples/composite-cross-panel-arrow.json.

Each panel is measured once on a scratch SVG (no boardClick / boardHover) to obtain layout bounds and cross-panel annotation geometry, then cleared before the next panel. For the final composite, each panel is live-rendered into #ap-composite-panels with the same renderRep path as a single board: ids are prefixed per panel, and boardClick / boardHover are wrapped as (panelId, row, col, piece) so every clickable area (cells, areas, button bars, renderer-specific UI, etc.) behaves like standalone render(). Panel titles (panels[].label) are drawn in layout space above each board, separate from row/column labels on the board itself.

API

import { renderComposite } from "@abstractplay/renderer";

renderComposite(compositeJson, {
  divid: "drawing",
  showAnnotations: true,
  boardClick(panelId, row, col, piece) { /* ... */ },
  boardHover(panelId, row, col, piece) { /* e.g. stacking-expanding column */ },
});

Integrators (Abstract Play front, gameslib) must detect kind: "composite", call renderComposite instead of render, and route clicks with panelId — not covered in this package beyond the API above.

Schema: src/schemas/composite-schema.json (references APRenderComposite in the main schema $defs).

Examples: