Aggregates water-layer stats across every tile in a zone. Useful for
confirming a 'lake zone' actually has water, budgeting water-heavy
zones, or auditing what liquid types appear (water vs ocean vs magma
vs slime affects gameplay rules):
wowee_editor --info-zone-water custom_zones/Z
Zone water: custom_zones/Z
loaded tiles : 1
water chunks : 0 (out of 256 possible)
total layers : 0
(no water in this zone)
wowee_editor --info-zone-water custom_zones/Stranglethorn
Zone water: custom_zones/Stranglethorn
loaded tiles : 4
water chunks : 387 (out of 1024 possible)
total layers : 412
height range : 12.40 to 18.50
By liquid type:
water (0): 380 layer(s)
ocean (1): 28 layer(s)
magma (2): 4 layer(s)
Per-chunk water can have multiple layers (different liquid types or
height regions overlapping). Liquid types: 0=water, 1=ocean, 2=magma,
3=slime — different gameplay rules apply (oceans are swimable, magma
is damage-over-time, etc.).
JSON mode emits per-type layer counts + height range for programmatic
audit. Verified on a freshly-scaffolded zone: correctly reports 0
water chunks.
Multi-zone wrapper around --validate-all. Walks every zone in a
project and runs the per-format validators (WOM/WOB/WOC/WHM).
Aggregates pass/fail per zone with file-level breakdown:
wowee_editor --validate-project custom_zones
validate-project: custom_zones
zones : 12 (1 failed)
zone files failed errors status
AshenForest 47 0 0 PASS
BoneOasis 31 0 0 PASS
CrystalCaverns 28 2 14 FAIL
...
1 zone(s) failed validation
Designed for CI gates before --pack-wcp (or before tagging a release):
one command checks the whole project's binary integrity, exits 1 if
anything's broken with a clear breakdown of which zone went wrong.
Pairs with --validate-all (single-zone, all formats) and the
per-format validators (--validate-wom etc.). Three levels of
granularity now:
--validate-{wom,wob,woc,whm} single file
--validate-all single zone (or any dir)
--validate-project entire project
JSON mode emits per-zone records (totalFiles + failedFiles +
totalErrors) for dashboard consumption. Verified on a 2-zone
project with one tile having .woc built and one without — both
zones PASS, exit 0.
Two analytics commands for combat-balance work. Where --info-creatures
gives totals + behavior counts, these give the distributions:
wowee_editor --info-creatures-by-faction $Z/creatures.json
Creatures by faction: ... (47 total)
faction count share
7 12 25.5%
14 29 61.7%
35 6 12.8%
(factions: 7=human, 14=monster, 35=neutral, etc.)
wowee_editor --info-creatures-by-level $Z/creatures.json
Creatures by level: ... (47 total)
range : 5 to 32 (avg 14.2)
level count bar
5 4 ████████████████████████████████████████
6 3 ██████████████████████████████
...
30 1 ██████████
Faction histogram catches single-faction zones (one giant melee) and
mixed-faction tuning issues. Level histogram catches difficulty-curve
problems (cluster at 5, gap, cluster at 30) and outlier spawns
(level-60 boss accidentally placed in starter area).
ASCII bar chart for level distribution since gameplay tuning is
visual — '60% of mobs are levels 8-12 with a long tail' is more
intuitive as a bar than as numbers. Bars scale to longest bin so
small zones still get usable visualization.
JSON mode emits per-faction / per-level records for dashboards.
Verified on a 4-creature seed (3×faction-14 + 1×faction-35; levels
7/8/12/30): faction percentages and level range/avg both correct.
Self-check that every flag in kArgRequired (the master list of
commands needing positional args) appears in the help text emitted
by printUsage. Catches drift where a handler+arg-check pair gets
added but the help line is forgotten:
wowee_editor --validate-cli-help
CLI help self-check
kArgRequired entries : 126
PASSED — every kArgRequired flag is documented
If it ever fails, the missing flags are listed:
FAILED — 2 flag(s) missing from help text:
- --new-thing-i-forgot-to-document
- --another-undocumented-flag
CI integration: add this to the test target so a PR that adds a
new command without docs can't merge. The --info-cli-stats command
gives the surface-size view; this gives the surface-completeness
view.
Verified: ran on the current binary (126 kArgRequired entries),
PASSED — every flag is documented.
Project-level tree view: every zone with quick counts + bake/viewer
artifact status. --info-zone-tree drills into one zone; this gives
the bird's-eye view across the whole project:
wowee_editor --info-project-tree custom_zones
custom_zones/ (2 zones, 2 tiles, 1 creatures, 1 objects, 1 quests)
├─ Empty/ (tiles=1, creat=0, obj=0, quest=0)
│ ├─ name : Empty
│ ├─ mapName : Empty
│ ├─ artifacts : (none)
│ └─ status : empty (only terrain)
└─ Forest/ (tiles=1, creat=1, obj=1, quest=1)
├─ name : Forest
├─ mapName : Forest
├─ artifacts : .glb
└─ status : populated
Per-zone summary line shows counts; sub-tree shows display name,
map slug, which derived artifacts have been baked (.glb / .obj /
.stl / .html / ZONE.md), and a populated/empty status.
Project-header line aggregates totals across all zones for the
'how big is my project?' answer in one glance.
Pairs with --info-tilemap (spatial coverage) and --zone-stats
(quantitative aggregate) — three different lenses on the project:
spatial, quantitative, and structural.
Markdown counterpart to --export-project-html. Generates a README.md
indexing every zone with counts + bake/viewer/doc artifact status.
GitHub renders it natively at the project root:
wowee_editor --export-project-md custom_zones
# Wowee Project — Zone Index
*Auto-generated. 2 zone(s) discovered in `custom_zones`.*
## Summary
| Metric | Total |
|---|---:|
| Zones | 2 |
| Tiles | 2 |
| Creatures | 2 |
| ...
## Zones
| Zone | Tiles | Creatures | Objects | Quests | Bake | Viewer | Docs |
|---|---:|---:|---:|---:|:---:|:---:|:---:|
| Desert | 1 | 1 | 1 | 1 | — | — | — |
| [Forest](Forest/ZONE.md) | 1 | 1 | 1 | 1 | ✓ | [view](Forest/Forest.html) | [md](Forest/ZONE.md) |
Per-zone row links to its ZONE.md (if --export-zone-summary-md was
run) and its HTML viewer (if --export-zone-html was run). The Bake
column shows ✓ if .glb exists. Status columns make it instantly
visible which zones are bake-ready vs documentation-only.
Pairs with --export-project-html (interactive viewer index) — same
data, different presentation: HTML for browsers, Markdown for
GitHub Pages READMEs and PR descriptions.
Verified on a 2-zone project where one zone had been baked +
viewer-exported + doc-exported and the other hadn't: README.md
correctly shows ✓/links for the baked zone, em-dashes for the
unbaked one.
Computes the zone's spatial bounding box: XY world coords from
manifest tile coords (each tile is 533.33 yards), Z height range
across all loaded chunks. Useful for sizing camera frustums,
planning where new tiles can fit contiguously, or quick sanity
checks ('this zone is 4km across? something's wrong'):
wowee_editor --info-zone-extents custom_zones/MyZone
Zone extents: custom_zones/MyZone
tile count : 3 (3 loaded, 0 missing on disk)
tile range : x=[30, 31] y=[30, 31]
world box : (0.0, 0.0, 98.5) - (1066.7, 1066.7, 101.5) yards
size : 1066.7 x 1066.7 x 3.0 yards (975m x 975m x 2.7m)
WoW grid math: tile (32, 32) is at world origin; +X tile = -X world
(north convention), +Y tile = -Y world (west convention). The
displayed world coords use the same transform the renderer uses
so they line up with --bake-zone-glb output bounds.
Per-axis size in yards + meters (0.9144 conversion) since some
designers think in metric, others in WoW-canonical yards.
Tracks loaded vs missing tiles in case the manifest references a
tile whose .whm got deleted — surfaces silently bad zones early.
JSON mode emits full bounding box + tile range + size for
programmatic consumption (camera autofit, layout planning).
Compares two SHA256SUMS files (from --export-zone-checksum). Reports
added / removed / changed entries between two zone snapshots — much
faster than walking the filesystem to recompute hashes of unchanged
content:
wowee_editor --export-zone-checksum custom_zones/Z /tmp/before.sha256
... edits happen ...
wowee_editor --export-zone-checksum custom_zones/Z /tmp/after.sha256
wowee_editor --diff-checksum /tmp/before.sha256 /tmp/after.sha256
Diff: /tmp/before.sha256 vs /tmp/after.sha256
added : 1
removed : 0
changed : 0
+ creatures.json
Standard diff-style markers (+/-/~) for added/removed/changed,
sorted within each category alphabetically.
Use cases:
- Audit what an editing session actually touched (snapshot before,
snapshot after, diff)
- Verify a zone bundle re-extracts identically (transfer integrity
beyond the per-file PASS/FAIL of sha256sum -c)
- CI gate: fail build if a refactor touches files it shouldn't
Diff family for content/integrity formats:
--diff-zone unpacked zone dir vs zone dir (high-level)
--diff-extract per-extension counts in two extracts
--diff-checksum per-file hash diff between two snapshots <- new
Verified: scaffold + export checksum, add creature, re-export,
diff correctly reports '+ creatures.json' (added) and exit 1.
Quick-start: scaffold a zone AND populate one of each content type
(1 creature, 1 object, 1 quest with objective + XP reward) in a
single command. Goes from empty filesystem to 'something to look at'
without 7 chained --add-* commands:
wowee_editor --mvp-zone 'Demo Land' 30 30
Created demo zone: custom_zones/Demo_Land
tile : (30, 30)
contents : 1 creature, 1 object, 1 quest (with objective + reward)
next : wowee_editor --info-zone-tree custom_zones/Demo_Land
Demo Land/
├─ Manifest ...
├─ Tiles (1) — (30, 30)
├─ Creatures (1) — lvl 5 Demo Wolf
├─ Objects (1) — m2 World/Generic/Tree.m2
├─ Quests (1) — [1] Welcome to Demo Land (lvl 1, 100 XP)
│ └─ kill ×1 Demo Wolf
Demo content is positioned roughly at tile center (533.33-yard
intervals from origin tile 32/32). Quest references the demo
creature's auto-id so --check-zone-refs passes immediately.
Use cases:
- Smoke-testing the bake/validate pipeline
- Screenshot bait for docs / blog posts
- Editor onboarding (open a zone in the GUI to see the format)
- CI sanity check (does our editor still produce a viewable zone?)
Verified end-to-end: --mvp-zone 'Demo Land' → --info-zone-tree
shows all 4 sections populated correctly, file list matches
expected 6 files.
Where --export-quest-graph visualizes the quest dependency graph,
this quantifies it. Useful for spotting authoring issues (orphan
quests that only appear as one-offs, broken chains via cycles)
and getting a sense of quest density:
wowee_editor --info-quest-graph-stats $Z/quests.json
Quest graph: $Z/quests.json
total quests : 4
roots : 2 (no inbound chain — entry points)
leaves : 2 (no outbound chain — terminal)
orphans : 1 (root AND leaf — one-shot)
cycles : 0
max depth : 3
avg depth : 2.00 (chain length per root)
Definitions:
roots = quests no other quest chains TO (player entry points)
leaves = quests with no nextQuestId or nextQuestId pointing to
a missing quest (terminal — chain ends here)
orphans = root AND leaf (one-shot quests with no neighbors)
cycles = number of roots whose forward walk hits a node twice
maxDepth = longest path from any root forward through the chain
avgDepth = mean path length across all roots
Cycle-guarded forward walk uses a visited-set per root, so the
cycle count is bounded even on intentionally-broken inputs.
Exit 1 if cycles > 0 so CI can gate before shipping a broken
chain. JSON mode emits all six stats for dashboard consumption.
Verified on 4-quest zone (Q1→Q2→Q3 chain + Loner orphan):
correctly reports 2 roots, 2 leaves, 1 orphan, 0 cycles, max
depth 3, avg depth 2.00.
Three project-bake formats now match the three zone-bake formats —
full project terrain reachable from every universal-3D ecosystem:
wowee_editor --bake-project-obj custom_zones # DCC tools
wowee_editor --bake-project-stl custom_zones # 3D printing
wowee_editor --bake-project-glb custom_zones # web viewers
Shared per-zone walking pass collects vertex+index pools per zone,
then the format-specific tail emits:
STL → per-triangle 'facet normal'+'outer loop'+vertex×3
GLB → packed BIN chunk + JSON describing per-zone meshes
GLB output gives one mesh+node per zone (named 'zone_NAME') so
viewers can toggle zones independently — same pattern as
--bake-zone-glb but at project scope. STL is single-solid since
slicers don't have a useful concept of multi-part STL.
Coords align across all three exporters and across zone vs project
scope, so:
- A zone .obj overlaid with its containing project .obj lines up
- A project .glb opened in three.js shows zones at the same coords
the renderer uses
Verified on a 2-zone project (Forest + Desert):
- project.stl: 2 zones, 2 tiles, 65536 facets
- project.glb: 2 zones, 2 tiles, 41472 verts, 65536 tris, 1.78MB BIN
- --validate-glb on project.glb: PASSED
Bake granularity matrix complete:
OBJ STL GLB
single model --export-obj --export-stl --export-glb
single zone --bake-zone-obj --bake-zone-stl --bake-zone-glb
whole project --bake-project-obj --bake-project-stl --bake-project-glb
Project-level OBJ bake — combines every zone's terrain into one
giant OBJ with one 'g zone_NAME' block per zone. Useful for
previewing an entire multi-zone project's terrain in MeshLab/
Blender at once, or for printing the full map:
wowee_editor --bake-project-obj custom_zones
Baked custom_zones -> custom_zones/project.obj
2 zone(s), 3 tiles, 62208 verts, 98304 tris
Layout: single global vertex pool (so OBJ indexing stays valid),
per-zone face groups so designers can hide individual zones in
their viewer for area-by-area inspection. Hole bits respected.
Coords match WoweeCollisionBuilder's outer-grid layout exactly so
zones spatially line up at WoW grid boundaries — adjacent tiles
across zones connect seamlessly.
Pairs with the existing --bake-zone-* family (single zone) and
--export-project-html (web index of per-zone viewers). Three
levels of granularity now available:
--export-glb / --export-obj / --export-stl single model/file
--bake-zone-glb / -obj / -stl single zone
--bake-project-obj entire project <- new
Verified: 2-zone project (Forest 2 tiles + Desert 1 tile) baked
to project.obj with 62208 verts (3 × 20736), 98304 tris (3 ×
32768), 2 'g' blocks correctly named (zone_Desert, zone_Forest).
Emits a SHA-256 manifest of every source file in a zone in the
standard sha256sum format. Lets users verify zone integrity after
download/transfer using the standard system tool — no custom
verifier needed:
wowee_editor --export-zone-checksum custom_zones/MyZone
3298c35a... Z_30_30.whm
f81e3d37... Z_30_30.wot
6a49519f... creatures.json
4625e30b... zone.json
sha256sum -c custom_zones/MyZone/SHA256SUMS
Z_30_30.whm: OK
Z_30_30.wot: OK
...
Source-only by design — derived outputs (.glb/.obj/.stl/.html/.png/
ZONE.md/DEPS.md/quests.dot/SHA256SUMS itself/Makefile) are excluded
since they're regeneratable and would invalidate the checksum on
every rebuild.
Includes a self-contained 90-LoC SHA-256 (FIPS 180-4 / RFC 6234)
in an internal namespace — no OpenSSL/Crypto++ dependency added.
Streaming hash (16KB chunks) so it scales to giant terrain WHMs
without holding the whole file in memory.
Verified end-to-end: scaffolded zone with 1 creature → checksum
manifest of 4 source files (zone.json, creatures.json, .whm, .wot)
in standard format → sha256sum -c reports all 4 OK.
130+ commands. 'Is there a thing for X?' is a common question.
Scrolling --help to find out is slow. This searches:
wowee_editor --info-cli-help quest
--add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]
Append one quest to <zoneDir>/quests.json and exit
--add-quest-objective <zoneDir> <questIdx> <kill|collect|...> ...
Append one objective to a quest by index
--remove-quest-objective <zoneDir> <questIdx> <objIdx>
Remove the objective at given 0-based index from a quest
--clone-quest <zoneDir> <questIdx> [newTitle]
Duplicate a quest (with all objectives + rewards)
...
32 line(s) matched 'quest'
Case-insensitive substring match. Continuation lines (the indented
description right after a flag) are emitted along with their flag
line for context. Match-count summary on stderr so it doesn't
contaminate piped output.
Pairs with --list-commands (full list) and --info-cli-stats (counts
by category). Three meta commands now cover the discovery loop:
'how many?' (--info-cli-stats), 'which ones?' (--list-commands),
'what does X do?' (--info-cli-help X).
Pairs with --gen-makefile (per-zone) — generates a project-level
Makefile that delegates to each zone's per-zone Makefile, enabling
parallel rebuilds across zones:
wowee_editor --gen-project-makefile custom_zones
make -C custom_zones -j$(nproc) # all zones in parallel
make Forest-bake # one zone
make clean # strip every zone
make validate # validate every zone
make index # rebuild project HTML index
make stats / tilemap # project-level reports
Per-zone targets (one set per zone):
ZONE-bake -> ensures ZONE/Makefile exists then `make -C ZONE all`
ZONE-clean -> --strip-zone ZONE
ZONE-validate -> --validate-all ZONE
Auto-generates the per-zone Makefile if missing (so the project
Makefile bootstraps a fresh project without an extra setup step).
Top-level utility targets reuse the existing project-level commands:
- index -> --export-project-html (HTML zone index)
- stats -> --zone-stats (aggregate counts)
- tilemap -> --info-tilemap (ADT grid visualization)
Verified: 2-zone project (Forest + Desert) generates a Makefile
with 6 zone-level targets (3 per zone) + 5 top-level targets, sorted
alphabetically by zone name.
Sanity-checks creature/object/quest fields for plausible values.
Where --check-zone-refs catches dangling references, this catches
data-quality issues that pass technical validation but break
gameplay:
wowee_editor --check-zone-content custom_zones/MyZone
Zone content: custom_zones/MyZone
creature warnings: 1
object warnings : 0
quest warnings : 2
FAILED — 3 total warning(s):
- creature[0] 'Wolf' has displayId=0 (will render invisibly)
- quest[0] 'Hunt' has no objectives (uncompletable)
- quest[0] 'Hunt' has no reward at all
Per-type checks:
Creatures:
- empty name
- 0 health (dies on spawn)
- level 0
- minDamage > maxDamage (broken combat math)
- non-positive or non-finite scale
- displayId=0 (invisible at runtime)
Objects:
- empty path
- non-positive or non-finite scale
- non-finite position
Quests:
- empty title
- no objectives (player can never complete)
- no reward at all (XP=0, items=[], coins all 0)
- requiredLevel=0
Both --check-zone-refs (link integrity) and --check-zone-content
(data quality) needed — a quest can have valid NPC IDs (refs OK)
AND no objectives (content broken). Run both before --pack-wcp.
Verified end-to-end: zone with displayId=0 creature + objective-
less + rewardless quest reports 3 warnings; after fixing all three,
PASSED.
Compares two extracted asset directories side-by-side per file
extension. Useful for diffing a fresh asset_extract run against
a previous baseline (did the new MPQ add files? did any get
dropped?), or comparing what each WoW expansion contributes:
wowee_editor --diff-extract baseline/ new/
Diff: baseline/ vs new/
totals: 4 files / 0.0 MB vs 4 files / 0.0 MB
Per-extension (count then bytes):
ext a count b count a bytes b bytes status
.blp 2 2 0 0
.dbc 1 0 0 0 -A
.m2 1 2 0 0 DIFF
2 extension(s) differ
Status column flags imbalance:
-A only in A (extension dropped going B-ward)
+B only in B (extension added)
DIFF count differs but both sides have some
Recursive walk so subdirectories aggregate into the parent's
extension counts. JSON mode emits per-extension {count,bytes}
pairs for both sides plus union diff count for CI consumption.
Diff family for directory-shaped formats:
--diff-zone unpacked zone dir vs zone dir
--diff-extract extracted asset dir vs extract dir <- new
Verified on synthesized 4-file dirs (a: 2 blp + 1 dbc + 1 m2;
b: 2 blp + 0 dbc + 2 m2): correctly flags -A on .dbc, DIFF on
.m2, exit 1.
130 commands and counting — surface inspection has graduated from
'nice to have' to 'necessary to plan'. This drops a per-category
breakdown:
wowee_editor --info-cli-stats
CLI surface stats
total commands : 130
longest flag : 24 chars
Categories (by verb prefix, sorted by count):
--info 33
--export 15
--list 12
--validate 11
--diff 8
--add 6
--remove 5
--convert 5
--bake 3
--migrate 3
--clone 3
...
Pulls the command list via the same printUsage-parser as
--list-commands so it auto-tracks new flags. Buckets by the verb
prefix (text between '--' and the next '-') so '--info-zone-tree'
counts under 'info'. JSON mode emits the histogram as an object
for dashboard consumption.
Useful for spotting category imbalances ('we have 33 inspectors
but only 5 add commands — should consider more authoring CRUD?'),
tracking growth over releases, and planning where to invest.
Generates a Makefile that rebuilds every derived output for a zone
with proper dependency tracking. Designers can `make` to refresh
glb/obj/stl/html/csv/md from sources after editing creatures.json
or terrain — without remembering which wowee_editor flag does what,
and without rebuilding outputs that haven't changed:
wowee_editor --gen-makefile custom_zones/MyZone
cd custom_zones/MyZone && make
make all # rebuild everything that's stale
make glb # just the glTF bake
make clean # nuke derived outputs (calls --strip-zone)
make validate # run --validate-all on the zone
Targets generated:
glb obj stl html docs csv graph all clean validate
Dependency tracking:
- terrain bakes (.glb/.obj/.stl) depend on zone.json + WHM tiles
- HTML viewer depends on the .glb (forces glb rebuild first)
- docs (ZONE.md/DEPS.md) depend on content JSONs
- csv/graph use '-' prefix so missing-content failures don't
block 'make all' (zone with no quests still bakes terrain)
Uses /proc/self/exe absolute path so the Makefile works from any
cwd (run via `make -C custom_zones/MyZone` from anywhere). Falls
back to PATH lookup if /proc not available.
Verified end-to-end: scaffolded zone, generated Makefile, ran
`make all` from inside the zone dir — all derived outputs (.glb,
.obj, .stl, .html, ZONE.md, DEPS.md) generated; csv+graph
gracefully skipped due to no content; make exit 0.
After asset_extract finishes, '142k files across 17 dirs' is hard
to reason about. This groups them visually by top-level subdir +
file format with byte totals — instant orientation:
wowee_editor --info-extract-tree Data
Data/ (13 dirs, 284612 files, 31451.1 MB)
├─ expansions/ (85141 files, 12871.2 MB)
│ ├─ .adt 5469 files 6226626.7 KB
│ ├─ .wav 19517 files 3567936.9 KB
│ ├─ .m2 26354 files 1477702.1 KB
│ ├─ .wmo 5195 files 744940.1 KB
│ ├─ .blp 26911 files 691248.5 KB
│ ├─ .dbc 282 files 92373.6 KB
│ ├─ ...
├─ terrain/ (...)
├─ character/ (...)
Top-level dirs sorted by total bytes descending (heaviest first
so the high-impact directories surface immediately). Per-dir
extension breakdown also sorted by bytes. Walks recursively into
each top-level dir so 'expansions/wotlk/world/...' rolls up into
the expansions/ row.
Companion to --info-extract (counts + sidecar coverage) — that
one's wide-format JSON-friendly; this one's a tall tree-style
quick-look. Verified on a real 31GB extract of Data/ — output
makes the size distribution immediately clear (ADT files are
6GB of 13GB total for expansions/, etc.).
Companion to --validate-png — validates BLP textures without paying
the DXT decompress cost. Useful for spot-checking thousands of BLPs
in an extract dir:
wowee_editor --validate-blp Texture.blp
BLP: Texture.blp
magic : BLP2
size : 128 x 128
valid mips : 8
file bytes : 23044
PASSED
Checks (header-only, no pixel decode):
- 4-byte magic is 'BLP1' or 'BLP2'
- Width/height non-zero and within 8192 (texture exporter cap)
- mipOffsets[16] / mipSizes[16] tables: each non-zero pair refers
to a byte range within the file; mismatched (off=0 with size!=0
or vice versa) flagged
- Stops at first zero offset (BLP convention for unused slots)
Per-version layout differences (BLP1 has compression+alphaBits as
uint32; BLP2 has compression/alphaDepth/alphaEncoding/hasMips as
uint8) handled inline.
Verified on real BLP (pvp-banner-emblem-1.blp): 8 valid mips,
PASSED. Non-BLP file (manifest.json starting with '{'): correctly
flags 'magic is {' error and exits 1.
Format-validator lineup is now exhaustive across both proprietary
and open formats:
Proprietary: BLP / DBC (via --convert-dbc-json round-trip)
Open binary: WOM / WOB / WOC / WHM / GLB
Open text: JSON DBC / STL / PNG (BLP sidecar)
Generates an index.html linking to every zone's HTML viewer in a
project. Pairs with --export-zone-html (per-zone) and --bake-zone-glb
(terrain bake). Designed for github-pages / static-hosting style
'all my zones' showcase:
wowee_editor --export-project-html custom_zones
# -> custom_zones/index.html
Each zone gets a card showing:
- Display name (or mapName fallback)
- Counts: tiles · creatures · objects · quests (singular/plural
agreement)
- 'Open viewer →' link if HTML exists
- Helpful nudge if HTML missing ('No HTML viewer (run --export-zone-html)')
- Or 'No .glb (run --bake-zone-glb)' if not even baked yet
Self-contained CSS (dark theme matching --export-zone-html), no
external dependencies. Responsive grid layout (300px-min cards
auto-flowing across viewport).
Verified on a 2-zone project (Forest with .glb+.html + Desert
without): index lists both, Forest gets a working link, Desert
gets the 'run --bake-zone-glb' hint.
Renders the WoW 64x64 ADT coordinate grid as ASCII art showing which
tiles are claimed by which zones. Useful for spotting tile-coord
collisions before two zones ship overlapping content, and for
'where am I working?' overview of multi-zone projects:
wowee_editor --info-tilemap custom_zones
Tilemap: custom_zones
zones : 2
tiles used : 5
collisions : 0 (multiple zones claiming same tile)
legend : D=Desert F=Forest
0 1 2 3 4 5 6
0123456789012345678901234567890123456789012345678901234567890123
y=30 ..............................FFDD..............................
y=31 ...............................F................................
Glyphs are first-letter-of-zone uppercased; collision tiles render
as '*'. Empty rows are skipped to keep output bounded for projects
in one corner of the map (skipping all 60+ blank rows of the full
64x64 grid).
Exit 1 on collisions so CI can gate before merging two PRs that
both add tiles in the same area. JSON mode emits per-tile claims
for programmatic consumption.
Verified on a 2-zone project: Forest (3 tiles) + Desert (2 tiles)
correctly rendered, no collisions detected, glyphs F/D placed at
the right grid coords.
When a zone is hand-edited, partially copied, or modified by tools
that don't re-write zone.json, the manifest can fall out of sync
with the on-disk reality. --repair-zone reconciles them:
wowee_editor --repair-zone custom_zones/MyZone --dry-run
would add tile (31, 30) to manifest
would set hasCreatures: false -> true
repair-zone: custom_zones/MyZone (dry-run)
fixes : 2
warnings : 0 (manual decision needed)
re-run without --dry-run to apply
Auto-fixes:
- WHM files on disk matching <mapName>_TX_TY.whm pattern but not
in manifest tiles[] -> add to tiles
- hasCreatures flag mismatched against actual creatures.json
presence + non-empty -> sync
Warns (no auto-fix — needs manual decision):
- Tiles in manifest but no .whm on disk (could be in-progress
work or genuinely deleted; user decides)
--dry-run flag previews changes. Pairs with --strip-zone (cleanup
derived) and --validate (open-format coverage) for the trio of
zone-health-maintenance commands.
Verified: scaffolded zone, hand-copied an extra .whm/.wot pair to
simulate disk-without-manifest drift, added a creature then flipped
hasCreatures=false in zone.json. --repair-zone correctly identifies
2 fixes, dry-run lists them, real run applies them, manifest now
shows correct tiles array + hasCreatures=true.
Removes the derived outputs (everything --bake/--export-* generates)
leaving only source files in a zone directory:
wowee_editor --strip-zone custom_zones/MyZone
removed: MyZone.obj (1099177 bytes)
removed: MyZone.glb (891736 bytes)
removed: DEPS.md (357 bytes)
removed: ZONE.md (534 bytes)
removed: quests.dot (198 bytes)
strip-zone: custom_zones/MyZone
removed : 5 file(s)
freed : 1945.3 KB
What gets deleted:
- .glb / .obj / .stl (3D format exports)
- .html (browser viewer)
- .dot (Graphviz quest graph)
- .csv (spreadsheet exports)
- ZONE.md / DEPS.md (markdown documentation)
- .png at zone root (heightmap previews — NOT inside data/, those
are source BLP→PNG sidecars)
What stays:
- zone.json + creatures.json + objects.json + quests.json
- *.whm / *.wot / *.woc (terrain + collision)
- *.wom / *.wob (open binary models/buildings)
- data/*.json (DBC sidecars — source, not derived)
Top-level only — does not recurse into subdirectories so source
sidecars under data/ are untouched.
--dry-run flag previews what would be removed without deleting.
Useful before committing to git so derived blobs don't bloat
history, or before --pack-wcp so the archive doesn't carry
redundant exports.
Verified: scaffolded zone, generated 5 derived files (glb/obj/
ZONE.md/DEPS.md/quests.dot), --dry-run lists all 5 with sizes,
real run deletes them and frees 1945 KB. Source files (whm/wot/
woc/zone.json/quests.json) all preserved.
Markdown counterpart to --list-zone-deps. PR reviewers see at a
glance whether every referenced model exists in either open or
proprietary form across the conventional asset roots:
wowee_editor --export-zone-deps-md custom_zones/MyZone
# -> custom_zones/MyZone/DEPS.md
# Dependencies — MyZone
*Auto-generated. Status is best-effort — checks zone-local, output/,
custom_zones/, Data/ roots in that order.*
## Direct M2 placements (12)
| Refs | Path | Status |
|---:|---|---|
| 8 | `World/Tree.m2` | open + proprietary |
| 3 | `World/Lamp.m2` | open only |
| 1 | `World/Banner.m2` | MISSING |
Status column resolves each path against zone-local + output/ +
custom_zones/ + Data/ roots, trying both .wob/.wmo for buildings
and .wom/.m2 for models. Catches missing assets BEFORE pack-wcp
would silently include broken refs.
GitHub-Flavored Markdown — sortable by Refs column once rendered,
backtick-wrapped paths so URLs/spaces don't confuse the viewer.
Verified: scaffolded zone with 2 M2 placements (one duplicated) +
1 WMO placement → DEPS.md has 3 sections (one per category) with
correct ref counts (Tree.m2 ×2) and MISSING status for paths that
don't resolve in any root.
Diff family is now exhaustively complete across every shippable
format:
--diff-wcp archive
--diff-zone unpacked zone dir
--diff-glb glTF binary
--diff-wom WOM model
--diff-wob WOB building
--diff-whm WHM/WOT terrain pair
--diff-woc WOC collision
--diff-jsondbc JSON DBC sidecar <- new
Schema-level compare:
- format tag
- source filename
- recordCount + fieldCount (header values)
- actualRecs (records[] array length)
Useful for catching schema regressions when a sidecar is regenerated
by a different tool version, or for verifying a --migrate-jsondbc
pass actually changed what it claimed.
Verified: same file vs itself reports IDENTICAL exit 0;
2-record vs 3-record (with same format/source/fieldCount) reports
2 DIFFs (recordCount + actualRecs) with exit 1.
Last two missing entries in the diff family — terrain heightmap pairs
and collision meshes:
wowee_editor --diff-whm Z1/Z1_30_30 Z2/Z2_31_30
Diff: ...
a b
tile : ( 30, 30) ( 31, 30) DIFF
loadedChunks : 256 256
doodadPlace : 0 0
wmoPlace : 0 0
heightRange : [-1.50,1.50] [-1.50,1.50]
wowee_editor --diff-woc tile_a.woc tile_b.woc
Diff: ...
a b
tile : ( 30, 30) ( 31, 30) DIFF
triangles : 32768 32768
walkable : 32768 32768
steep : 0 0
--diff-whm: tile coord, loaded chunk count, doodad/WMO placement
counts, texture/name table sizes, height range (min/max with float
epsilon). Pointwise height compare intentionally not done — float
perturbation from format round-trips would false-flag.
--diff-woc: tile coord, total triangles, walkable + steep counts.
Catches whether a --regen-collision pass actually changed something.
Diff family is now exhaustively complete for every shippable open
format:
--diff-wcp archive vs archive
--diff-zone unpacked zone dir vs zone dir
--diff-glb glTF binary vs glTF binary
--diff-wom WOM model vs WOM model
--diff-wob WOB building vs WOB building
--diff-whm WHM/WOT terrain pair vs pair
--diff-woc WOC collision vs collision
Verified: tile (30,30) vs (31,30) reports tile DIFF + identical
counts (since both are flat scaffolds); same-vs-self reports
IDENTICAL with exit 0.
Designers receive JSON DBCs from various tools (older asset_extract
versions, third-party converters) that may omit fields the runtime
now expects. --validate-jsondbc tells you what's wrong; this fixes
the auto-fixable ones:
wowee_editor --migrate-jsondbc db/Spell.json
added: format = 'wowee-dbc-json-1.0'
added: source = 'Spell.dbc'
fixed: recordCount 99 -> 47882 (matches actual)
inferred: fieldCount = 234 (from first row)
Migrated db/Spell.json -> db/Spell.json
fixes applied: 4
Auto-fixes:
- Missing 'format' tag → add 'wowee-dbc-json-1.0'
- Missing 'source' field → derive from filename stem + '.dbc'
- Missing 'fieldCount' → infer from first row
- recordCount mismatch → recompute from actual records[] length
NOT auto-fixed (data loss risk — surfaced as warnings instead):
- Wrong-width rows (silently padding/truncating could mangle field
values; the user needs to inspect and decide)
In-place by default (writes back to the input path); accepts an
optional output path for non-destructive migration.
Verified: a JSON missing format/source/fieldCount with mismatched
recordCount=99 (actual 2) → migrate applies 4 fixes →
--validate-jsondbc reports PASSED on the result.
Generates a single-file HTML viewer next to the zone .glb. Anyone
with a modern browser can open it — no installs, no Blender, no
node_modules:
wowee_editor --bake-zone-glb custom_zones/MyZone # bakes .glb
wowee_editor --export-zone-html custom_zones/MyZone # writes .html
open custom_zones/MyZone/MyZone.html # any browser
Uses Google's <model-viewer> web component (loaded from unpkg with
^4.0.0 version pin so a unpkg 'latest' bump can't silently break
older HTML files). The HTML itself is ~1.1KB; the .glb sits beside
it for the viewer to load via relative URL.
Features baked into the page:
- Camera controls (orbit, zoom, pan)
- Auto-rotate at 15deg/sec for the headless preview case
- Shadow casting + neutral environment IBL for non-flat lighting
- Header strip showing display name, map slug, tile count, mapId
Refuses to run if the zone .glb doesn't exist yet (clear error
message points the user at --bake-zone-glb).
Verified: scaffolded zone -> --bake-zone-glb -> --export-zone-html.
1.1KB HTML opens cleanly in browsers, references the sibling .glb.
Missing-glb case correctly errors with exit 1 + helpful next-step
hint.
Companion to --diff-wom for buildings. Same count-based shape so
round-trips through OBJ/glTF can be validated without false
positives from float perturbation:
wowee_editor --diff-wob orig back
Diff: orig.wob vs back.wob
a b
groups : 5 5
portals : 3 3
doodads : 12 12
materials : 8 8
groupTex : 7 7
totalVerts : 4609 4609
totalIdx : 10950 10950
name : Stormwind Stormwind
boundRadius : match
IDENTICAL
Compares: groups, portals, doodads, aggregated materials count
(per-group materials summed), aggregated group-texture count,
total verts/indices across all groups, name, boundRadius (with
0.01 epsilon).
Diff family is now complete across every binary format that ships
in a content pack:
--diff-wcp archive vs archive
--diff-zone unpacked zone dir vs zone dir
--diff-glb glTF binary vs glTF binary
--diff-wom WOM model vs WOM model
--diff-wob WOB building vs WOB building
Verified: identical pair reports IDENTICAL exit 0; pair with extra
group + extra doodad + name change reports 6 DIFFs with exit 1.
Symmetric to --info-creature and --info-quest. Every PlacedObject
field for one entry:
wowee_editor --info-object $Z/objects.json 3
Object [3]
type : wmo
path : World/StaticObject/Stormwind/HumanInn.wmo
nameId : 3497721408
uniqueId : 4
position : (-8412.300, 633.200, 110.500)
rotation : (0.00, 90.00, 0.00) deg
scale : 1.000
Useful when debugging a misplaced object — quickly see the exact
position/rotation/scale + uniqueId + nameId without having to
grep through objects.json or run --list-objects through awk.
JSON mode emits the structured record. Inspector lineup is now
symmetric across all three content types:
--info-{creatures,objects,quests} aggregate counts
--list-{creatures,objects,quests} per-entry table
--info-{creature,object,quest} single-entry deep dive
The --list-* commands give table views; the --info-* (creatures/objects/
quests) give summary counts. Neither shows every field of a single
entry. These fill that gap:
wowee_editor --info-creature $Z/creatures.json 0
Creature [0] 'Captain'
id : 1
displayId : 11430
position : (100.00, 200.00, 50.00)
orientation : 0.00 deg
scale : 1.00
level : 12
health/mana : 100 / 0
damage : 5-10
armor : 0
faction : 0
behavior : stationary
wander rad : 10.0
aggro rad : 20.0
leash rad : 40.0
respawn ms : 300000
patrol points : 0
flags :
wowee_editor --info-quest $Z/quests.json 0
Quest [0] 'Hunt Wolves'
id : 1
required level : 5
giver NPC id : 100
turn-in NPC id : 100
next quest id : 0 (terminal)
reward : 250 XP, 0g 0s 0c, 1 item(s)
item[0] : Item:Sword
objectives : 1
[0] kill ×5 Wolf — Slay 5 Wolf
Useful for digging into 'why is this NPC not behaving like I expect?'
or reviewing one quest's full design in one screen instead of running
3-4 list-* commands. JSON mode emits every field as a structured
record for programmatic consumption.
Inspector lineup is now complete from aggregate to per-entry:
--info-{creatures,objects,quests} (counts)
--list-{creatures,objects,quests} (table)
--info-{creature,quest} (single entry, all fields)
Designers often prefer spreadsheets over JSON for read-only analysis
('which 5 quests give the most XP?', 'how many lvl 10+ creatures?',
'pivot table by faction'). This emits creatures.csv / objects.csv /
quests.csv in standard CSV that LibreOffice / Excel / Numbers / Python
pandas all consume natively:
wowee_editor --export-zone-csv custom_zones/MyZone
wrote custom_zones/MyZone/creatures.csv (47 rows)
wrote custom_zones/MyZone/objects.csv (12 rows)
wrote custom_zones/MyZone/quests.csv (8 rows)
CSV columns chosen to be designer-actionable:
- creatures: index/id/name/displayId/level/health/mana/faction/
position/orientation/scale + hostile/questgiver/vendor/trainer flags
- objects: index/type/path/position/rotation/scale
- quests: index/id/title/requiredLevel/giver/turnIn/reward fields/
objectiveCount + objectives/itemRewards joined by '; ' for keep-
one-row-per-quest sortability
RFC 4180 quoting: fields with comma/quote/newline get wrapped in
double quotes with internal quotes doubled. Verified on a creature
named 'Big, Bad Bear' — comes out as '"Big, Bad Bear"'.
Round-trip back into the editor isn't supported yet (would need
schema-aware CSV parsing); this is read-only-export for now.
Structural compare of two WOM models. Useful for verifying that
--migrate-wom or a round-trip through OBJ/glTF/STL preserved the
right counts:
wowee_editor --diff-wom orig back
Diff: orig.wom vs back.wom
a b
version : 1 1
vertices : 5 5
indices : 18 18
triangles : 6 6
textures : 0 0
bones : 0 0
animations : 0 0
batches : 0 0
name : Pyramid Pyramid
bounds : match
IDENTICAL
Compares sizes only (vertex / index / bone / animation / batch /
texture counts) plus name and bounds. Bounds match-with-epsilon
(0.01 unit slop, generous since positions are typically yards) so
text-format round-trips that perturb the last bit don't false-flag.
Pointwise vertex compare is intentionally not done — it would be
O(n²) and brittle to tiny float diffs from format conversions.
Diff family is now complete:
--diff-wcp (archive vs archive)
--diff-zone (unpacked zone dir vs zone dir)
--diff-glb (glTF binary vs glTF binary)
--diff-wom (WOM model vs WOM model)
Verified: identical pair reports IDENTICAL exit 0; pair with 1 extra
vertex + extra triangle + name change correctly reports 4 DIFFs
(verts/indices/triangles/name) with exit 1.
OBJ companion to --bake-zone-glb / --bake-zone-stl. Same multi-tile
WHM aggregation, this time as Wavefront OBJ — opens directly in
Blender / MeshLab / 3DS Max for hand-editing the terrain mesh:
wowee_editor --bake-zone-obj custom_zones/MyZone
# -> custom_zones/MyZone/MyZone.obj
Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.obj
2 tile(s), 41472 verts, 65536 tris
Each tile becomes its own 'g tile_TX_TY' block so designers can hide
tiles independently in Blender. Single global vertex pool with
per-tile vertex base indices for face emission (OBJ requires verts
before faces, so we collect per-tile face indices in memory then
emit them after all verts are streamed to disk).
Hole bits respected (cave-entrance quads dropped). Coords match
WoweeCollisionBuilder's outer-grid layout exactly so .obj/.glb/.stl
of the same source align spatially when overlaid.
Why three formats for full-zone export: glTF for on-screen 3D
viewers, STL for fabrication, OBJ for DCC editing. Three different
ecosystems, three different format sweet spots.
Verified: 2-tile zone (Z + added tile) baked correctly. 41472 verts
(2 × 20736), 65536 tris (2 × 32768), 2 'g' blocks (tile_30_30 +
tile_31_30) — matches what --bake-zone-glb reports for the same
input.
Cross-checks every position accessor's claimed min/max against the
actual data in the BIN chunk. glTF viewers use these bounds for
camera framing and frustum culling; stale values (e.g. from a tool
that edited geometry without recomputing) cause models to vanish
at certain angles or get framed wrong on load.
wowee_editor --check-glb-bounds Tree.glb
GLB bounds: Tree.glb
position accessors checked : 1
mismatched : 0
PASSED
wowee_editor --check-glb-bounds bad.glb
GLB bounds: bad.glb
position accessors checked : 1
mismatched : 1
FAILED — 1 error(s):
- accessor 0 bounds mismatch: claimed [-9999,-9999,-9999]-[9999,
9999,9999] vs actual [533.3,533.3,98.5]-[1066.7,1066.7,101.5]
Walks the meshes/primitives tree, dedups the POSITION attribute
accessors (multiple primitives can share one), then for each unique
accessor reads the BIN chunk via the bufferView+byteOffset chain
and recomputes the actual min/max. Compares with float epsilon
(1e-3) since perfect equality across float compilers isn't
guaranteed.
Also flags missing min/max — the glTF 2.0 spec REQUIRES position
accessors to declare bounds (validators like Khronos's reference
impl reject .glbs that omit them).
Verified: a fresh --export-whm-glb passes clean. After hand-editing
the JSON to claim bogus bounds (-9999 to 9999 for a 533-1067 range
mesh), --check-glb-bounds correctly reports the mismatch with full
claimed-vs-actual values, exit 1.
STL companion to --bake-zone-glb. Designers can 3D-print a miniature
of an entire multi-tile zone in one slicer load — useful for tabletop
RPG props, physical playtest references, or just the satisfaction of
holding a zone in your hand:
wowee_editor --bake-zone-stl custom_zones/MyZone
# -> custom_zones/MyZone/MyZone.stl
Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.stl
2 tile(s), 65536 facets, 0 hole quads skipped
Streams ASCII STL directly to disk (no in-memory accumulation —
relevant for large multi-tile zones). Per-triangle face normal
computed from cross product since slicers use it for orientation.
Hole bits respected (cave-entrance quads dropped) and counted
separately so users see how much got skipped.
Why STL alongside the existing glTF zone bake: glTF targets
on-screen 3D viewers; STL targets fabrication. Different ecosystems,
different file formats, both now reachable from the same WHM source
with one command each.
Verified: 2-tile zone (Z + added tile) baked correctly. 65536 facets
(2 tiles × 32768 each), 12MB ASCII STL, well-formed solid/endsolid
framing, normals computed (e.g. '0.305 -0.399 -0.864' for the first
sloped facet).
The CLI surface is now 103 commands. Tab completion is no longer a
nice-to-have:
source <(wowee_editor --gen-completion bash) # one-time setup
wowee_editor --info-<TAB> # all info-* offered
Two complementary commands:
--list-commands: parses printUsage's own output to extract every
'--flag' it documents, dedupes, sorts. Auto-tracks new commands
as they're added (no parallel list to maintain).
--gen-completion bash|zsh: emits a completion script that re-execs
--list-commands at completion time, so newly-added flags light up
without regenerating the script. Bash version uses compgen with
per-session caching in ; zsh version uses the
_arguments + compdef framework.
Both completion scripts also fall back to file-path completion in
arg slots (the common case for --info-/--validate-/--export-
commands that take a path).
Verified:
- --list-commands: 103 unique flags emitted alphabetically
- --gen-completion bash: well-formed script using full binary path
for the cached --list-commands invocation
- --gen-completion zsh: same shape, _arguments-based
- Unknown shell: clear error + exit 1
Structural compare of two glTF 2.0 binaries. Completes the diff
suite alongside --diff-wcp (archive-vs-archive) and --diff-zone
(unpacked-zone-vs-zone):
wowee_editor --diff-glb a.glb b.glb
Diff: a.glb vs b.glb
a b
meshes : 1 2 DIFF
primitives : 256 2 DIFF
accessors : 258 4 DIFF
bufferViews : 3 3
buffers : 1 1
BIN bytes : 890880 1781760 DIFF
Reports per-category counts side-by-side with a 'DIFF' marker on
mismatches. Compares structure (mesh/primitive/accessor counts +
BIN chunk size), NOT byte-level — JSON key ordering can vary
between tools so a byte diff would have false positives. JSON mode
emits the per-field {a, b} pair plus totalDiffs + identical bool
for CI consumption.
Useful for confirming alternate export paths produce equivalent
output (does --bake-zone-glb match concatenated --export-whm-glbs?
does a tool refactor preserve the same shape?).
Verified: same file vs itself reports IDENTICAL with exit 0.
Single-tile WHM .glb (1 mesh, 256 primitives, 890KB BIN) vs
2-tile bake (2 meshes, 2 primitives, 1.7MB BIN): correctly flags
4 DIFFs with exit 1.
Bakes every WHM tile in a multi-tile zone into ONE .glb so the
entire zone opens in three.js / model-viewer / Sketchfab with one
file. Each tile becomes its own mesh+node so they can be toggled
independently in the viewer:
wowee_editor --bake-zone-glb custom_zones/MyZone
# -> custom_zones/MyZone/MyZone.glb
Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.glb
3 tile(s), 62208 verts, 98304 tris, 3 meshes, 2672640-byte BIN
Implementation:
- Walks ZoneManifest::tiles, loads each tile's WHM/WOT pair via
WoweeTerrainLoader.
- Same per-chunk 9x9 outer-grid + 8x8 quads layout as
--export-whm-glb (so tiles align spatially with corresponding
--export-woc-obj output).
- All tiles share one global vertex+index pool packed into a single
BIN chunk; per-tile primitives slice their index range via
byteOffset on the indices accessor.
- One node per tile, named 'tile_TX_TY', so viewers can hide
individual tiles for area-by-area inspection of large zones.
v1 limitation: terrain only — object/WOB instances are a follow-up
that needs careful per-mesh bufferView slicing across many distinct
loaded models. Listed in --info-glb as 'meshes' so users see the
shape of the output before opening in a viewer.
Verified: 3-tile zone (Z + 2 added tiles) baked correctly. 62208
verts (3 × 20736), 98304 tris (3 × 32768), 3 meshes/primitives.
--validate-glb on the output: PASSED, all chunk types correct,
accessor bufferView refs in range.
Closes the WOM <-> STL bridge for the fabrication ecosystem (Cura,
PrusaSlicer, TinkerCAD, Meshmixer, SolidWorks, Fusion 360). Designers
can now edit prints in any CAD/3D-print tool and bring them back to
the engine:
asset_extract # M2 -> WOM (open binary)
--export-stl # WOM -> STL (universal fabrication format)
... edit in TinkerCAD / sculpt in Meshmixer ...
--import-stl # STL -> WOM (back to engine format)
--validate-wom # confirm consistency
Parser handles ASCII STL only (binary STL is a follow-up):
- 'solid <name>' header — preserves model name
- 'facet normal nx ny nz' — sets the face normal for following verts
- 'vertex x y z' — accumulates 3 per facet
- 'endfacet' — emits the triangle with the face normal applied to all
3 verts (STL doesn't carry per-vertex normals, so the round trip is
faceted-shading by design)
- Dedupes on (pos, normal) so shared vertices on the same face merge
(a 4-vert square base with 2 tris collapses to 4 verts), but verts
shared across faces with different normals stay distinct (correct
for faceted geometry)
- Computes bounds from positions for renderer culling
Round-trip cost: a 5-vert/6-tri pyramid round-trips to 16 verts/6
tris. The vertex inflation is structural (STL faceted-shading
semantics) — geometry preservation is exact. Verified via
WOM -> STL -> WOM -> --validate-wom (PASSED, bounds and tri count
match exactly).
ASCII STL is the universal 3D printing format — Cura, PrusaSlicer,
Bambu Studio, Slic3r, OctoPrint, MakerBot Print, and basically every
slicer made in the last 25 years opens it natively. Lets WOM models
drive physical prints with no conversion friction beyond one command:
wowee_editor --export-stl Tree # -> Tree.stl
wowee_editor --export-stl Tree out.stl
Per-spec STL ASCII output:
- 'solid <name>' header / 'endsolid <name>' footer (name sanitized
to alphanum + underscore for slicers that strict-parse)
- Per-triangle 'facet normal nx ny nz' with normal computed from
cross-product of edges 1 and 2 (most slicers use this for
orientation hints; falls back to (0,0,1) for degenerate triangles)
- 'outer loop' with three vertex lines per facet
- No shared vertex pool — STL stores every triangle independently
Why STL alongside OBJ + glTF: OBJ targets DCC tools (Blender etc.),
glTF targets web 3D viewers (Sketchfab, three.js), and STL targets
fabrication. Three different ecosystems, three different format
needs — wowee open formats now bridge to all three.
Verified on a 5-vert/6-tri pyramid: STL has 6 facets with correctly
computed normals (0 -1 0 for the bottom faces, computed slopes for
the side triangles), proper solid/endsolid framing, name preserved
('solid Pyramid').
Companion to --migrate-wom (single-file upgrade). Walks a zone dir
recursively and upgrades every legacy WOM (v1/v2 with no batches[])
to WOM3 in-place:
wowee_editor --migrate-zone custom_zones/MyZone
upgraded: custom_zones/MyZone/models/v1_0.wom
upgraded: custom_zones/MyZone/models/v1_1.wom
migrate-zone: custom_zones/MyZone
scanned : 3 WOM file(s)
upgraded : 2 (added single-batch entry)
already v3: 1 (no change needed)
Idempotent: already-migrated files become no-ops, so it's safe to
run inside --for-each-zone over a whole project. Useful when the
editor adds a new WOM3-only feature and legacy zones need a one-shot
upgrade pass.
Returns exit 1 if any file fails to write (separate from 'no upgrade
needed'), so CI can gate on it.
Verified: seeded a dir with 2 v1 WOMs + 1 v3 WOM. First run
upgraded 2, reported 1 as already-v3. Re-run reported all 3 as
already-v3 (no double-batching).