Paint
scm-js/plugin-paint, a default — Tools ▸ Paint…,
Ctrl+Shift+P, or Paint… on
the map's right-click menu then opens a panel that floats
over the map (api.ui.panel). Pick a tool in it — freehand, line, rectangle, ellipse,
polygon, star, spray, text, eraser — and draw on the map; the brush is whatever the
active layer's palette has picked (api.palette.active() and api.terrain.active(),
refreshed on the "palette" and "layer" events): flat terrain or a tile, a doodad, a
unit for a player, a sprite, or fog for some players. Closed shapes can be filled (grid,
staggered grid, random), objects are spaced along the outline (auto = their own size) and
can be jittered, units and sprites can cycle through players 1–8 or take random ones, and
units can skip the spots the Units palette's placement checks refuse (tx.canPlaceUnit).
Every stroke is one api.document.edit.
shapes.ts is the pure part and that repository's tests/shapes.test.ts pins it: a tool's
drag becomes an outline Path (linePath, rectPath, ellipsePath, starPath, with
constrainSquare / constrainAngle for Shift and boxFromCenter for Alt); objects come
from samplePath (arc length, so corners are not doubled) plus fillPoints inside a
closed shape, then jitterPoints and dedupePoints; tiles and fog cells come from
strokeCells (a Bresenham walk dilated by a round brush of the panel's width) plus
fillCells (cells whose centre is inside); textCells lays a 5 × 7 dot font (font.ts)
out as cells, which become one object per dot or width × width tiles per dot. A
deterministic generator seeded on mouse-down makes the preview and the commit scatter the
same way. plugin.ts is the panel, the running api.ui.mapTool (the gesture per tool,
onCancel returning true to drop a shape and keep painting, draw for the dashed
outline, the per-point boxes in player colour, the eraser's crosses and the count), and
the transaction: stampTerrain / setTiles / setFog for cells, placeUnit /
placeSprite / placeDoodad for points, removeUnits / removeSprites / removeDoodads
for the eraser.