clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

VOID DECOR (M18b): the empty air gets furniture — drifting shoals, ships, ghosts, meteors

Dami committed 74731b2
New generative system, zero perturbation of existing streams (new labeled
family "void-decor"):
- gen/endless/voidDecor.ts: per-segment clusters (0-2, expected ~1) of
  1-5 pieces hung off the segment's own path samples — mid band 8-16u
  (slightly below the line), far band 18-40u scaled into fog silhouettes;
  biome-flavored tables (ships off tropical coasts, a ghost + crypt in
  spooky air, snowflakes in winter, sand shoals in autumn) + generic
  rock/crystal shoals; rare far-band EVENTS ~0.08 (meteor trains, a
  passing craft/satellite) with pure-in-index quiet-after suppression.
  Sites keep >=6u of every path sample (main+alt), island cells, and any
  occupancy claim at their column. Space Kit imported (153 pieces).
- EndlessSegment.voidDecor is a SEPARATE field: the proxy path
  structurally cannot see it — non-collision by construction (contract
  test: proxies byte-count-identical with the field stripped).
- Placement.float {amp, period, phase, drift} — the wind model: canon
  hashes it; genVersion untouched (finite never emits it).
- client/voidRender.ts: instanced flat in the frame-correct segment
  group (spawn/exit waves see direct children; twist halves split per
  anchor), castShadow off, no occluder flag; float pass in finishFrame
  gated on the spawn wave (writer-overlap-free) with a 2s bob envelope +
  capped segment-local drift; fit box skips voidDecor-flagged meshes
  (far shoals must not inflate shadow/focus).
- Tests: same-seed determinism, 30x8 structural scan (budget, clearance,
  resolution, float-on-every-piece, multi-pack emergence), proxy
  blindness contract, event rarity + never-consecutive law.
  188 green, check clean. Endless goldens re-pinned (canon + content);
  genVersion g491fc4c7a685 unchanged.
commit
74731b242648c8051e0cd701703bffcc99804114
parent
ad5a11148863dca8b4d2d345465fe3982e7cbdd9
author
Dami <dami.etoile@gmail.com>
12 files changed, +6100 -50
Aassets/space-kit.glbbin
Aassets/space-kit.manifest.json5366++++++++++++++++++++++++++++++
Msrc/client/assets.ts2+
Msrc/client/endless.ts41+-
Asrc/client/voidRender.ts182+
Asrc/gen/endless/voidDecor.ts301++
Msrc/gen/endless/world.ts8+
Msrc/shared/types.ts6+
Mtests/golden_endless.json96+-
Atests/void_decor_test.ts135+
Mtools/import_assets.ts2+
Mtools/pin_endless.ts11+
A assets/space-kit.glb +0 -0

Binary file.

