Planning
Operations verticalPreviewMilitary map symbology for Tome sites — MIL-STD-2525 / APP-6 symbol and modifier registries, preset shortcuts, server-side symbol rendering, a reference grid, and collision-free label placement for mission-planning maps.
Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKENThen install:
npm install @wabbit/tome-planning
Overview
@wabbit/tome-planning
MIL-STD-2525 / APP-6 symbology for the Tome platform: a pre-rendered symbol registry, the modifier fragments that qualify those symbols, curated presets, a fixed reference grid, collision-aware label placement, and server-side symbol composition.
The package names the STANDARD, never a product. Its vocabulary is the standard's own — SIDC, symbol set, modifier slot 1 / slot 2, echelon, affiliation, amplifier — and it has no opinion whatsoever about what the symbols get drawn onto, who is allowed to draw one, or how a drawing is stored.
Why this exists
Rendering a military symbol correctly is a solved problem with an unsolved cost. `milsymbol` does the rendering and does it well, and it is 862 KB minified, 196 KB gzipped, and its manifest declares no sideEffects field, so a bundler must assume it has side effects and will not trim it and any client component that touches it ships the whole library.
This package's answer is to pay that cost once, at build time, and never at run time in a browser:
scripts/generate-symbols.cjsrenders a curated catalogue of 151 icons and 41 modifier fragments through the realmilsymbolat generate time and writes them tosrc/generated/symbols.tsas inline SVG. Each entry costs a few hundred bytes of path text, and path text gzips hard.- Every entry is VERIFIED, not trusted: the SIDC must resolve to a real icon (
validIcon), and no two entries may render the same drawing. Both checks exist becausemilsymbolanswers an unknown entity code with an EMPTY symbol rather than an error — so a wrong code silently produces a duplicate of some other entry's drawing, and the generator fails on the duplicate instead of shipping a picker full of blanks. - The one operation that genuinely needs the library at run time — composing an icon TOGETHER WITH its modifiers — lives behind a separate, server-only export.
Installation
pnpm add @wabbit/tome-planningNo peer dependencies. The package is self-contained: it does not depend on @wabbit/tome-core, on Payload, or on React.
Module surface
Four entry points, and the split between them is the package's central structural claim rather than a convenience.
| Entry | Safe in a client bundle | What it carries | |---|---|---| | @wabbit/tome-planning | yes | the registry, its shelving, presets, shared types | | @wabbit/tome-planning/grid | yes | the fixed reference grid | | @wabbit/tome-planning/labels | yes | label wrapping and collision placement | | @wabbit/tome-planning/compose | no — server only | symbol composition, and the milsymbol runtime dependency |
Root — registry, shelving, presets, types
import {
MAP_SYMBOLS,
MAP_MODIFIERS,
MAP_SYMBOL_KEYS,
MAP_SYMBOL_CATEGORIES,
MAP_SYMBOL_SETS,
symbolKeysByCategory,
symbolCountsByCategory,
modifierKeysFor,
SYMBOL_PRESETS,
getSymbolPreset,
matchPreset,
PLANNING_LAYER_VERSION,
type MapSymbol,
type MapModifier,
type MapSymbolCategory,
type SymbolPreset,
type ThreatLevel,
type SymbolSpec,
} from '@wabbit/tome-planning'MAP_SYMBOLS is keyed by a stable identifier that describes the ROLE (eq-machine-gun, site-landingpad) rather than the current label, because renaming a key orphans every mark that carries it. Each entry carries its label, its category, its set, the entity code it was generated from, and the viewBox + inner SVG to draw it in currentColor.
The set is load-bearing. The same two-digit modifier code means a different thing in a different symbol set, so a modifier is only ever valid on an icon from the SAME set, in the slot it claims. modifierKeysFor(symbolKey, slot) is the lookup that keeps a picker honest; composeSymbol re-checks the same rule on read, and matchPreset checks it for the bundled presets.
SYMBOL_PRESETS is the answer to "what is a modifier and why would I want one" — nine curated shortcuts that put down a correctly-composed symbol without anyone having to learn the vocabulary first. They are deliberately unaffiliated: the same emplacement is friendly or hostile depending on context.
ThreatLevel ('red' | 'amber' | 'green' | 'neutral') is the four-value rank a mark carries. The package owns this type so that consumers import it rather than the package importing theirs; map your own domain vocabulary onto it at the boundary.
/grid — a shared name for a region
import { cellForPoint, cellBounds, columnLabels, rowLabels, PLAN_GRID_COLUMNS } from '@wabbit/tome-planning/grid'
cellForPoint(0.5, 0.5) // → { column: 4, row: 4, ref: 'E5' }Also exported: PLAN_GRID_ROWS (8, alongside PLAN_GRID_COLUMNS), columnLabel(index) (0 → 'A'), and the GridCell type returned by cellForPoint.
A fixed 8×8 grid over a normalised 0–1 drawing space, with spreadsheet-style references. It claims NO distance semantics on purpose: backdrop imagery may have no fixed scale, so a measurement tool over it would produce numbers that look authoritative and are not. What it gives instead is a reference that is sayable over a radio and findable by everyone looking at the same drawing.
/labels — a label may move, a mark never does
import { placeLabels, wrapLabel, type LabelRequest } from '@wabbit/tome-planning/labels'
const { placed, hidden } = placeLabels(requests, { fontSize: 12, width, height, obstacles })Types: LabelRequest (in), PlaceLabelsOptions, LabelPlacement (the { placed, hidden } result, where placed maps a request id to a PlacedLabel), LabelSlot, and Box (for obstacles and a mark's body).
Pure placement arithmetic — no React, no DOM measurement; text width is estimated from a fixed monospace advance. Labels are offered a ladder of positions around their anchor (default, one rung up, below, right, left, then the far rungs) and one that will not fit is reported in hidden rather than drawn over its neighbour, because a drawing that silently stops mentioning things is a failure worth surfacing loudly. Each request carries rank: ThreatLevel, which decides who keeps the default slot in a collision; a selected request outranks everything.
/compose — server only
import { composeSymbol } from '@wabbit/tome-planning/compose'
const composed = composeSymbol('eq-machine-gun', 'm1eq-armoured', 'm2eq-wheeled')
// → { viewBox: '46 46 108 98', inner: '…' } — or null when there is nothing to composeThe result type is exported as ComposedSymbol ({ viewBox, inner }).
Never import this from a client component. The module's first line is import 'server-only', so a client bundle fails at build time instead of quietly inlining milsymbol. It is the only module in the package that can reach the library, and tests/milsymbol-unreachable.test.ts proves that against the built dist/ — by source scan, by export map, and by requiring each client entry in a clean Node process and inspecting its module graph.
Composition cannot be faked by overlaying separately-rendered fragments: when modifiers are present milsymbol SHRINKS AND REPOSITIONS the base icon to make room for them (a land-equipment icon with both slots filled becomes translate(55,55) scale(0.45)), and it emits the combined drawing as a whole. The library composes; a caller does not. composeSymbol returns null whenever there is nothing to compose — no symbol, no valid modifier, or a failure — so callers fall through to the pre-rendered registry icon, which is cheaper and already correct.
Regenerating the registry
pnpm --filter @wabbit/tome-planning generateThis rewrites src/generated/symbols.ts from the catalogue in scripts/generate-symbols.cjs. Two tests hold it honest: the generated key order must match the catalogue key order exactly, and a SHA-256 digest over the entry rows is pinned. When the digest flips intentionally, update the constant in the same commit as the regeneration and re-verify the counts by hand — a milsymbol version bump that changes emitted path data flips it too, and that is a change worth reading before accepting.
What this package deliberately does not do
No React. No Payload collection or field factory. No access model, no persistence, no rendering surface. What stays consumer-side: the drawing runtime and whatever the host calls its surface — a board, a kneeboard, a plan surface, an AO map, a tactical map — along with the authority model that decides who may draw (commander, group-lead, and any audience / visibleToGroups / compartment rules), and the schema that stores a mark; none of that is symbology, so none of it is here, and tests/vocabulary.test.ts asserts this is the only sentence in the package that says those words.
SymbolSpec ships as a TYPE for the symbology subset of a stored mark (type, symbol, mod1, mod2, echelon, affiliation) precisely so a consumer can model the rest itself. A field-group or collection factory is a documented trigger, not a plan: it lands when a SECOND consumer needs to persist a mark, at which point there is a real second shape to generalise against instead of a guess.
The bundled catalogue is likewise the only catalogue in v0.1. A consumer-supplied catalogue is the same kind of trigger — it lands when a second consumer needs a different symbol subset, and not before.
Development
pnpm --filter @wabbit/tome-planning build # tsup, per-file transpile (bundle: false)
pnpm --filter @wabbit/tome-planning test # vitest
pnpm --filter @wabbit/tome-planning typecheck # tsc --noEmitbundle: false is not a preference here. It is what keeps import 'server-only' at the top of the emitted compose files, and what guarantees the client entries' emitted output cannot acquire an import the source never wrote.
Exports
@wabbit/tome-planning@wabbit/tome-planning/grid@wabbit/tome-planning/labels@wabbit/tome-planning/compose
Changelog
775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
- 775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
5f84426: Initial release — MIL-STD-2525 / APP-6 symbology for Tome sites: a pre-rendered symbol registry, modifier fragments, curated presets, a fixed reference grid, collision-aware label placement, and server-side symbol composition. - **Root (`@wabbit/tome-planning`), client-safe:** `MAP_SYMBOLS` (151 pre-rendered icons) and `MAP_MODIFIERS` (41 modifier fragments), generated at build time by `scripts/generate-symbols.cjs`; category and symbol-set metadata with the set/slot rule a picker must obey (`MAP_SYMBOL_CATEGORIES`, `MAP_SYMBOL_SETS`, `symbolKeysByCategory`, `symbolCountsByCategory`, `modifierKeysFor`); nine presets (`SYMBOL_PRESETS`, `getSymbolPreset`, `matchPreset`); the shared `ThreatLevel` and `SymbolSpec` types. - **`./grid`:** a fixed 8×8 reference grid over a normalised 0–1 drawing surface, giving spreadsheet-style cell references (`C4`). - **`./labels`:** collision-aware label placement. - **`./compose`, server-only:** `composeSymbol`, the one module that loads `milsymbol` (it opens with `import 'server-only'`), so the renderer never reaches a client bundle. `sideEffects` names the compose artifacts so bundlers keep that guard. - No React runtime, Payload collection or access model ships in this release; those stay in the consuming app.
- 5f84426: Initial release — MIL-STD-2525 / APP-6 symbology for Tome sites: a pre-rendered symbol registry, modifier fragments, curated presets, a fixed reference grid, collision-aware label placement, and server-side symbol composition. - **Root (`@wabbit/tome-planning`), client-safe:** `MAP_SYMBOLS` (151 pre-rendered icons) and `MAP_MODIFIERS` (41 modifier fragments), generated at build time by `scripts/generate-symbols.cjs`; category and symbol-set metadata with the set/slot rule a picker must obey (`MAP_SYMBOL_CATEGORIES`, `MAP_SYMBOL_SETS`, `symbolKeysByCategory`, `symbolCountsByCategory`, `modifierKeysFor`); nine presets (`SYMBOL_PRESETS`, `getSymbolPreset`, `matchPreset`); the shared `ThreatLevel` and `SymbolSpec` types. - **`./grid`:** a fixed 8×8 reference grid over a normalised 0–1 drawing surface, giving spreadsheet-style cell references (`C4`). - **`./labels`:** collision-aware label placement. - **`./compose`, server-only:** `composeSymbol`, the one module that loads `milsymbol` (it opens with `import 'server-only'`), so the renderer never reaches a client bundle. `sideEffects` names the compose artifacts so bundlers keep that guard. - No React runtime, Payload collection or access model ships in this release; those stay in the consuming app.