@doppelfun/gen
v0.1.0
Published
## Overview: Doppel documents and MML
Downloads
79
Readme
@doppelfun/gen
Overview: Doppel documents and MML
In Doppel, a block (one engine instance) holds documents — versioned blobs of MML (scene markup) that the engine parses into world entities. Agents and tools submit MML via the agent document API (create / update / append / delete); the server applies the markup and syncs entities into the Colyseus room.
MML is XML-style markup for 3D content: elements like <m-group>, <m-cube>, <m-model>, <m-grass>, <m-particle>, <m-attr-anim>, etc. Each entity should have a unique id; positions use x, y, z (meters). <m-model> references the block catalog via catalogId. The engine enforces size and triangle limits per document/block.
This package generates MML strings only — no HTTP, no document API. Callers use DoppelClient or their own HTTP flow to create/update/append documents; that is outside this package.
Generators in this package
| Generator | Export | Purpose |
|-----------|--------|--------|
| Pyramid | generatePyramidMml | Hollow stepped pyramid (m-cube shell, doorway, glowing corners). |
| City | generateCityMml | Street grid + seed buildings (m-model by catalogId), optional pyramid cell; roads get street lights (poles + emissive lamps) and ping-pong vehicles (m-attr-anim on x/z, collide="false"). |
| Grass | generateGrassMml | Multiple m-grass patches in block bounds; neon palette + optional emission (same style as engine create-agent-documents / loadtest). |
| Trees | generateTreesMml | Random m-model placements with catalogId (default def-tree); optional catalogIds array to rotate (e.g. pine/palm from fixtures). |
Core services are pure (no fetch, no process.exit): config in → MML string out.
Build
Gen depends on doppel-engine packages via file: paths. From doppel-sdk root:
pnpm install --no-frozen-lockfile
pnpm --filter @doppelfun/gen run buildUsage (library)
import {
generatePyramidMml,
generateCityMml,
runProceduralMml,
listProceduralKinds,
clampPyramidConfig,
clampCityConfig,
} from "@doppelfun/gen";
const pyramidMml = generatePyramidMml({ baseWidth: 30, layers: 15, seed: 99 });
const cityMml = generateCityMml({ gridRows: 6, gridCols: 6, seed: 42 });- Pyramid —
PyramidGenConfig:baseWidth,layers,blockSize,doorWidthBlocks,seed,cx,cz, optionalcornerColors(string[] — one color for all corners, or multiple mapped to the four perimeter corners / cycled by layer), optionalcornerEmissionIntensity(fixed intensity instead of random per corner). Ifseed/cx/czare omitted, they are randomized each run (pass explicit values for reproducible MML). City pyramid cell uses one random emissive color for all corners and placement jitter inside the cell, derived fromcitySeed+ cell bounds so the same city seed stays stable. - City —
CityGenConfig:gridRows,gridCols,blockSize,streetWidth,buildingSetback,seed, optionalpyramidRow/pyramidCol(seesrc/city/config.ts). - Grass —
GrassGenConfig:patches,count,spreadMin/spreadMax,height,y,seed,margin,emissionIntensity(seesrc/grass/config.ts). - Trees —
TreesGenConfig:count,catalogId, optionalcatalogIds[],seed,margin,collide(seesrc/trees/config.ts).
Use clampPyramidConfig / clampCityConfig on partial input before generating.
Dispatch: runProceduralMml(kind, raw) — raw.params holds kind-specific options (pyramid/city fields go inside params). listProceduralKinds() lists registered kinds (city, pyramid). Claw’s Zod tool schema normalizes common LLM variants (e.g. procedural-city → city) before calling here.
Adding a new generator
Open-source / external PRs: touch this package only. See CONTRIBUTING.md for the checklist. Claw’s generate_procedural already forwards kind + params to runProceduralMml; registering a new kind in src/procedural.ts is enough.
Anyone can add another procedural by following the same pattern as pyramid/ and city/:
- Add a folder under
src/<name>/with:config.ts— Export a config type,DEFAULT_*_CONFIG, andclamp*Config(partial)so callers and tools get safe bounds without duplicating validation.service.ts— Exportgenerate<Name>Mml(config): string(and helpers like*BlockCountif useful). No I/O inside — only sync logic, PRNG fromshared/prng.jsif needed.
- Re-export from
src/index.ts(types, defaults, clamp, main generator). - Register — append
{ kind: "<kind>", run: yourHandler }toPROCEDURAL_REGISTRYinsrc/procedural.ts. Claw does not need changes; bump gen version / dependency when shipping. - Wire other callers (optional) — scripts can still call your generator directly; gen stays unaware of documents.
- Dependencies — Prefer keeping new generators free of unpublished engine packages; if you need layout or catalog data, copy or inject via config from the caller (city uses
src/city/layout/locally).
Pyramid is the minimal template (config + service + shared PRNG only). City combines local layout + seed buildings with MML emission.
Model dimensions (GLB AABB)
Same approach as doppel-engine analyze-model-dimensions: glTF-Transform traverses the scene graph with node TRS so scaled roots (e.g. 0.01) yield true world-space size.
- API:
getModelDimensionsFromDocument(doc)— pass a@gltf-transform/coreDocumentfromNodeIO.read(url)orreadBinary(buffer)(registerKHRDracoMeshCompression+ draco decoder if GLB uses Draco). - Origin offsets:
dimensionsToOriginOffsets(dims)→{ originOffsetX, originOffsetZ }forDEFAULT_SEED_BUILDING_DIMENSIONS-style tables. - CLI:
pnpm run analyze-model-dimensions(frompackages/gen) — fetches eachSEED_BUILDINGSURL, prints JSON to stdout; pass URLs as args to analyse arbitrary GLBs.
Dependencies
- Pyramid — no engine runtime; only shared PRNG/helpers.
- City — uses
src/city/layout/(layout generator, seed buildings,BLOCK_SIZE_M) — vendored so@doppelfun/genpublishes without@doppel-engine.
Hub catalog: catalogEntriesToSeedBuildings(entries) maps hub/engine CatalogEntry[] into a building pool (category Buildings or known seed ids; otherwise any .glb with fallback dims). generateCityMml(cfg, { buildings }) uses that pool. Claw generate_procedural city prefetches the block catalog and passes params.buildings automatically when the catalog returns entries.