A assets/space-kit.manifest.json +5366 -0
@@ -0,0 +1,5366 @@
1+{2+ "version": 1,3+ "pack": "Space Kit",4+ "slug": "space-kit",5+ "license": "CC0",6+ "glbPath": "assets/space-kit.glb",7+ "glbBytes": 1370060,8+ "pieceCount": 153,9+ "pieces": [10+ {11+ "id": "alien",12+ "family": "unknown",13+ "solid": false,14+ "hole": false,15+ "wide": false,16+ "long": false,17+ "short": false,18+ "double": false,19+ "color": false,20+ "high": false,21+ "low": false,22+ "tier": null,23+ "size": null,24+ "chirality": null,25+ "span": null,26+ "count": null,27+ "segment": null,28+ "bboxMin": [29+ 1.758408,30+ 0,31+ 1.3732+ ],33+ "bboxMax": [34+ 2.241592,35+ 0.843184,36+ 1.737+ ],38+ "footprint": [39+ 1,40+ 141+ ],42+ "vertices": 1326,43+ "triangles": 29244+ },45+ {46+ "id": "astronautA",47+ "family": "unknown",48+ "solid": false,49+ "hole": false,50+ "wide": false,51+ "long": false,52+ "short": false,53+ "double": false,54+ "color": false,55+ "high": false,56+ "low": false,57+ "tier": null,58+ "size": null,59+ "chirality": null,60+ "span": null,61+ "count": null,62+ "segment": null,63+ "bboxMin": [64+ 1.758408,65+ 0,66+ 1.3767+ ],68+ "bboxMax": [69+ 2.241592,70+ 0.793184,71+ 1.772+ ],73+ "footprint": [74+ 1,75+ 176+ ],77+ "vertices": 1302,78+ "triangles": 29679+ },80+ {81+ "id": "astronautB",82+ "family": "unknown",83+ "solid": false,84+ "hole": false,85+ "wide": false,86+ "long": false,87+ "short": false,88+ "double": false,89+ "color": false,90+ "high": false,91+ "low": false,92+ "tier": null,93+ "size": null,94+ "chirality": null,95+ "span": null,96+ "count": null,97+ "segment": null,98+ "bboxMin": [99+ 1.758408,100+ 0,101+ 1.37102+ ],103+ "bboxMax": [104+ 2.241592,105+ 0.793184,106+ 1.7107+ ],108+ "footprint": [109+ 1,110+ 1111+ ],112+ "vertices": 1196,113+ "triangles": 314114+ },115+ {116+ "id": "barrel",117+ "family": "unknown",118+ "solid": false,119+ "hole": false,120+ "wide": false,121+ "long": false,122+ "short": false,123+ "double": false,124+ "color": false,125+ "high": false,126+ "low": false,127+ "tier": null,128+ "size": null,129+ "chirality": null,130+ "span": null,131+ "count": null,132+ "segment": null,133+ "bboxMin": [134+ 1.9,135+ 0,136+ 1.413397137+ ],138+ "bboxMax": [139+ 2.1,140+ 0.25,141+ 1.586603142+ ],143+ "footprint": [144+ 1,145+ 1146+ ],147+ "vertices": 132,148+ "triangles": 56149+ },150+ {151+ "id": "barrels",152+ "family": "unknown",153+ "solid": false,154+ "hole": false,155+ "wide": false,156+ "long": false,157+ "short": false,158+ "double": false,159+ "color": false,160+ "high": false,161+ "low": false,162+ "tier": null,163+ "size": null,164+ "chirality": null,165+ "span": null,166+ "count": null,167+ "segment": null,168+ "bboxMin": [169+ 1.725,170+ 0,171+ 1.251795172+ ],173+ "bboxMax": [174+ 2.275,175+ 0.35,176+ 1.748205177+ ],178+ "footprint": [179+ 1,180+ 1181+ ],182+ "vertices": 672,183+ "triangles": 268184+ },185+ {186+ "id": "barrels_rail",187+ "family": "unknown",188+ "solid": false,189+ "hole": false,190+ "wide": false,191+ "long": false,192+ "short": false,193+ "double": false,194+ "color": false,195+ "high": false,196+ "low": false,197+ "tier": null,198+ "size": null,199+ "chirality": null,200+ "span": null,201+ "count": null,202+ "segment": null,203+ "bboxMin": [204+ 1.725,205+ 0,206+ 1.251795207+ ],208+ "bboxMax": [209+ 2.275,210+ 0.35,211+ 1.748205212+ ],213+ "footprint": [214+ 1,215+ 1216+ ],217+ "vertices": 1422,218+ "triangles": 428219+ },220+ {221+ "id": "bones",222+ "family": "unknown",223+ "solid": false,224+ "hole": false,225+ "wide": false,226+ "long": false,227+ "short": false,228+ "double": false,229+ "color": false,230+ "high": false,231+ "low": false,232+ "tier": null,233+ "size": null,234+ "chirality": null,235+ "span": null,236+ "count": null,237+ "segment": null,238+ "bboxMin": [239+ 1.5693,240+ 0,241+ 1.20894242+ ],243+ "bboxMax": [244+ 2.4307,245+ 0.4422,246+ 1.79106247+ ],248+ "footprint": [249+ 1,250+ 1251+ ],252+ "vertices": 336,253+ "triangles": 168254+ },255+ {256+ "id": "chimney",257+ "family": "unknown",258+ "solid": false,259+ "hole": false,260+ "wide": false,261+ "long": false,262+ "short": false,263+ "double": false,264+ "color": false,265+ "high": false,266+ "low": false,267+ "tier": null,268+ "size": null,269+ "chirality": null,270+ "span": null,271+ "count": null,272+ "segment": null,273+ "bboxMin": [274+ 1.8,275+ 0,276+ 1.3277+ ],278+ "bboxMax": [279+ 2.2,280+ 2,281+ 1.7282+ ],283+ "footprint": [284+ 1,285+ 1286+ ],287+ "vertices": 336,288+ "triangles": 140289+ },290+ {291+ "id": "chimney_detailed",292+ "family": "unknown",293+ "solid": false,294+ "hole": false,295+ "wide": false,296+ "long": false,297+ "short": false,298+ "double": false,299+ "color": false,300+ "high": false,301+ "low": false,302+ "tier": null,303+ "size": null,304+ "chirality": null,305+ "span": null,306+ "count": null,307+ "segment": null,308+ "bboxMin": [309+ 1.8,310+ 0,311+ 1.3312+ ],313+ "bboxMax": [314+ 2.2,315+ 2,316+ 1.7317+ ],318+ "footprint": [319+ 1,320+ 1321+ ],322+ "vertices": 1224,323+ "triangles": 332324+ },325+ {326+ "id": "corridor",327+ "family": "unknown",328+ "solid": false,329+ "hole": false,330+ "wide": false,331+ "long": false,332+ "short": false,333+ "double": false,334+ "color": false,335+ "high": false,336+ "low": false,337+ "tier": null,338+ "size": null,339+ "chirality": null,340+ "span": null,341+ "count": null,342+ "segment": null,343+ "bboxMin": [344+ 1.5,345+ 0,346+ 1347+ ],348+ "bboxMax": [349+ 2.5,350+ 1,351+ 2352+ ],353+ "footprint": [354+ 1,355+ 1356+ ],357+ "vertices": 336,358+ "triangles": 84359+ },360+ {361+ "id": "corridor_corner",362+ "family": "unknown",363+ "solid": false,364+ "hole": false,365+ "wide": false,366+ "long": false,367+ "short": false,368+ "double": false,369+ "color": false,370+ "high": false,371+ "low": false,372+ "tier": null,373+ "size": null,374+ "chirality": null,375+ "span": null,376+ "count": null,377+ "segment": null,378+ "bboxMin": [379+ 1.5,380+ 0,381+ 1382+ ],383+ "bboxMax": [384+ 2.5,385+ 1,386+ 2387+ ],388+ "footprint": [389+ 1,390+ 1391+ ],392+ "vertices": 444,393+ "triangles": 110394+ },395+ {396+ "id": "corridor_cornerRound",397+ "family": "unknown",398+ "solid": false,399+ "hole": false,400+ "wide": false,401+ "long": false,402+ "short": false,403+ "double": false,404+ "color": false,405+ "high": false,406+ "low": false,407+ "tier": null,408+ "size": null,409+ "chirality": null,410+ "span": null,411+ "count": null,412+ "segment": null,413+ "bboxMin": [414+ 1.5,415+ 0,416+ 1417+ ],418+ "bboxMax": [419+ 2.5,420+ 1,421+ 2422+ ],423+ "footprint": [424+ 1,425+ 1426+ ],427+ "vertices": 1716,428+ "triangles": 414429+ },430+ {431+ "id": "corridor_cornerRoundWindow",432+ "family": "unknown",433+ "solid": false,434+ "hole": false,435+ "wide": false,436+ "long": false,437+ "short": false,438+ "double": false,439+ "color": false,440+ "high": false,441+ "low": false,442+ "tier": null,443+ "size": null,444+ "chirality": null,445+ "span": null,446+ "count": null,447+ "segment": null,448+ "bboxMin": [449+ 1.5,450+ 0,451+ 1452+ ],453+ "bboxMax": [454+ 2.5,455+ 1,456+ 2457+ ],458+ "footprint": [459+ 1,460+ 1461+ ],462+ "vertices": 2744,463+ "triangles": 484464+ },465+ {466+ "id": "corridor_cross",467+ "family": "unknown",468+ "solid": false,469+ "hole": false,470+ "wide": false,471+ "long": false,472+ "short": false,473+ "double": false,474+ "color": false,475+ "high": false,476+ "low": false,477+ "tier": null,478+ "size": null,479+ "chirality": null,480+ "span": null,481+ "count": null,482+ "segment": null,483+ "bboxMin": [484+ 1.5,485+ 0,486+ 1487+ ],488+ "bboxMax": [489+ 2.5,490+ 1,491+ 2492+ ],493+ "footprint": [494+ 1,495+ 1496+ ],497+ "vertices": 756,498+ "triangles": 172499+ },500+ {501+ "id": "corridor_detailed",502+ "family": "unknown",503+ "solid": false,504+ "hole": false,505+ "wide": false,506+ "long": false,507+ "short": false,508+ "double": false,509+ "color": false,510+ "high": false,511+ "low": false,512+ "tier": null,513+ "size": null,514+ "chirality": null,515+ "span": null,516+ "count": null,517+ "segment": null,518+ "bboxMin": [519+ 1.4,520+ 0,521+ 1522+ ],523+ "bboxMax": [524+ 2.6,525+ 1.1,526+ 2527+ ],528+ "footprint": [529+ 2,530+ 1531+ ],532+ "vertices": 872,533+ "triangles": 164534+ },535+ {536+ "id": "corridor_end",537+ "family": "unknown",538+ "solid": false,539+ "hole": false,540+ "wide": false,541+ "long": false,542+ "short": false,543+ "double": false,544+ "color": false,545+ "high": false,546+ "low": false,547+ "tier": null,548+ "size": null,549+ "chirality": null,550+ "span": null,551+ "count": null,552+ "segment": null,553+ "bboxMin": [554+ 1.5,555+ 0,556+ 1.5557+ ],558+ "bboxMax": [559+ 2.5,560+ 1,561+ 2562+ ],563+ "footprint": [564+ 1,565+ 1566+ ],567+ "vertices": 592,568+ "triangles": 120569+ },570+ {571+ "id": "corridor_open",572+ "family": "unknown",573+ "solid": false,574+ "hole": false,575+ "wide": false,576+ "long": false,577+ "short": false,578+ "double": false,579+ "color": false,580+ "high": false,581+ "low": false,582+ "tier": null,583+ "size": null,584+ "chirality": null,585+ "span": null,586+ "count": null,587+ "segment": null,588+ "bboxMin": [589+ 1.5,590+ 0,591+ 1592+ ],593+ "bboxMax": [594+ 2.5,595+ 1,596+ 2597+ ],598+ "footprint": [599+ 1,600+ 1601+ ],602+ "vertices": 460,603+ "triangles": 170604+ },605+ {606+ "id": "corridor_roof",607+ "family": "unknown",608+ "solid": false,609+ "hole": false,610+ "wide": false,611+ "long": false,612+ "short": false,613+ "double": false,614+ "color": false,615+ "high": false,616+ "low": false,617+ "tier": null,618+ "size": null,619+ "chirality": null,620+ "span": null,621+ "count": null,622+ "segment": null,623+ "bboxMin": [624+ 1.5,625+ 0,626+ 1627+ ],628+ "bboxMax": [629+ 2.5,630+ 0.241421,631+ 2632+ ],633+ "footprint": [634+ 1,635+ 1636+ ],637+ "vertices": 136,638+ "triangles": 44639+ },640+ {641+ "id": "corridor_split",642+ "family": "unknown",643+ "solid": false,644+ "hole": false,645+ "wide": false,646+ "long": false,647+ "short": false,648+ "double": false,649+ "color": false,650+ "high": false,651+ "low": false,652+ "tier": null,653+ "size": null,654+ "chirality": null,655+ "span": null,656+ "count": null,657+ "segment": null,658+ "bboxMin": [659+ 1.5,660+ 0,661+ 1662+ ],663+ "bboxMax": [664+ 2.5,665+ 1,666+ 2667+ ],668+ "footprint": [669+ 1,670+ 1671+ ],672+ "vertices": 728,673+ "triangles": 128674+ },675+ {676+ "id": "corridor_wall",677+ "family": "unknown",678+ "solid": false,679+ "hole": false,680+ "wide": false,681+ "long": false,682+ "short": false,683+ "double": false,684+ "color": false,685+ "high": false,686+ "low": false,687+ "tier": null,688+ "size": null,689+ "chirality": null,690+ "span": null,691+ "count": null,692+ "segment": null,693+ "bboxMin": [694+ 2.4,695+ 0,696+ 1697+ ],698+ "bboxMax": [699+ 2.5,700+ 0.8,701+ 2702+ ],703+ "footprint": [704+ 1,705+ 1706+ ],707+ "vertices": 144,708+ "triangles": 36709+ },710+ {711+ "id": "corridor_wallCorner",712+ "family": "unknown",713+ "solid": false,714+ "hole": false,715+ "wide": false,716+ "long": false,717+ "short": false,718+ "double": false,719+ "color": false,720+ "high": false,721+ "low": false,722+ "tier": null,723+ "size": null,724+ "chirality": null,725+ "span": null,726+ "count": null,727+ "segment": null,728+ "bboxMin": [729+ 1.5,730+ 0,731+ 1732+ ],733+ "bboxMax": [734+ 2.5,735+ 0.8,736+ 2737+ ],738+ "footprint": [739+ 1,740+ 1741+ ],742+ "vertices": 1432,743+ "triangles": 256744+ },745+ {746+ "id": "corridor_window",747+ "family": "unknown",748+ "solid": false,749+ "hole": false,750+ "wide": false,751+ "long": false,752+ "short": false,753+ "double": false,754+ "color": false,755+ "high": false,756+ "low": false,757+ "tier": null,758+ "size": null,759+ "chirality": null,760+ "span": null,761+ "count": null,762+ "segment": null,763+ "bboxMin": [764+ 1.5,765+ 0,766+ 1767+ ],768+ "bboxMax": [769+ 2.5,770+ 1,771+ 2772+ ],773+ "footprint": [774+ 1,775+ 1776+ ],777+ "vertices": 984,778+ "triangles": 190779+ },780+ {781+ "id": "corridor_windowClosed",782+ "family": "unknown",783+ "solid": false,784+ "hole": false,785+ "wide": false,786+ "long": false,787+ "short": false,788+ "double": false,789+ "color": false,790+ "high": false,791+ "low": false,792+ "tier": null,793+ "size": null,794+ "chirality": null,795+ "span": null,796+ "count": null,797+ "segment": null,798+ "bboxMin": [799+ 1.5,800+ 0,801+ 1802+ ],803+ "bboxMax": [804+ 2.5,805+ 1,806+ 2807+ ],808+ "footprint": [809+ 1,810+ 1811+ ],812+ "vertices": 1112,813+ "triangles": 214814+ },815+ {816+ "id": "craft_cargoA",817+ "family": "unknown",818+ "solid": false,819+ "hole": false,820+ "wide": false,821+ "long": false,822+ "short": false,823+ "double": false,824+ "color": false,825+ "high": false,826+ "low": false,827+ "tier": null,828+ "size": null,829+ "chirality": null,830+ "span": null,831+ "count": null,832+ "segment": null,833+ "bboxMin": [834+ 1.2,835+ 0,836+ 0.275837+ ],838+ "bboxMax": [839+ 2.8,840+ 0.8,841+ 2.725842+ ],843+ "footprint": [844+ 2,845+ 3846+ ],847+ "vertices": 1632,848+ "triangles": 264849+ },850+ {851+ "id": "craft_cargoB",852+ "family": "unknown",853+ "solid": false,854+ "hole": false,855+ "wide": false,856+ "long": false,857+ "short": false,858+ "double": false,859+ "color": false,860+ "high": false,861+ "low": false,862+ "tier": null,863+ "size": null,864+ "chirality": null,865+ "span": null,866+ "count": null,867+ "segment": null,868+ "bboxMin": [869+ 1.25,870+ 0,871+ 0.25872+ ],873+ "bboxMax": [874+ 2.75,875+ 0.9,876+ 2.75877+ ],878+ "footprint": [879+ 2,880+ 3881+ ],882+ "vertices": 2376,883+ "triangles": 380884+ },885+ {886+ "id": "craft_miner",887+ "family": "unknown",888+ "solid": false,889+ "hole": false,890+ "wide": false,891+ "long": false,892+ "short": false,893+ "double": false,894+ "color": false,895+ "high": false,896+ "low": false,897+ "tier": null,898+ "size": null,899+ "chirality": null,900+ "span": null,901+ "count": null,902+ "segment": null,903+ "bboxMin": [904+ 1.1,905+ 0,906+ 0.2907+ ],908+ "bboxMax": [909+ 2.9,910+ 0.7,911+ 2.8912+ ],913+ "footprint": [914+ 2,915+ 3916+ ],917+ "vertices": 2416,918+ "triangles": 384919+ },920+ {921+ "id": "craft_racer",922+ "family": "unknown",923+ "solid": false,924+ "hole": false,925+ "wide": false,926+ "long": false,927+ "short": false,928+ "double": false,929+ "color": false,930+ "high": false,931+ "low": false,932+ "tier": null,933+ "size": null,934+ "chirality": null,935+ "span": null,936+ "count": null,937+ "segment": null,938+ "bboxMin": [939+ 1.4,940+ 0,941+ 0.487165942+ ],943+ "bboxMax": [944+ 2.6,945+ 0.75,946+ 2.512835947+ ],948+ "footprint": [949+ 2,950+ 3951+ ],952+ "vertices": 1640,953+ "triangles": 280954+ },955+ {956+ "id": "craft_speederA",957+ "family": "unknown",958+ "solid": false,959+ "hole": false,960+ "wide": false,961+ "long": false,962+ "short": false,963+ "double": false,964+ "color": false,965+ "high": false,966+ "low": false,967+ "tier": null,968+ "size": null,969+ "chirality": null,970+ "span": null,971+ "count": null,972+ "segment": null,973+ "bboxMin": [974+ 1,975+ 0,976+ 0.45977+ ],978+ "bboxMax": [979+ 3,980+ 0.8,981+ 2.55982+ ],983+ "footprint": [984+ 2,985+ 3986+ ],987+ "vertices": 1800,988+ "triangles": 280989+ },990+ {991+ "id": "craft_speederB",992+ "family": "unknown",993+ "solid": false,994+ "hole": false,995+ "wide": false,996+ "long": false,997+ "short": false,998+ "double": false,999+ "color": false,1000+ "high": false,1001+ "low": false,1002+ "tier": null,1003+ "size": null,1004+ "chirality": null,1005+ "span": null,1006+ "count": null,1007+ "segment": null,1008+ "bboxMin": [1009+ 1,1010+ 0,1011+ 0.4871651012+ ],1013+ "bboxMax": [1014+ 3,1015+ 0.6,1016+ 2.5128351017+ ],1018+ "footprint": [1019+ 2,1020+ 31021+ ],1022+ "vertices": 1644,1023+ "triangles": 2701024+ },1025+ {1026+ "id": "craft_speederC",1027+ "family": "unknown",1028+ "solid": false,1029+ "hole": false,1030+ "wide": false,1031+ "long": false,1032+ "short": false,1033+ "double": false,1034+ "color": false,1035+ "high": false,1036+ "low": false,1037+ "tier": null,1038+ "size": null,1039+ "chirality": null,1040+ "span": null,1041+ "count": null,1042+ "segment": null,1043+ "bboxMin": [1044+ 0.6,1045+ 0,1046+ 0.5371651047+ ],1048+ "bboxMax": [1049+ 3.4,1050+ 0.6,1051+ 2.4628351052+ ],1053+ "footprint": [1054+ 3,1055+ 21056+ ],1057+ "vertices": 1784,1058+ "triangles": 2921059+ },1060+ {1061+ "id": "craft_speederD",1062+ "family": "unknown",1063+ "solid": false,1064+ "hole": false,1065+ "wide": false,1066+ "long": false,1067+ "short": false,1068+ "double": false,1069+ "color": false,1070+ "high": false,1071+ "low": false,1072+ "tier": null,1073+ "size": null,1074+ "chirality": null,1075+ "span": null,1076+ "count": null,1077+ "segment": null,1078+ "bboxMin": [1079+ 0.6,1080+ 0,1081+ 0.3871651082+ ],1083+ "bboxMax": [1084+ 3.4,1085+ 0.9,1086+ 2.6128351087+ ],1088+ "footprint": [1089+ 3,1090+ 31091+ ],1092+ "vertices": 2032,1093+ "triangles": 3221094+ },1095+ {1096+ "id": "crater",1097+ "family": "unknown",1098+ "solid": false,1099+ "hole": false,1100+ "wide": false,1101+ "long": false,1102+ "short": false,1103+ "double": false,1104+ "color": false,1105+ "high": false,1106+ "low": false,1107+ "tier": null,1108+ "size": null,1109+ "chirality": null,1110+ "span": null,1111+ "count": null,1112+ "segment": null,1113+ "bboxMin": [1114+ 1.642505,1115+ 0,1116+ 1.08721117+ ],1118+ "bboxMax": [1119+ 2.357495,1120+ 0.1176,1121+ 1.91281122+ ],1123+ "footprint": [1124+ 1,1125+ 11126+ ],1127+ "vertices": 192,1128+ "triangles": 601129+ },1130+ {1131+ "id": "craterLarge",1132+ "family": "unknown",1133+ "solid": false,1134+ "hole": false,1135+ "wide": false,1136+ "long": false,1137+ "short": false,1138+ "double": false,1139+ "color": false,1140+ "high": false,1141+ "low": false,1142+ "tier": null,1143+ "size": null,1144+ "chirality": null,1145+ "span": null,1146+ "count": null,1147+ "segment": null,1148+ "bboxMin": [1149+ 1.556975,1150+ 0,1151+ 1.0569751152+ ],1153+ "bboxMax": [1154+ 2.443025,1155+ 0.12,1156+ 1.9430251157+ ],1158+ "footprint": [1159+ 1,1160+ 11161+ ],1162+ "vertices": 192,1163+ "triangles": 601164+ },1165+ {1166+ "id": "desk_chair",1167+ "family": "unknown",1168+ "solid": false,1169+ "hole": false,1170+ "wide": false,1171+ "long": false,1172+ "short": false,1173+ "double": false,1174+ "color": false,1175+ "high": false,1176+ "low": false,1177+ "tier": null,1178+ "size": null,1179+ "chirality": null,1180+ "span": null,1181+ "count": null,1182+ "segment": null,1183+ "bboxMin": [1184+ 1.925,1185+ 0,1186+ 1.4251187+ ],1188+ "bboxMax": [1189+ 2.075,1190+ 0.29375,1191+ 1.5751192+ ],1193+ "footprint": [1194+ 1,1195+ 11196+ ],1197+ "vertices": 426,1198+ "triangles": 881199+ },1200+ {1201+ "id": "desk_chairArms",1202+ "family": "unknown",1203+ "solid": false,1204+ "hole": false,1205+ "wide": false,1206+ "long": false,1207+ "short": false,1208+ "double": false,1209+ "color": false,1210+ "high": false,1211+ "low": false,1212+ "tier": null,1213+ "size": null,1214+ "chirality": null,1215+ "span": null,1216+ "count": null,1217+ "segment": null,1218+ "bboxMin": [1219+ 1.875,1220+ 0,1221+ 1.4251222+ ],1223+ "bboxMax": [1224+ 2.125,1225+ 0.34375,1226+ 1.5751227+ ],1228+ "footprint": [1229+ 1,1230+ 11231+ ],1232+ "vertices": 618,1233+ "triangles": 1281234+ },1235+ {1236+ "id": "desk_chairStool",1237+ "family": "unknown",1238+ "solid": false,1239+ "hole": false,1240+ "wide": false,1241+ "long": false,1242+ "short": false,1243+ "double": false,1244+ "color": false,1245+ "high": false,1246+ "low": false,1247+ "tier": null,1248+ "size": null,1249+ "chirality": null,1250+ "span": null,1251+ "count": null,1252+ "segment": null,1253+ "bboxMin": [1254+ 1.925,1255+ 0,1256+ 1.4251257+ ],1258+ "bboxMax": [1259+ 2.075,1260+ 0.16875,1261+ 1.5751262+ ],1263+ "footprint": [1264+ 1,1265+ 11266+ ],1267+ "vertices": 256,1268+ "triangles": 761269+ },1270+ {1271+ "id": "desk_computer",1272+ "family": "unknown",1273+ "solid": false,1274+ "hole": false,1275+ "wide": false,1276+ "long": false,1277+ "short": false,1278+ "double": false,1279+ "color": false,1280+ "high": false,1281+ "low": false,1282+ "tier": null,1283+ "size": null,1284+ "chirality": null,1285+ "span": null,1286+ "count": null,1287+ "segment": null,1288+ "bboxMin": [1289+ 1.7375,1290+ 0,1291+ 1.38751292+ ],1293+ "bboxMax": [1294+ 2.2625,1295+ 0.375,1296+ 1.61251297+ ],1298+ "footprint": [1299+ 1,1300+ 11301+ ],1302+ "vertices": 336,1303+ "triangles": 881304+ },1305+ {1306+ "id": "desk_computerCorner",1307+ "family": "unknown",1308+ "solid": false,1309+ "hole": false,1310+ "wide": false,1311+ "long": false,1312+ "short": false,1313+ "double": false,1314+ "color": false,1315+ "high": false,1316+ "low": false,1317+ "tier": null,1318+ "size": null,1319+ "chirality": null,1320+ "span": null,1321+ "count": null,1322+ "segment": null,1323+ "bboxMin": [1324+ 1.625,1325+ 0,1326+ 1.1251327+ ],1328+ "bboxMax": [1329+ 2.375,1330+ 0.375,1331+ 1.8751332+ ],1333+ "footprint": [1334+ 1,1335+ 11336+ ],1337+ "vertices": 456,1338+ "triangles": 1261339+ },1340+ {1341+ "id": "desk_computerScreen",1342+ "family": "unknown",1343+ "solid": false,1344+ "hole": false,1345+ "wide": false,1346+ "long": false,1347+ "short": false,1348+ "double": false,1349+ "color": false,1350+ "high": false,1351+ "low": false,1352+ "tier": null,1353+ "size": null,1354+ "chirality": null,1355+ "span": null,1356+ "count": null,1357+ "segment": null,1358+ "bboxMin": [1359+ 1.7375,1360+ 0,1361+ 1.38751362+ ],1363+ "bboxMax": [1364+ 2.2625,1365+ 0.6375,1366+ 1.61251367+ ],1368+ "footprint": [1369+ 1,1370+ 11371+ ],1372+ "vertices": 432,1373+ "triangles": 1081374+ },1375+ {1376+ "id": "gate_complex",1377+ "family": "unknown",1378+ "solid": false,1379+ "hole": false,1380+ "wide": false,1381+ "long": false,1382+ "short": false,1383+ "double": false,1384+ "color": false,1385+ "high": false,1386+ "low": false,1387+ "tier": null,1388+ "size": null,1389+ "chirality": null,1390+ "span": null,1391+ "count": null,1392+ "segment": null,1393+ "bboxMin": [1394+ 1.5,1395+ 0,1396+ 1.251397+ ],1398+ "bboxMax": [1399+ 2.5,1400+ 1.013013,1401+ 1.751402+ ],1403+ "footprint": [1404+ 1,1405+ 11406+ ],1407+ "vertices": 2704,1408+ "triangles": 4601409+ },1410+ {1411+ "id": "gate_simple",1412+ "family": "unknown",1413+ "solid": false,1414+ "hole": false,1415+ "wide": false,1416+ "long": false,1417+ "short": false,1418+ "double": false,1419+ "color": false,1420+ "high": false,1421+ "low": false,1422+ "tier": null,1423+ "size": null,1424+ "chirality": null,1425+ "span": null,1426+ "count": null,1427+ "segment": null,1428+ "bboxMin": [1429+ 1.5,1430+ 0,1431+ 1.251432+ ],1433+ "bboxMax": [1434+ 2.5,1435+ 1.08,1436+ 1.751437+ ],1438+ "footprint": [1439+ 1,1440+ 11441+ ],1442+ "vertices": 954,1443+ "triangles": 2121444+ },1445+ {1446+ "id": "hangar_largeA",1447+ "family": "unknown",1448+ "solid": false,1449+ "hole": false,1450+ "wide": false,1451+ "long": false,1452+ "short": false,1453+ "double": false,1454+ "color": false,1455+ "high": false,1456+ "low": false,1457+ "tier": null,1458+ "size": null,1459+ "chirality": null,1460+ "span": null,1461+ "count": null,1462+ "segment": null,1463+ "bboxMin": [1464+ 1,1465+ 0,1466+ 01467+ ],1468+ "bboxMax": [1469+ 3,1470+ 1,1471+ 31472+ ],1473+ "footprint": [1474+ 2,1475+ 31476+ ],1477+ "vertices": 1404,1478+ "triangles": 4121479+ },1480+ {1481+ "id": "hangar_largeB",1482+ "family": "unknown",1483+ "solid": false,1484+ "hole": false,1485+ "wide": false,1486+ "long": false,1487+ "short": false,1488+ "double": false,1489+ "color": false,1490+ "high": false,1491+ "low": false,1492+ "tier": null,1493+ "size": null,1494+ "chirality": null,1495+ "span": null,1496+ "count": null,1497+ "segment": null,1498+ "bboxMin": [1499+ 0.9,1500+ 0,1501+ 01502+ ],1503+ "bboxMax": [1504+ 3.1,1505+ 1.1,1506+ 31507+ ],1508+ "footprint": [1509+ 3,1510+ 31511+ ],1512+ "vertices": 1484,1513+ "triangles": 3601514+ },1515+ {1516+ "id": "hangar_roundA",1517+ "family": "unknown",1518+ "solid": false,1519+ "hole": false,1520+ "wide": false,1521+ "long": false,1522+ "short": false,1523+ "double": false,1524+ "color": false,1525+ "high": false,1526+ "low": false,1527+ "tier": null,1528+ "size": null,1529+ "chirality": null,1530+ "span": null,1531+ "count": null,1532+ "segment": null,1533+ "bboxMin": [1534+ 0.364378,1535+ 0,1536+ 0.083511537+ ],1538+ "bboxMax": [1539+ 3.635622,1540+ 1.5,1541+ 2.916491542+ ],1543+ "footprint": [1544+ 4,1545+ 31546+ ],1547+ "vertices": 920,1548+ "triangles": 1661549+ },1550+ {1551+ "id": "hangar_roundB",1552+ "family": "unknown",1553+ "solid": false,1554+ "hole": false,1555+ "wide": false,1556+ "long": false,1557+ "short": false,1558+ "double": false,1559+ "color": false,1560+ "high": false,1561+ "low": false,1562+ "tier": null,1563+ "size": null,1564+ "chirality": null,1565+ "span": null,1566+ "count": null,1567+ "segment": null,1568+ "bboxMin": [1569+ 0.364378,1570+ 0,1571+ 0.083511572+ ],1573+ "bboxMax": [1574+ 3.635622,1575+ 1.8,1576+ 2.916491577+ ],1578+ "footprint": [1579+ 4,1580+ 31581+ ],1582+ "vertices": 920,1583+ "triangles": 1661584+ },1585+ {1586+ "id": "hangar_roundGlass",1587+ "family": "unknown",1588+ "solid": false,1589+ "hole": false,1590+ "wide": false,1591+ "long": false,1592+ "short": false,1593+ "double": false,1594+ "color": false,1595+ "high": false,1596+ "low": false,1597+ "tier": null,1598+ "size": null,1599+ "chirality": null,1600+ "span": null,1601+ "count": null,1602+ "segment": null,1603+ "bboxMin": [1604+ 0.364378,1605+ 0,1606+ 0.083511607+ ],1608+ "bboxMax": [1609+ 3.635622,1610+ 1.4,1611+ 2.916491612+ ],1613+ "footprint": [1614+ 4,1615+ 31616+ ],1617+ "vertices": 748,1618+ "triangles": 1131619+ },1620+ {1621+ "id": "hangar_smallA",1622+ "family": "unknown",1623+ "solid": false,1624+ "hole": false,1625+ "wide": false,1626+ "long": false,1627+ "short": false,1628+ "double": false,1629+ "color": false,1630+ "high": false,1631+ "low": false,1632+ "tier": null,1633+ "size": null,1634+ "chirality": null,1635+ "span": null,1636+ "count": null,1637+ "segment": null,1638+ "bboxMin": [1639+ 1,1640+ 0,1641+ 0.51642+ ],1643+ "bboxMax": [1644+ 3,1645+ 1,1646+ 2.51647+ ],1648+ "footprint": [1649+ 2,1650+ 21651+ ],1652+ "vertices": 948,1653+ "triangles": 2801654+ },1655+ {1656+ "id": "hangar_smallB",1657+ "family": "unknown",1658+ "solid": false,1659+ "hole": false,1660+ "wide": false,1661+ "long": false,1662+ "short": false,1663+ "double": false,1664+ "color": false,1665+ "high": false,1666+ "low": false,1667+ "tier": null,1668+ "size": null,1669+ "chirality": null,1670+ "span": null,1671+ "count": null,1672+ "segment": null,1673+ "bboxMin": [1674+ 1,1675+ 0,1676+ 0.51677+ ],1678+ "bboxMax": [1679+ 3,1680+ 1,1681+ 2.51682+ ],1683+ "footprint": [1684+ 2,1685+ 21686+ ],1687+ "vertices": 972,1688+ "triangles": 2841689+ },1690+ {1691+ "id": "machine_barrel",1692+ "family": "unknown",1693+ "solid": false,1694+ "hole": false,1695+ "wide": false,1696+ "long": false,1697+ "short": false,1698+ "double": false,1699+ "color": false,1700+ "high": false,1701+ "low": false,1702+ "tier": null,1703+ "size": null,1704+ "chirality": null,1705+ "span": null,1706+ "count": null,1707+ "segment": null,1708+ "bboxMin": [1709+ 1.776795,1710+ 0,1711+ 1.2422651712+ ],1713+ "bboxMax": [1714+ 2.423205,1715+ 0.6,1716+ 1.7577351717+ ],1718+ "footprint": [1719+ 1,1720+ 11721+ ],1722+ "vertices": 1272,1723+ "triangles": 3381724+ },1725+ {1726+ "id": "machine_barrelLarge",1727+ "family": "unknown",1728+ "solid": false,1729+ "hole": false,1730+ "wide": false,1731+ "long": false,1732+ "short": false,1733+ "double": false,1734+ "color": false,1735+ "high": false,1736+ "low": false,1737+ "tier": null,1738+ "size": null,1739+ "chirality": null,1740+ "span": null,1741+ "count": null,1742+ "segment": null,1743+ "bboxMin": [1744+ 1.622836,1745+ 0,1746+ 1.1228361747+ ],1748+ "bboxMax": [1749+ 2.377164,1750+ 0.6,1751+ 1.8771641752+ ],1753+ "footprint": [1754+ 1,1755+ 11756+ ],1757+ "vertices": 1100,1758+ "triangles": 1921759+ },1760+ {1761+ "id": "machine_generator",1762+ "family": "unknown",1763+ "solid": false,1764+ "hole": false,1765+ "wide": false,1766+ "long": false,1767+ "short": false,1768+ "double": false,1769+ "color": false,1770+ "high": false,1771+ "low": false,1772+ "tier": null,1773+ "size": null,1774+ "chirality": null,1775+ "span": null,1776+ "count": null,1777+ "segment": null,1778+ "bboxMin": [1779+ 1.6,1780+ 0,1781+ 1.1751782+ ],1783+ "bboxMax": [1784+ 2.3,1785+ 0.4,1786+ 1.71787+ ],1788+ "footprint": [1789+ 1,1790+ 11791+ ],1792+ "vertices": 1066,1793+ "triangles": 4061794+ },1795+ {1796+ "id": "machine_generatorLarge",1797+ "family": "unknown",1798+ "solid": false,1799+ "hole": false,1800+ "wide": false,1801+ "long": false,1802+ "short": false,1803+ "double": false,1804+ "color": false,1805+ "high": false,1806+ "low": false,1807+ "tier": null,1808+ "size": null,1809+ "chirality": null,1810+ "span": null,1811+ "count": null,1812+ "segment": null,1813+ "bboxMin": [1814+ 1.5,1815+ 0,1816+ 0.851817+ ],1818+ "bboxMax": [1819+ 2.5,1820+ 0.683013,1821+ 2.151822+ ],1823+ "footprint": [1824+ 1,1825+ 21826+ ],1827+ "vertices": 744,1828+ "triangles": 1261829+ },1830+ {1831+ "id": "machine_wireless",1832+ "family": "unknown",1833+ "solid": false,1834+ "hole": false,1835+ "wide": false,1836+ "long": false,1837+ "short": false,1838+ "double": false,1839+ "color": false,1840+ "high": false,1841+ "low": false,1842+ "tier": null,1843+ "size": null,1844+ "chirality": null,1845+ "span": null,1846+ "count": null,1847+ "segment": null,1848+ "bboxMin": [1849+ 1.625,1850+ 0,1851+ 1.251852+ ],1853+ "bboxMax": [1854+ 2.375,1855+ 0.6,1856+ 1.751857+ ],1858+ "footprint": [1859+ 1,1860+ 11861+ ],1862+ "vertices": 1072,1863+ "triangles": 1741864+ },1865+ {1866+ "id": "machine_wirelessCable",1867+ "family": "unknown",1868+ "solid": false,1869+ "hole": false,1870+ "wide": false,1871+ "long": false,1872+ "short": false,1873+ "double": false,1874+ "color": false,1875+ "high": false,1876+ "low": false,1877+ "tier": null,1878+ "size": null,1879+ "chirality": null,1880+ "span": null,1881+ "count": null,1882+ "segment": null,1883+ "bboxMin": [1884+ 1.55,1885+ 0,1886+ 1.151887+ ],1888+ "bboxMax": [1889+ 2.3,1890+ 0.8,1891+ 1.751892+ ],1893+ "footprint": [1894+ 1,1895+ 11896+ ],1897+ "vertices": 1494,1898+ "triangles": 2821899+ },1900+ {1901+ "id": "meteor",1902+ "family": "unknown",1903+ "solid": false,1904+ "hole": false,1905+ "wide": false,1906+ "long": false,1907+ "short": false,1908+ "double": false,1909+ "color": false,1910+ "high": false,1911+ "low": false,1912+ "tier": null,1913+ "size": null,1914+ "chirality": null,1915+ "span": null,1916+ "count": null,1917+ "segment": null,1918+ "bboxMin": [1919+ 1.566987,1920+ 0,1921+ 1.1251922+ ],1923+ "bboxMax": [1924+ 2.433013,1925+ 0.735,1926+ 1.8751927+ ],1928+ "footprint": [1929+ 1,1930+ 11931+ ],1932+ "vertices": 132,1933+ "triangles": 681934+ },1935+ {1936+ "id": "meteor_detailed",1937+ "family": "unknown",1938+ "solid": false,1939+ "hole": false,1940+ "wide": false,1941+ "long": false,1942+ "short": false,1943+ "double": false,1944+ "color": false,1945+ "high": false,1946+ "low": false,1947+ "tier": null,1948+ "size": null,1949+ "chirality": null,1950+ "span": null,1951+ "count": null,1952+ "segment": null,1953+ "bboxMin": [1954+ 1.566987,1955+ 0,1956+ 1.08751957+ ],1958+ "bboxMax": [1959+ 2.433013,1960+ 0.735,1961+ 1.91251962+ ],1963+ "footprint": [1964+ 1,1965+ 11966+ ],1967+ "vertices": 324,1968+ "triangles": 1961969+ },1970+ {1971+ "id": "meteor_half",1972+ "family": "unknown",1973+ "solid": false,1974+ "hole": false,1975+ "wide": false,1976+ "long": false,1977+ "short": false,1978+ "double": false,1979+ "color": false,1980+ "high": false,1981+ "low": false,1982+ "tier": null,1983+ "size": null,1984+ "chirality": null,1985+ "span": null,1986+ "count": null,1987+ "segment": null,1988+ "bboxMin": [1989+ 1.566987,1990+ 0,1991+ 1.1251992+ ],1993+ "bboxMax": [1994+ 2.433013,1995+ 0.3675,1996+ 1.8751997+ ],1998+ "footprint": [1999+ 1,2000+ 12001+ ],2002+ "vertices": 84,2003+ "triangles": 442004+ },2005+ {2006+ "id": "monorail_trackCornerLarge",2007+ "family": "unknown",2008+ "solid": false,2009+ "hole": false,2010+ "wide": false,2011+ "long": false,2012+ "short": false,2013+ "double": false,2014+ "color": false,2015+ "high": false,2016+ "low": false,2017+ "tier": null,2018+ "size": null,2019+ "chirality": null,2020+ "span": null,2021+ "count": null,2022+ "segment": null,2023+ "bboxMin": [2024+ 1,2025+ 0,2026+ 0.52027+ ],2028+ "bboxMax": [2029+ 2.6,2030+ 0.13,2031+ 2.12032+ ],2033+ "footprint": [2034+ 2,2035+ 22036+ ],2037+ "vertices": 624,2038+ "triangles": 2042039+ },2040+ {2041+ "id": "monorail_trackCornerSmall",2042+ "family": "unknown",2043+ "solid": false,2044+ "hole": false,2045+ "wide": false,2046+ "long": false,2047+ "short": false,2048+ "double": false,2049+ "color": false,2050+ "high": false,2051+ "low": false,2052+ "tier": null,2053+ "size": null,2054+ "chirality": null,2055+ "span": null,2056+ "count": null,2057+ "segment": null,2058+ "bboxMin": [2059+ 1.5,2060+ 0,2061+ 12062+ ],2063+ "bboxMax": [2064+ 2.1,2065+ 0.13,2066+ 1.62067+ ],2068+ "footprint": [2069+ 1,2070+ 12071+ ],2072+ "vertices": 1248,2073+ "triangles": 4082074+ },2075+ {2076+ "id": "monorail_trackSlope",2077+ "family": "unknown",2078+ "solid": false,2079+ "hole": false,2080+ "wide": false,2081+ "long": false,2082+ "short": false,2083+ "double": false,2084+ "color": false,2085+ "high": false,2086+ "low": false,2087+ "tier": null,2088+ "size": null,2089+ "chirality": null,2090+ "span": null,2091+ "count": null,2092+ "segment": null,2093+ "bboxMin": [2094+ 1.9,2095+ 0,2096+ 12097+ ],2098+ "bboxMax": [2099+ 2.1,2100+ 0.63,2101+ 22102+ ],2103+ "footprint": [2104+ 1,2105+ 12106+ ],2107+ "vertices": 96,2108+ "triangles": 282109+ },2110+ {2111+ "id": "monorail_trackStraight",2112+ "family": "unknown",2113+ "solid": false,2114+ "hole": false,2115+ "wide": false,2116+ "long": false,2117+ "short": false,2118+ "double": false,2119+ "color": false,2120+ "high": false,2121+ "low": false,2122+ "tier": null,2123+ "size": null,2124+ "chirality": null,2125+ "span": null,2126+ "count": null,2127+ "segment": null,2128+ "bboxMin": [2129+ 1.9,2130+ 0,2131+ 12132+ ],2133+ "bboxMax": [2134+ 2.1,2135+ 0.13,2136+ 22137+ ],2138+ "footprint": [2139+ 1,2140+ 12141+ ],2142+ "vertices": 96,2143+ "triangles": 282144+ },2145+ {2146+ "id": "monorail_trackSupport",2147+ "family": "unknown",2148+ "solid": false,2149+ "hole": false,2150+ "wide": false,2151+ "long": false,2152+ "short": false,2153+ "double": false,2154+ "color": false,2155+ "high": false,2156+ "low": false,2157+ "tier": null,2158+ "size": null,2159+ "chirality": null,2160+ "span": null,2161+ "count": null,2162+ "segment": null,2163+ "bboxMin": [2164+ 1.9,2165+ 0,2166+ 1.42167+ ],2168+ "bboxMax": [2169+ 2.1,2170+ 0.5,2171+ 1.62172+ ],2173+ "footprint": [2174+ 1,2175+ 12176+ ],2177+ "vertices": 720,2178+ "triangles": 1762179+ },2180+ {2181+ "id": "monorail_trackSupportCorner",2182+ "family": "unknown",2183+ "solid": false,2184+ "hole": false,2185+ "wide": false,2186+ "long": false,2187+ "short": false,2188+ "double": false,2189+ "color": false,2190+ "high": false,2191+ "low": false,2192+ "tier": null,2193+ "size": null,2194+ "chirality": null,2195+ "span": null,2196+ "count": null,2197+ "segment": null,2198+ "bboxMin": [2199+ 1.9,2200+ 0,2201+ 1.42202+ ],2203+ "bboxMax": [2204+ 2.1,2205+ 0.5,2206+ 1.62207+ ],2208+ "footprint": [2209+ 1,2210+ 12211+ ],2212+ "vertices": 684,2213+ "triangles": 1642214+ },2215+ {2216+ "id": "monorail_trainBox",2217+ "family": "unknown",2218+ "solid": false,2219+ "hole": false,2220+ "wide": false,2221+ "long": false,2222+ "short": false,2223+ "double": false,2224+ "color": false,2225+ "high": false,2226+ "low": false,2227+ "tier": null,2228+ "size": null,2229+ "chirality": null,2230+ "span": null,2231+ "count": null,2232+ "segment": null,2233+ "bboxMin": [2234+ 1.75,2235+ 0,2236+ 12237+ ],2238+ "bboxMax": [2239+ 2.25,2240+ 0.8,2241+ 22242+ ],2243+ "footprint": [2244+ 1,2245+ 12246+ ],2247+ "vertices": 408,2248+ "triangles": 1002249+ },2250+ {2251+ "id": "monorail_trainCargo",2252+ "family": "unknown",2253+ "solid": false,2254+ "hole": false,2255+ "wide": false,2256+ "long": false,2257+ "short": false,2258+ "double": false,2259+ "color": false,2260+ "high": false,2261+ "low": false,2262+ "tier": null,2263+ "size": null,2264+ "chirality": null,2265+ "span": null,2266+ "count": null,2267+ "segment": null,2268+ "bboxMin": [2269+ 1.75,2270+ 0,2271+ 12272+ ],2273+ "bboxMax": [2274+ 2.25,2275+ 0.64,2276+ 22277+ ],2278+ "footprint": [2279+ 1,2280+ 12281+ ],2282+ "vertices": 4608,2283+ "triangles": 8162284+ },2285+ {2286+ "id": "monorail_trainEnd",2287+ "family": "unknown",2288+ "solid": false,2289+ "hole": false,2290+ "wide": false,2291+ "long": false,2292+ "short": false,2293+ "double": false,2294+ "color": false,2295+ "high": false,2296+ "low": false,2297+ "tier": null,2298+ "size": null,2299+ "chirality": null,2300+ "span": null,2301+ "count": null,2302+ "segment": null,2303+ "bboxMin": [2304+ 1.75,2305+ 0,2306+ 12307+ ],2308+ "bboxMax": [2309+ 2.25,2310+ 0.8,2311+ 22312+ ],2313+ "footprint": [2314+ 1,2315+ 12316+ ],2317+ "vertices": 1040,2318+ "triangles": 1902319+ },2320+ {2321+ "id": "monorail_trainFlat",2322+ "family": "unknown",2323+ "solid": false,2324+ "hole": false,2325+ "wide": false,2326+ "long": false,2327+ "short": false,2328+ "double": false,2329+ "color": false,2330+ "high": false,2331+ "low": false,2332+ "tier": null,2333+ "size": null,2334+ "chirality": null,2335+ "span": null,2336+ "count": null,2337+ "segment": null,2338+ "bboxMin": [2339+ 1.75,2340+ 0,2341+ 12342+ ],2343+ "bboxMax": [2344+ 2.25,2345+ 0.25,2346+ 22347+ ],2348+ "footprint": [2349+ 1,2350+ 12351+ ],2352+ "vertices": 396,2353+ "triangles": 842354+ },2355+ {2356+ "id": "monorail_trainFront",2357+ "family": "unknown",2358+ "solid": false,2359+ "hole": false,2360+ "wide": false,2361+ "long": false,2362+ "short": false,2363+ "double": false,2364+ "color": false,2365+ "high": false,2366+ "low": false,2367+ "tier": null,2368+ "size": null,2369+ "chirality": null,2370+ "span": null,2371+ "count": null,2372+ "segment": null,2373+ "bboxMin": [2374+ 1.7,2375+ 0,2376+ 0.952377+ ],2378+ "bboxMax": [2379+ 2.3,2380+ 0.8,2381+ 22382+ ],2383+ "footprint": [2384+ 1,2385+ 22386+ ],2387+ "vertices": 1116,2388+ "triangles": 2422389+ },2390+ {2391+ "id": "monorail_trainPassenger",2392+ "family": "unknown",2393+ "solid": false,2394+ "hole": false,2395+ "wide": false,2396+ "long": false,2397+ "short": false,2398+ "double": false,2399+ "color": false,2400+ "high": false,2401+ "low": false,2402+ "tier": null,2403+ "size": null,2404+ "chirality": null,2405+ "span": null,2406+ "count": null,2407+ "segment": null,2408+ "bboxMin": [2409+ 1.75,2410+ 0,2411+ 12412+ ],2413+ "bboxMax": [2414+ 2.25,2415+ 0.8,2416+ 22417+ ],2418+ "footprint": [2419+ 1,2420+ 12421+ ],2422+ "vertices": 968,2423+ "triangles": 1642424+ },2425+ {2426+ "id": "pipe_corner",2427+ "family": "unknown",2428+ "solid": false,2429+ "hole": false,2430+ "wide": false,2431+ "long": false,2432+ "short": false,2433+ "double": false,2434+ "color": false,2435+ "high": false,2436+ "low": false,2437+ "tier": null,2438+ "size": null,2439+ "chirality": null,2440+ "span": null,2441+ "count": null,2442+ "segment": null,2443+ "bboxMin": [2444+ 1.5,2445+ 0,2446+ 12447+ ],2448+ "bboxMax": [2449+ 2.34641,2450+ 0.6,2451+ 1.846412452+ ],2453+ "footprint": [2454+ 1,2455+ 12456+ ],2457+ "vertices": 254,2458+ "triangles": 842459+ },2460+ {2461+ "id": "pipe_cornerDiagonal",2462+ "family": "unknown",2463+ "solid": false,2464+ "hole": false,2465+ "wide": false,2466+ "long": false,2467+ "short": false,2468+ "double": false,2469+ "color": false,2470+ "high": false,2471+ "low": false,2472+ "tier": null,2473+ "size": null,2474+ "chirality": null,2475+ "span": null,2476+ "count": null,2477+ "segment": null,2478+ "bboxMin": [2479+ 1.5,2480+ 0,2481+ 12482+ ],2483+ "bboxMax": [2484+ 2.34641,2485+ 0.6,2486+ 1.846412487+ ],2488+ "footprint": [2489+ 1,2490+ 12491+ ],2492+ "vertices": 144,2493+ "triangles": 482494+ },2495+ {2496+ "id": "pipe_cornerRound",2497+ "family": "unknown",2498+ "solid": false,2499+ "hole": false,2500+ "wide": false,2501+ "long": false,2502+ "short": false,2503+ "double": false,2504+ "color": false,2505+ "high": false,2506+ "low": false,2507+ "tier": null,2508+ "size": null,2509+ "chirality": null,2510+ "span": null,2511+ "count": null,2512+ "segment": null,2513+ "bboxMin": [2514+ 1.5,2515+ 0,2516+ 12517+ ],2518+ "bboxMax": [2519+ 2.34641,2520+ 0.6,2521+ 1.846412522+ ],2523+ "footprint": [2524+ 1,2525+ 12526+ ],2527+ "vertices": 1044,2528+ "triangles": 3522529+ },2530+ {2531+ "id": "pipe_cornerRoundLarge",2532+ "family": "unknown",2533+ "solid": false,2534+ "hole": false,2535+ "wide": false,2536+ "long": false,2537+ "short": false,2538+ "double": false,2539+ "color": false,2540+ "high": false,2541+ "low": false,2542+ "tier": null,2543+ "size": null,2544+ "chirality": null,2545+ "span": null,2546+ "count": null,2547+ "segment": null,2548+ "bboxMin": [2549+ 1,2550+ 0,2551+ 0.52552+ ],2553+ "bboxMax": [2554+ 2.84641,2555+ 0.6,2556+ 2.346412557+ ],2558+ "footprint": [2559+ 2,2560+ 22561+ ],2562+ "vertices": 1068,2563+ "triangles": 3562564+ },2565+ {2566+ "id": "pipe_cross",2567+ "family": "unknown",2568+ "solid": false,2569+ "hole": false,2570+ "wide": false,2571+ "long": false,2572+ "short": false,2573+ "double": false,2574+ "color": false,2575+ "high": false,2576+ "low": false,2577+ "tier": null,2578+ "size": null,2579+ "chirality": null,2580+ "span": null,2581+ "count": null,2582+ "segment": null,2583+ "bboxMin": [2584+ 1.5,2585+ 0,2586+ 12587+ ],2588+ "bboxMax": [2589+ 2.5,2590+ 0.6,2591+ 22592+ ],2593+ "footprint": [2594+ 1,2595+ 12596+ ],2597+ "vertices": 825,2598+ "triangles": 2002599+ },2600+ {2601+ "id": "pipe_end",2602+ "family": "unknown",2603+ "solid": false,2604+ "hole": false,2605+ "wide": false,2606+ "long": false,2607+ "short": false,2608+ "double": false,2609+ "color": false,2610+ "high": false,2611+ "low": false,2612+ "tier": null,2613+ "size": null,2614+ "chirality": null,2615+ "span": null,2616+ "count": null,2617+ "segment": null,2618+ "bboxMin": [2619+ 1.65359,2620+ 0,2621+ 12622+ ],2623+ "bboxMax": [2624+ 2.34641,2625+ 0.6,2626+ 22627+ ],2628+ "footprint": [2629+ 1,2630+ 12631+ ],2632+ "vertices": 318,2633+ "triangles": 822634+ },2635+ {2636+ "id": "pipe_entrance",2637+ "family": "unknown",2638+ "solid": false,2639+ "hole": false,2640+ "wide": false,2641+ "long": false,2642+ "short": false,2643+ "double": false,2644+ "color": false,2645+ "high": false,2646+ "low": false,2647+ "tier": null,2648+ "size": null,2649+ "chirality": null,2650+ "span": null,2651+ "count": null,2652+ "segment": null,2653+ "bboxMin": [2654+ 1.53812,2655+ 0,2656+ 1.42657+ ],2658+ "bboxMax": [2659+ 2.46188,2660+ 0.8,2661+ 22662+ ],2663+ "footprint": [2664+ 1,2665+ 12666+ ],2667+ "vertices": 348,2668+ "triangles": 1262669+ },2670+ {2671+ "id": "pipe_open",2672+ "family": "unknown",2673+ "solid": false,2674+ "hole": false,2675+ "wide": false,2676+ "long": false,2677+ "short": false,2678+ "double": false,2679+ "color": false,2680+ "high": false,2681+ "low": false,2682+ "tier": null,2683+ "size": null,2684+ "chirality": null,2685+ "span": null,2686+ "count": null,2687+ "segment": null,2688+ "bboxMin": [2689+ 1.65359,2690+ 0,2691+ 12692+ ],2693+ "bboxMax": [2694+ 2.34641,2695+ 0.6,2696+ 22697+ ],2698+ "footprint": [2699+ 1,2700+ 12701+ ],2702+ "vertices": 280,2703+ "triangles": 1042704+ },2705+ {2706+ "id": "pipe_rampLarge",2707+ "family": "unknown",2708+ "solid": false,2709+ "hole": false,2710+ "wide": false,2711+ "long": false,2712+ "short": false,2713+ "double": false,2714+ "color": false,2715+ "high": false,2716+ "low": false,2717+ "tier": null,2718+ "size": null,2719+ "chirality": null,2720+ "span": null,2721+ "count": null,2722+ "segment": null,2723+ "bboxMin": [2724+ 1.65359,2725+ 0,2726+ 12727+ ],2728+ "bboxMax": [2729+ 2.34641,2730+ 1.2,2731+ 32732+ ],2733+ "footprint": [2734+ 1,2735+ 22736+ ],2737+ "vertices": 672,2738+ "triangles": 1602739+ },2740+ {2741+ "id": "pipe_rampSmall",2742+ "family": "unknown",2743+ "solid": false,2744+ "hole": false,2745+ "wide": false,2746+ "long": false,2747+ "short": false,2748+ "double": false,2749+ "color": false,2750+ "high": false,2751+ "low": false,2752+ "tier": null,2753+ "size": null,2754+ "chirality": null,2755+ "span": null,2756+ "count": null,2757+ "segment": null,2758+ "bboxMin": [2759+ 1.65359,2760+ 0,2761+ 12762+ ],2763+ "bboxMax": [2764+ 2.34641,2765+ 0.9,2766+ 32767+ ],2768+ "footprint": [2769+ 1,2770+ 22771+ ],2772+ "vertices": 672,2773+ "triangles": 1602774+ },2775+ {2776+ "id": "pipe_ring",2777+ "family": "unknown",2778+ "solid": false,2779+ "hole": false,2780+ "wide": false,2781+ "long": false,2782+ "short": false,2783+ "double": false,2784+ "color": false,2785+ "high": false,2786+ "low": false,2787+ "tier": null,2788+ "size": null,2789+ "chirality": null,2790+ "span": null,2791+ "count": null,2792+ "segment": null,2793+ "bboxMin": [2794+ 1.53812,2795+ 0,2796+ 12797+ ],2798+ "bboxMax": [2799+ 2.46188,2800+ 0.8,2801+ 22802+ ],2803+ "footprint": [2804+ 1,2805+ 12806+ ],2807+ "vertices": 752,2808+ "triangles": 1442809+ },2810+ {2811+ "id": "pipe_ringHigh",2812+ "family": "unknown",2813+ "solid": false,2814+ "hole": false,2815+ "wide": false,2816+ "long": false,2817+ "short": false,2818+ "double": false,2819+ "color": false,2820+ "high": false,2821+ "low": false,2822+ "tier": null,2823+ "size": null,2824+ "chirality": null,2825+ "span": null,2826+ "count": null,2827+ "segment": null,2828+ "bboxMin": [2829+ 1.53812,2830+ 0,2831+ 12832+ ],2833+ "bboxMax": [2834+ 2.46188,2835+ 1.3,2836+ 22837+ ],2838+ "footprint": [2839+ 1,2840+ 12841+ ],2842+ "vertices": 1340,2843+ "triangles": 1902844+ },2845+ {2846+ "id": "pipe_ringHighEnd",2847+ "family": "unknown",2848+ "solid": false,2849+ "hole": false,2850+ "wide": false,2851+ "long": false,2852+ "short": false,2853+ "double": false,2854+ "color": false,2855+ "high": false,2856+ "low": false,2857+ "tier": null,2858+ "size": null,2859+ "chirality": null,2860+ "span": null,2861+ "count": null,2862+ "segment": null,2863+ "bboxMin": [2864+ 1.53812,2865+ 0,2866+ 1.22867+ ],2868+ "bboxMax": [2869+ 2.46188,2870+ 1.3,2871+ 22872+ ],2873+ "footprint": [2874+ 1,2875+ 12876+ ],2877+ "vertices": 1360,2878+ "triangles": 1862879+ },2880+ {2881+ "id": "pipe_ringSupport",2882+ "family": "unknown",2883+ "solid": false,2884+ "hole": false,2885+ "wide": false,2886+ "long": false,2887+ "short": false,2888+ "double": false,2889+ "color": false,2890+ "high": false,2891+ "low": false,2892+ "tier": null,2893+ "size": null,2894+ "chirality": null,2895+ "span": null,2896+ "count": null,2897+ "segment": null,2898+ "bboxMin": [2899+ 1.53812,2900+ 0,2901+ 1.32902+ ],2903+ "bboxMax": [2904+ 2.46188,2905+ 1.3,2906+ 1.72907+ ],2908+ "footprint": [2909+ 1,2910+ 12911+ ],2912+ "vertices": 480,2913+ "triangles": 1042914+ },2915+ {2916+ "id": "pipe_split",2917+ "family": "unknown",2918+ "solid": false,2919+ "hole": false,2920+ "wide": false,2921+ "long": false,2922+ "short": false,2923+ "double": false,2924+ "color": false,2925+ "high": false,2926+ "low": false,2927+ "tier": null,2928+ "size": null,2929+ "chirality": null,2930+ "span": null,2931+ "count": null,2932+ "segment": null,2933+ "bboxMin": [2934+ 1.5,2935+ 0,2936+ 12937+ ],2938+ "bboxMax": [2939+ 2.5,2940+ 0.6,2941+ 1.846412942+ ],2943+ "footprint": [2944+ 1,2945+ 12946+ ],2947+ "vertices": 582,2948+ "triangles": 1422949+ },2950+ {2951+ "id": "pipe_straight",2952+ "family": "unknown",2953+ "solid": false,2954+ "hole": false,2955+ "wide": false,2956+ "long": false,2957+ "short": false,2958+ "double": false,2959+ "color": false,2960+ "high": false,2961+ "low": false,2962+ "tier": null,2963+ "size": null,2964+ "chirality": null,2965+ "span": null,2966+ "count": null,2967+ "segment": null,2968+ "bboxMin": [2969+ 1.65359,2970+ 0,2971+ 12972+ ],2973+ "bboxMax": [2974+ 2.34641,2975+ 0.6,2976+ 22977+ ],2978+ "footprint": [2979+ 1,2980+ 12981+ ],2982+ "vertices": 300,2983+ "triangles": 802984+ },2985+ {2986+ "id": "pipe_supportHigh",2987+ "family": "unknown",2988+ "solid": false,2989+ "hole": false,2990+ "wide": false,2991+ "long": false,2992+ "short": false,2993+ "double": false,2994+ "color": false,2995+ "high": false,2996+ "low": false,2997+ "tier": null,2998+ "size": null,2999+ "chirality": null,3000+ "span": null,
diff truncated
M src/client/assets.ts +2 -0
M src/client/endless.ts +39 -2
@@ -87,6 +87,7 @@ import { buildIslandInstanced, type InstancedBuild } from "./islandRender.ts";
8787 import { FanField, fanVolumesOf } from "./fanField.ts";8888 import { buildChuteMesh, extractChannelSection } from "./twistChute.ts";8989 import { Atmosphere, drawZone } from "./atmosphere.ts";90+import { buildVoidDecor, type FloatBuild } from "./voidRender.ts";9091 import type { DecorCatalog } from "../gen/decor.ts";9192 import { genVersion } from "../gen/version.ts";9293 import { drawPipeColor } from "./pipeColor.ts";
@@ -134,6 +135,9 @@ interface BuiltSegment {
134135 entry: { x: number; y: number; z: number; dir: number };135136 /** Per-build GPU state (island instance buffers + cloned materials). */136137 disposables: InstancedBuild[];138+ /** Floating void decor builds (M18b) — ticked in finishFrame once the139+ * spawn wave is done; disposed with the segment. */140+ floats: FloatBuild[];137141 /** Lowest channel base (PLAN-space y, scaled) — fall detection compares138142 * in plan space (a reoriented racer "falls" along its local down). */139143 lowY: number;
@@ -1142,6 +1146,19 @@ class Session {
11421146 missing,11431147 )11441148 );1149+ // Floating void decor (M18b): instanced, flat in the frame-correct1150+ // group (the waves see direct children), physics/occlusion/shadow1151+ // free. Split across twist halves per anchor like placements.1152+ const floats: FloatBuild[] = [];1153+ const voidByGroup = new Map<THREE.Group, Placement[]>();1154+ for (const p of data.voidDecor) {1155+ const g = groupFor(p.pos as [number, number, number]);1156+ voidByGroup.set(g, [...(voidByGroup.get(g) ?? []), p]);1157+ }1158+ for (const [g, list] of voidByGroup) {1159+ const fb = buildVoidDecor(list, this.assets, g, missing);1160+ if (fb) floats.push(fb);1161+ }11451162 // Tunnel render variants (glass / wire) — position-hashed, visual11461163 // only, deterministic per seed.11471164 disposables.push(applyTunnelVariants(group, this.ctx.renderer));
@@ -1259,6 +1276,7 @@ class Session {
12591276 dir: data.plan.entry.dir,12601277 },12611278 disposables,1279+ floats,12621280 lowY: Math.min(...data.plan.cells.map((c) => c.baseY)) * 2,12631281 ...(fr ? { frame: fr } : {}),12641282 ...(postGroup ? { postGroup } : {}),
@@ -1357,7 +1375,10 @@ class Session {
13571375 ),13581376 new THREE.Vector3(step[0], 0, step[1]),13591377 ),1360- disposables: seg.disposables,1378+ // Float builds ride along: ticking stops here (the segment leaves1379+ // this.built — ExitAnim owns the matrices), disposal happens when1380+ // the wave completes.1381+ disposables: [...seg.disposables, ...seg.floats],13611382 });13621383 if (seg.postGroup) {13631384 this.exiting.push({
@@ -1395,7 +1416,16 @@ class Session {
13951416 if (seg.index < lo || seg.index > hi) this.disposeSegment(seg);13961417 }13971418 if (added) {1398- const box = new THREE.Box3().setFromObject(this.root);1419+ // Fit box skips void decor (M18b): a far-band shoal 40u off the1420+ // line would inflate the shadow fit and the camera focus radius.1421+ const box = new THREE.Box3();1422+ this.root.updateWorldMatrix(true, true);1423+ const expand = (o: THREE.Object3D): void => {1424+ if (o.userData.voidDecor) return;1425+ if ((o as THREE.Mesh).isMesh) box.expandByObject(o);1426+ for (const c of o.children) expand(c);1427+ };1428+ expand(this.root);13991429 if (!box.isEmpty()) {14001430 fitShadowsTo(this.ctx.sun, box);14011431 this.cams.frame(box);
@@ -2536,6 +2566,13 @@ class Session {
25362566 for (const sp of built.spinners) {25372567 sp.obj.rotateOnAxis(sp.axis, sp.radPerSec * dt);25382568 }2569+ // Void decor float pass (M18b): only after the assemble wave —2570+ // SpawnAnim owns instance matrices while a segment is spawning2571+ // (writer overlap would fight the wave), and the FloatBuild's own2572+ // envelope ramps the bob in from the handoff.2573+ if (!this.spawning.has(built.index)) {2574+ for (const f of built.floats) f.tick(this.windT);2575+ }25392576 }25402577 25412578 // Stream animations: assembly waves ahead, sink-out behind.
A src/client/voidRender.ts +182 -0
@@ -0,0 +1,182 @@
1+/**2+ * Floating void decor renderer (M18b): one InstancedMesh per (pieceId,3+ * submesh), flat in the segment group so the spawn/exit waves animate4+ * instances exactly like island decor (`animInstances`), plus a per-frame5+ * FLOAT pass — bob (amp·sin over sim time) and slow drift — that owns the6+ * instance matrices only AFTER the assemble wave finished (the session7+ * gates tick() on its spawning/exiting sets; a 2s envelope ramps the bob8+ * in so the handoff never pops).9+ *10+ * Void decor never collides (the generator keeps it out of `placements`),11+ * never casts shadows (the sun fits the track box), and never occludes12+ * the camera (a drifting shoal must not yank the boom): castShadow off,13+ * no `occluder` flag, and every mesh carries `userData.voidDecor` so the14+ * session's fit box skips it (far-band silhouettes would inflate focus15+ * and shadow fit otherwise).16+ */17+import * as THREE from "three";18+import type { Placement } from "../shared/types.ts";19+import type { Assets } from "./assets.ts";20+21+interface FloatParams {22+ amp: number;23+ period: number;24+ phase: number;25+ drift: readonly [number, number, number] | null;26+}27+28+interface AnimInstances {29+ base: THREE.Matrix4[];30+ worldPos: THREE.Vector3[];31+}32+33+export interface FloatBuild {34+ meshes: THREE.InstancedMesh[];35+ /** Advance the float pass to sim time t (seconds). First call stamps36+ * the epoch: bob eases in over ~2s from there, drift time is37+ * segment-local (bounded — a long session never walks a far shoal38+ * across the track). */39+ tick(simT: number): void;40+ dispose(): void;41+}42+43+const tmpM = new THREE.Matrix4();44+const RAMP_SECONDS = 2;45+/** Drift displacement saturates after this many seconds. */46+const DRIFT_CAP_SECONDS = 45;47+48+/** Submeshes of a template with root-relative transforms (the49+ * islandRender walk, minus foliage classification). */50+function subMeshesOf(51+ template: THREE.Object3D,52+): { geometry: THREE.BufferGeometry; material: THREE.Material; local: THREE.Matrix4 }[] {53+ const out: {54+ geometry: THREE.BufferGeometry;55+ material: THREE.Material;56+ local: THREE.Matrix4;57+ }[] = [];58+ const walk = (node: THREE.Object3D, parent: THREE.Matrix4) => {59+ node.updateMatrix();60+ const local = node === template61+ ? new THREE.Matrix4()62+ : new THREE.Matrix4().multiplyMatrices(parent, node.matrix);63+ const mesh = node as THREE.Mesh;64+ if (mesh.isMesh) {65+ out.push({66+ geometry: mesh.geometry,67+ material: mesh.material as THREE.Material,68+ local,69+ });70+ }71+ for (const child of node.children) walk(child, local);72+ };73+ walk(template, new THREE.Matrix4());74+ return out;75+}76+77+export function buildVoidDecor(78+ placements: readonly Placement[],79+ assets: Assets,80+ group: THREE.Group,81+ missing: Set<string>,82+): FloatBuild | null {83+ if (placements.length === 0) return null;84+85+ const byPiece = new Map<string, Placement[]>();86+ for (const p of placements) {87+ const list = byPiece.get(p.pieceId) ?? [];88+ list.push(p);89+ byPiece.set(p.pieceId, list);90+ }91+92+ const meshes: THREE.InstancedMesh[] = [];93+ const floatParams: FloatParams[][] = [];94+ const tmpP = new THREE.Vector3();95+ const tmpQ = new THREE.Quaternion();96+ const tmpS = new THREE.Vector3();97+98+ for (const [pieceId, list] of byPiece) {99+ const template = assets.templates.get(pieceId);100+ if (!template) {101+ missing.add(pieceId);102+ continue;103+ }104+ for (const sub of subMeshesOf(template)) {105+ const imesh = new THREE.InstancedMesh(106+ sub.geometry,107+ sub.material,108+ list.length,109+ );110+ imesh.castShadow = false;111+ imesh.receiveShadow = false;112+ const anim: AnimInstances = { base: [], worldPos: [] };113+ const params: FloatParams[] = [];114+ list.forEach((p, i) => {115+ const s = p.scale ?? 1;116+ tmpP.set(p.pos[0], p.pos[1], p.pos[2]);117+ tmpQ.setFromAxisAngle(118+ new THREE.Vector3(0, 1, 0),119+ p.rotation * Math.PI / 2,120+ );121+ tmpS.set(s, s, s);122+ tmpM.compose(tmpP, tmpQ, tmpS);123+ tmpM.multiply(sub.local);124+ imesh.setMatrixAt(i, tmpM);125+ anim.base.push(tmpM.clone());126+ anim.worldPos.push(tmpP.clone());127+ const f = p.float;128+ params.push({129+ amp: f?.amp ?? 0,130+ period: f?.period ?? 8,131+ phase: f?.phase ?? 0,132+ drift: f?.drift ?? null,133+ });134+ });135+ imesh.instanceMatrix.needsUpdate = true;136+ imesh.computeBoundingSphere();137+ // Bounding sphere must survive the bob/drift walk or instances138+ // vanish to frustum culling at the offset extremes.139+ if (imesh.boundingSphere) imesh.boundingSphere.radius += 12;140+ imesh.userData.animInstances = anim;141+ imesh.userData.voidDecor = true;142+ group.add(imesh);143+ meshes.push(imesh);144+ floatParams.push(params);145+ }146+ }147+148+ if (meshes.length === 0) return null;149+150+ let epoch: number | null = null;151+ return {152+ meshes,153+ tick(simT: number): void {154+ if (epoch === null) epoch = simT;155+ const local = simT - epoch;156+ const env = Math.min(1, local / RAMP_SECONDS);157+ const driftT = Math.min(local, DRIFT_CAP_SECONDS);158+ for (let m = 0; m < meshes.length; m++) {159+ const mesh = meshes[m];160+ const anim = mesh.userData.animInstances as AnimInstances;161+ const params = floatParams[m];162+ for (let i = 0; i < params.length; i++) {163+ const f = params[i];164+ const bob = f.amp * env *165+ Math.sin((2 * Math.PI * (simT + f.phase)) / f.period);166+ tmpM.copy(anim.base[i]);167+ tmpM.elements[13] += bob;168+ if (f.drift) {169+ tmpM.elements[12] += f.drift[0] * driftT;170+ tmpM.elements[13] += f.drift[1] * driftT;171+ tmpM.elements[14] += f.drift[2] * driftT;172+ }173+ mesh.setMatrixAt(i, tmpM);174+ }175+ mesh.instanceMatrix.needsUpdate = true;176+ }177+ },178+ dispose(): void {179+ for (const m of meshes) m.dispose(); // instance buffers only180+ },181+ };182+}
A src/gen/endless/voidDecor.ts +301 -0
@@ -0,0 +1,301 @@
1+/**2+ * Floating void decor (M18b): sparse set-dressing drifting in the empty3+ * air around the line — rock shoals and motes everywhere, biome-flavored4+ * silhouettes near themed islands (ships off tropical coasts, a ghost in5+ * spooky air, snowflakes in winter), rare space events (meteor showers,6+ * a passing craft). Purely atmospheric:7+ *8+ * - NEW LABELED FAMILY (`void-decor`): by the seed law, adding it9+ * perturbs no existing stream.10+ * - SEPARATE OUTPUT (`EndlessSegment.voidDecor`, never `placements`):11+ * the proxy path structurally cannot see it — non-collision by12+ * construction, and it claims nothing in occupancy (decor must never13+ * constrain the line).14+ * - DENSITY IS THE LIVELINESS LAW: 0-2 clusters a segment (expected15+ * ~1) plus a rare event ~0.08 with quiet-after suppression —16+ * distribution over a long stretch, never crowd density.17+ *18+ * Sites hang off the segment's own path samples: a lateral offset into19+ * the mid band (8-16u, slightly below the line) or the far band20+ * (18-40u, scaled up into fog silhouettes). Sites keep ≥CLEARANCE from21+ * every path sample (main + alt), island cells, and any occupancy claim22+ * at their column. Future segments can wander anywhere, so a far shoal23+ * may eventually share air with track generated later — accepted24+ * residual (sparse, non-colliding, reads as parallax).25+ */26+import type { LoopPath, Placement, Vec3 } from "../../shared/types.ts";27+import { rngFamily } from "../rng.ts";28+import type { Rng } from "../rng.ts";29+import { segLength, segPoint } from "../geom.ts";30+import { TRACK_SCALE } from "./scale.ts";31+import type { OccView } from "./occupancy.ts";32+import type { Island } from "./islands.ts";33+34+/** Minimum world distance from any path sample / island cell. */35+export const VOID_CLEARANCE = 6;36+/** Cluster budget per segment (hard cap, the structural-scan law). */37+export const VOID_MAX_PIECES = 14;38+const EVENT_CHANCE = 0.08;39+40+interface VoidGroup {41+ ids: readonly string[];42+ scale: readonly [number, number];43+ weight: number;44+}45+46+/** Always-on shoals: nature rocks read as void-neutral debris. */47+const GENERIC: readonly VoidGroup[] = [48+ {49+ ids: [50+ "nature-kit:rock_largeA",51+ "nature-kit:rock_largeD",52+ "nature-kit:rock_tallB",53+ "nature-kit:stone_largeC",54+ "nature-kit:stone_tallD",55+ ],56+ scale: [0.8, 1.6],57+ weight: 3,58+ },59+ {60+ ids: [61+ "space-kit:meteor_half",62+ "space-kit:rock_crystals",63+ "space-kit:rock_crystalsLargeA",64+ ],65+ scale: [0.9, 1.6],66+ weight: 1.5,67+ },68+];69+70+/** Biome-flavored clusters, keyed by this segment's island biomes. */71+const BIOME_GROUPS: Record<string, readonly VoidGroup[]> = {72+ tropical: [73+ {74+ ids: [75+ "pirate-kit:ship-wreck",76+ "pirate-kit:ship-ghost",77+ "pirate-kit:ship-pirate-small",78+ "pirate-kit:ship-small",79+ ],80+ scale: [0.6, 0.9],81+ weight: 3,82+ },83+ { ids: ["pirate-kit:rocks-sand-a"], scale: [0.25, 0.4], weight: 1.5 },84+ ],85+ spooky: [86+ { ids: ["graveyard-kit:character-ghost"], scale: [1.2, 1.8], weight: 2.5 },87+ { ids: ["graveyard-kit:crypt"], scale: [1.6, 2.4], weight: 1.5 },88+ ],89+ winter: [90+ {91+ ids: [92+ "holiday-kit:snowflake-a",93+ "holiday-kit:snowflake-b",94+ "holiday-kit:snowflake-c",95+ ],96+ scale: [0.8, 1.4],97+ weight: 3,98+ },99+ {100+ ids: ["holiday-kit:rocks-medium", "holiday-kit:rocks-small"],101+ scale: [0.5, 0.8],102+ weight: 2,103+ },104+ ],105+ autumn: [106+ {107+ ids: [108+ "survival-kit:rock-sand-a",109+ "survival-kit:rock-sand-b",110+ "survival-kit:rock-sand-c",111+ ],112+ scale: [0.9, 1.5],113+ weight: 2.5,114+ },115+ ],116+};117+118+/** Rare events: a drifting shoal train or a passing craft, far band. */119+const EVENTS: readonly VoidGroup[] = [120+ {121+ ids: ["space-kit:meteor", "space-kit:meteor_detailed"],122+ scale: [1.5, 2.6],123+ weight: 3,124+ },125+ {126+ ids: [127+ "space-kit:craft_speederA",128+ "space-kit:craft_miner",129+ "space-kit:satelliteDish_detailed",130+ ],131+ scale: [1.2, 2.0],132+ weight: 2,133+ },134+];135+136+interface PathSample {137+ p: Vec3;138+ /** Unit lateral (up × tangent), horizontal. */139+ lat: Vec3;140+}141+142+/** Coarse world-space samples of a path (~2u spacing). */143+function samplePath(path: LoopPath, out: PathSample[]): void {144+ for (const seg of path.segments) {145+ for (const c of seg.curve) {146+ const len = segLength(c);147+ const n = Math.max(1, Math.round(len / 2));148+ for (let k = 0; k <= n; k++) {149+ const p = segPoint(c, k / n);150+ const q = segPoint(c, Math.min(1, k / n + 0.01));151+ const tx = q[0] - p[0];152+ const tz = q[2] - p[2];153+ const tl = Math.hypot(tx, tz);154+ out.push({155+ p,156+ lat: tl > 1e-6 ? [-tz / tl, 0, tx / tl] : [1, 0, 0],157+ });158+ }159+ }160+ }161+}162+163+/** Pure event roll for a segment index — order-independent, so segment164+ * i can consult i−1's outcome without generating it. */165+function eventRolls(seed: string, index: number): boolean {166+ return rngFamily(seed, "void-decor").child(`segment/${index}`).child("event")167+ .chance(EVENT_CHANCE);168+}169+170+export function planVoidDecor(171+ seed: string,172+ index: number,173+ path: LoopPath,174+ altPath: LoopPath | undefined,175+ islands: readonly Island[],176+ occ: OccView,177+): Placement[] {178+ const rng: Rng = rngFamily(seed, "void-decor").child(`segment/${index}`);179+ const out: Placement[] = [];180+181+ const samples: PathSample[] = [];182+ samplePath(path, samples);183+ if (altPath) samplePath(altPath, samples);184+ if (samples.length === 0) return out;185+186+ const isleCells: [number, number][] = [];187+ for (const isle of islands) {188+ for (const c of isle.cells) {189+ const [x, z] = c.split(",").map(Number);190+ isleCells.push([x, z]);191+ }192+ }193+194+ const clear = (x: number, y: number, z: number): boolean => {195+ for (const s of samples) {196+ const dx = x - s.p[0];197+ const dy = y - s.p[1];198+ const dz = z - s.p[2];199+ if (dx * dx + dy * dy + dz * dz < VOID_CLEARANCE * VOID_CLEARANCE) {200+ return false;201+ }202+ }203+ for (const [ix, iz] of isleCells) {204+ const dx = x - ix;205+ const dz = z - iz;206+ if (dx * dx + dz * dz < VOID_CLEARANCE * VOID_CLEARANCE) return false;207+ }208+ // Occupancy column: past/current claims at this cell must not share209+ // the site's band (future claims stay residual — decor claims none).210+ const ty = y / TRACK_SCALE;211+ const bands = occ.at(212+ Math.round(x / TRACK_SCALE),213+ Math.round(z / TRACK_SCALE),214+ );215+ for (const b of bands) {216+ if (ty - 0.75 < b.y1 && b.y0 < ty + 0.75) return false;217+ }218+ return true;219+ };220+221+ const biomes = new Set(islands.map((isle) => isle.biome));222+ const groups: [VoidGroup, number][] = GENERIC.map((223+ g,224+ ) => [g, g.weight] as [VoidGroup, number]);225+ for (const b of biomes) {226+ for (const g of BIOME_GROUPS[b] ?? []) groups.push([g, g.weight * 2]);227+ }228+229+ const emit = (230+ r: Rng,231+ group: VoidGroup,232+ anchor: PathSample,233+ side: number,234+ far: boolean,235+ count: number,236+ ): void => {237+ const latDist = far ? 18 + r.float() * 22 : 8 + r.float() * 8;238+ const yOff = far ? -12 + r.float() * 20 : -6 + r.float() * 10;239+ const cx = anchor.p[0] + anchor.lat[0] * latDist * side;240+ const cz = anchor.p[2] + anchor.lat[2] * latDist * side;241+ const cy = anchor.p[1] + yOff;242+ for (let k = 0; k < count && out.length < VOID_MAX_PIECES; k++) {243+ const px = cx + (r.float() - 0.5) * 6;244+ const py = cy + (r.float() - 0.5) * 4;245+ const pz = cz + (r.float() - 0.5) * 6;246+ if (!clear(px, py, pz)) continue;247+ const scale = group.scale[0] +248+ r.float() * (group.scale[1] - group.scale[0]) * (far ? 1.4 : 1);249+ const period = 6 + r.float() * 8;250+ const drift = far && r.chance(0.6)251+ ? [252+ (r.float() - 0.5) * 0.24,253+ 0,254+ (r.float() - 0.5) * 0.24,255+ ] as Vec3256+ : undefined;257+ out.push({258+ pieceId: r.pick(group.ids as string[]),259+ pos: [px, py, pz],260+ rotation: r.int(0, 4) as Placement["rotation"],261+ role: "decor",262+ scale,263+ float: {264+ amp: 0.1 + r.float() * 0.3,265+ period,266+ phase: r.float() * period,267+ ...(drift ? { drift } : {}),268+ },269+ });270+ }271+ };272+273+ // Clusters: 0-2 a segment, expected ~1.274+ const clusters = rng.child("clusters").weighted(275+ [[0, 1.2], [1, 2.2], [2, 0.8]] as const,276+ );277+ for (let c = 0; c < clusters; c++) {278+ const r = rng.child(`cluster/${c}`);279+ const group = r.weighted(groups);280+ const anchor = samples[r.int(0, samples.length)];281+ emit(282+ r,283+ group,284+ anchor,285+ r.chance(0.5) ? 1 : -1,286+ r.chance(0.45),287+ 1 + r.int(0, 5),288+ );289+ }290+291+ // Rare far-band event, suppressed right after another event so two292+ // never crowd consecutive segments (both rolls are pure in index).293+ if (eventRolls(seed, index) && !(index > 0 && eventRolls(seed, index - 1))) {294+ const r = rng.child("event-build");295+ const group = r.weighted(EVENTS.map((g) => [g, g.weight] as const));296+ const anchor = samples[r.int(0, samples.length)];297+ emit(r, group, anchor, r.chance(0.5) ? 1 : -1, true, 2 + r.int(0, 4));298+ }299+300+ return out;301+}
M src/gen/endless/world.ts +8 -0
M src/shared/types.ts +6 -0
M tests/golden_endless.json +48 -48
@@ -1,59 +1,59 @@
11 {22 "__version": "g491fc4c7a685",33 "alpha": [4- "fc5bb5852166bfc3",5- "2b50b879ec8b708a",6- "b77b964030e615bc",7- "2496a4b6dd5001a5",8- "c295b43fa4a6e21f",9- "365c061d7b201f4f",10- "40b9111a0fead19c",11- "40d88fbb2cf132cd",12- "ed166c0fbc83dd10",13- "f33a019c11f552c8",14- "078749c92db0603d",15- "e1bed1c9ac8df99d"4+ "2301fe8351939c11",5+ "871af9903d52caa8",6+ "a4a5bce81ce5960f",7+ "afb9de4dd5d431fe",8+ "9aa74add78799dac",9+ "bd57986bdb77189e",10+ "75a5037e72ba44f2",11+ "3e9ca1ceba2578d9",12+ "c2e1aa8597fc836f",13+ "352971e191d9728d",14+ "06ecf63f454cfed9",15+ "cb2571d849301758"1616 ],1717 "zx9k2m4p1q": [18- "5549417fb16e95ed",19- "ef39a57246d72895",20- "e876fa7b8c9bf5b5",21- "37f5afccbce628d4",22- "1b8090583d29674c",23- "67de521fd10da071",24- "9ccfd536cf74b87f",25- "36b203e391cbcc54",26- "666b8d0bc7c21597",27- "3dfdf8b8a7f410d3",28- "08dde2a3787ea70e",29- "c2f84e64a75356d4"18+ "d71e9ebc0003f12f",19+ "112a943058f2929d",20+ "40921f6f098a50a8",21+ "180d6a4505e0328b",22+ "9f69d40d1333cb2d",23+ "cec0a275de871833",24+ "7215ea2d06a3c990",25+ "5099dc10e08aad5f",26+ "6b6184fc9a642961",27+ "59bdf1590b49428a",28+ "e7c31d2e4fdcfe95",29+ "de2d58870eb8b6db"3030 ],3131 "dami": [32- "5e2a002860fda25e",33- "46eaec3aafe537f1",34- "d7abca4a3d544644",35- "04b346f980745fbe",36- "d145cfec2788560d",37- "48b4fc82718106c6",38- "22ccdd982057c862",39- "6a04b3a171cf20d2",40- "d667aad8f4067140",41- "a9f1f90ea2f1aa2b",42- "31926b484090dbf3",43- "4ec26ba1e23d18de"32+ "30afe7333e4f8093",33+ "1a3b97a3750a6e90",34+ "caca8a0bb120466f",35+ "490024f85c78e457",36+ "24df67ad9c1af1b9",37+ "8c901bc15e00a673",38+ "53a805d9b41c764c",39+ "3aa866ecc03656e0",40+ "be4bd2617188131e",41+ "f55825096afd639b",42+ "fec3cd94aa504386",43+ "b906f7aab6f942a9"4444 ],4545 "marbles": [46- "c5ef91cdb62a327c",47- "1e950170d28a8696",48- "473e361652bbfa29",49- "c79a3482a8acd1fa",50- "6f4651658099186f",51- "f9e1993574c18cd9",52- "df3acd6d48439183",53- "33a486156155368b",54- "5306836210fbaf9a",55- "4a0e8f0e1abba63b",56- "22039caa827a549e",57- "e53d8ef37deb26ff"46+ "2c2040556134a62e",47+ "7cc8312c3da8b26d",48+ "2b60d112e1b7ad0a",49+ "f2bfea4f444874f9",50+ "30ff7e88a1caab15",51+ "45f1bb8759e2b972",52+ "044b086c9f69dca5",53+ "e77b8633d9e2015a",54+ "082b0b12bf2becf5",55+ "ae7e93c04de083b3",56+ "e089bf39f8898247",57+ "09841e8d72c5e4fd"5858 ]5959 }
A tests/void_decor_test.ts +135 -0
@@ -0,0 +1,135 @@
1+/**2+ * Floating void decor (M18b): deterministic, sparse, clear of the line,3+ * and STRUCTURALLY collision-free — the proxy path never sees the4+ * voidDecor field.5+ */6+import { assert, assertEquals } from "@std/assert";7+import { EndlessWorld } from "../src/gen/endless/world.ts";8+import {9+ bumperProxies,10+ islandProxies,11+ segmentProxies,12+} from "../src/gen/endless/proxies.ts";13+import {14+ VOID_CLEARANCE,15+ VOID_MAX_PIECES,16+} from "../src/gen/endless/voidDecor.ts";17+import { segLength, segPoint } from "../src/gen/geom.ts";18+import type { DecorCatalog } from "../src/gen/decor.ts";19+import { goldenCatalog } from "../tools/pin_endless.ts";20+21+const catalog: DecorCatalog = goldenCatalog();22+23+Deno.test("void decor: identical across worlds of the same seed", () => {24+ const a = new EndlessWorld("void-det", catalog);25+ const b = new EndlessWorld("void-det", catalog);26+ for (let i = 0; i < 12; i++) {27+ assertEquals(a.get(i).voidDecor, b.get(i).voidDecor, `segment ${i}`);28+ }29+});30+31+Deno.test("void decor: sparse, resolvable, floating, clear of the line", () => {32+ const known = new Map(catalog.packs.map((p) => [p.slug, new Set(p.ids)]));33+ let pieces = 0;34+ let drifted = 0;35+ const seenPacks = new Set<string>();36+ for (let n = 0; n < 30; n++) {37+ const w = new EndlessWorld(`void-${n}`, catalog);38+ for (let i = 0; i < 8; i++) {39+ const seg = w.get(i);40+ assert(41+ seg.voidDecor.length <= VOID_MAX_PIECES,42+ `void-${n} seg ${i}: ${seg.voidDecor.length} pieces over budget`,43+ );44+ // Path samples for the clearance re-check (main + alt).45+ const samples: (readonly [number, number, number] | number[])[] = [];46+ for (47+ const path of [seg.path, ...(seg.altPath ? [seg.altPath] : [])]48+ ) {49+ for (const s of path.segments) {50+ for (const c of s.curve) {51+ const steps = Math.max(1, Math.round(segLength(c) / 2));52+ for (let k = 0; k <= steps; k++) samples.push(segPoint(c, k / steps));53+ }54+ }55+ }56+ for (const p of seg.voidDecor) {57+ pieces++;58+ assertEquals(p.role, "decor", "void decor must be decor-role");59+ assert(p.float, `void-${n} seg ${i}: piece without float params`);60+ assert(61+ p.float!.amp > 0 && p.float!.period > 0,62+ "float params must be positive",63+ );64+ if (p.float!.drift) drifted++;65+ const colon = p.pieceId.indexOf(":");66+ assert(colon > 0, `unprefixed void piece ${p.pieceId}`);67+ seenPacks.add(p.pieceId.slice(0, colon));68+ assert(69+ known.get(p.pieceId.slice(0, colon))?.has(p.pieceId.slice(colon + 1)),70+ `void-${n} seg ${i}: unresolvable ${p.pieceId}`,71+ );72+ for (const s of samples) {73+ const d = Math.hypot(74+ p.pos[0] - s[0],75+ p.pos[1] - s[1],76+ p.pos[2] - s[2],77+ );78+ assert(79+ d >= VOID_CLEARANCE - 1e-9,80+ `void-${n} seg ${i}: ${p.pieceId} ${d.toFixed(2)}u from the line`,81+ );82+ }83+ }84+ }85+ }86+ // Distribution law: sparse but alive — and multi-pack across a corpus.87+ assert(pieces > 50, `only ${pieces} void pieces across the corpus`);88+ assert(pieces < 30 * 8 * VOID_MAX_PIECES * 0.5, "void decor reads as crowd");89+ assert(drifted > 0, "no drifting far-band pieces emerged");90+ assert(seenPacks.size >= 2, `only packs ${[...seenPacks]} emerged`);91+});92+93+Deno.test("void decor: proxies are blind to it (non-collision contract)", () => {94+ const w = new EndlessWorld("void-det", catalog);95+ for (let i = 0; i < 6; i++) {96+ const seg = w.get(i);97+ if (seg.voidDecor.length === 0) continue;98+ const withVoid = [99+ ...segmentProxies(seg),100+ ...seg.islands.flatMap((isle) => islandProxies(isle.placements)),101+ ...bumperProxies(seg.placements),102+ ];103+ const stripped = { ...seg, voidDecor: [] };104+ const without = [105+ ...segmentProxies(stripped),106+ ...stripped.islands.flatMap((isle) => islandProxies(isle.placements)),107+ ...bumperProxies(stripped.placements),108+ ];109+ assertEquals(withVoid.length, without.length, `segment ${i}`);110+ }111+});112+113+Deno.test("void decor: events are rare and never consecutive", () => {114+ // The quiet-after rule: two far-band events never land on back-to-back115+ // segments. Detect events structurally (space-kit craft/meteor far116+ // clusters are the event vocabulary's signature pieces).117+ const EVENT_IDS = /^space-kit:(meteor$|meteor_detailed$|craft_|satelliteDish)/;118+ let events = 0;119+ let segments = 0;120+ for (let n = 0; n < 20; n++) {121+ const w = new EndlessWorld(`void-ev-${n}`, catalog);122+ let prev = false;123+ for (let i = 0; i < 10; i++) {124+ segments++;125+ const has = w.get(i).voidDecor.some((p) => EVENT_IDS.test(p.pieceId));126+ if (has) {127+ events++;128+ assert(!prev, `void-ev-${n}: events on consecutive segments ${i - 1},${i}`);129+ }130+ prev = has;131+ }132+ }133+ assert(events > 0, "no void events across the corpus");134+ assert(events / segments < 0.2, `events too dense: ${events}/${segments}`);135+});
M tools/import_assets.ts +2 -0
M tools/pin_endless.ts +11 -0