@marble.fun/track-catalog 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/assets.json +346 -0
  4. package/package.json +32 -0
  5. package/release.json +135 -0
  6. package/src/audio/game-audio-preference.ts +9 -0
  7. package/src/authoritative.ts +6 -0
  8. package/src/catalog.ts +1 -0
  9. package/src/index.ts +14 -0
  10. package/src/legacy-stock-catalog.ts +130 -0
  11. package/src/pages/lobby/lobby-types.ts +140 -0
  12. package/src/pages/lobby/marble-factory.ts +931 -0
  13. package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
  14. package/src/pages/play/race/egypt-portal-frame.ts +382 -0
  15. package/src/pages/play/race/finish-detection.ts +1 -0
  16. package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
  17. package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
  18. package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
  19. package/src/pages/play/race/jungle-portal-frame.ts +308 -0
  20. package/src/pages/play/race/jungle-temple-lift.ts +366 -0
  21. package/src/pages/play/race/marble-entity.ts +257 -0
  22. package/src/pages/play/race/marble-material.ts +20 -0
  23. package/src/pages/play/race/marble-race-engine.ts +2652 -0
  24. package/src/pages/play/race/path-pattern-layout.ts +142 -0
  25. package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
  26. package/src/pages/play/race/physics-debug.ts +1 -0
  27. package/src/pages/play/race/physics-dry-run.ts +90 -0
  28. package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
  29. package/src/pages/play/race/portal-pair-color.ts +20 -0
  30. package/src/pages/play/race/portal-proportions.ts +25 -0
  31. package/src/pages/play/race/portal-visual.ts +1108 -0
  32. package/src/pages/play/race/race-camera-director.ts +586 -0
  33. package/src/pages/play/race/race-camera-obstruction.ts +37 -0
  34. package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
  35. package/src/pages/play/race/race-camera.ts +206 -0
  36. package/src/pages/play/race/race-completion.ts +5 -0
  37. package/src/pages/play/race/race-configuration.ts +57 -0
  38. package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
  39. package/src/pages/play/race/race-physics-profile.ts +7 -0
  40. package/src/pages/play/race/race-physics.ts +16 -0
  41. package/src/pages/play/race/race-playback.ts +41 -0
  42. package/src/pages/play/race/race-rolling-audio.ts +616 -0
  43. package/src/pages/play/race/race-simulation-renderer.ts +221 -0
  44. package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
  45. package/src/pages/play/race/race-transform-interpolation.ts +50 -0
  46. package/src/pages/play/race/race-types.ts +12 -0
  47. package/src/pages/play/race/rebound-visual.ts +70 -0
  48. package/src/pages/play/race/seeded-random.ts +23 -0
  49. package/src/pages/play/race/space-station-portal-frame.ts +275 -0
  50. package/src/pages/play/race/track-appearance-canopy.ts +946 -0
  51. package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
  52. package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
  53. package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
  54. package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
  55. package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
  56. package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
  57. package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
  58. package/src/pages/play/race/track-appearance-support.ts +983 -0
  59. package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
  60. package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
  61. package/src/pages/play/race/track-course.ts +271 -0
  62. package/src/pages/play/race/track-definition.ts +443 -0
  63. package/src/pages/play/race/track-fence-architecture.ts +33 -0
  64. package/src/pages/play/race/track-floor-material-rules.ts +51 -0
  65. package/src/pages/play/race/track-mechanisms.ts +1350 -0
  66. package/src/pages/play/race/track-physics-surface.ts +6 -0
  67. package/src/pages/play/race/track-route-network.ts +9 -0
  68. package/src/pages/play/race/track-shadow-batching.ts +760 -0
  69. package/src/pages/play/race/track-surface-basis.ts +1 -0
  70. package/src/pages/play/race/track-surface-topology.ts +326 -0
  71. package/src/pages/play/race/track-world.ts +10181 -0
  72. package/src/pages/play/race/under-track-support-architecture.ts +122 -0
  73. package/src/pages/play/race/vortex-profile.ts +5 -0
  74. package/src/pages/play/race/vortex-water-effect.ts +863 -0
  75. package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
  76. package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
  77. package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
  78. package/src/pages/track-lab/cone-field-layout.ts +1 -0
  79. package/src/pages/track-lab/conveyor-layout.ts +1 -0
  80. package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
  81. package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
  82. package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
  83. package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
  84. package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
  85. package/src/pages/track-lab/flipper-component.ts +127 -0
  86. package/src/pages/track-lab/rising-wall-layout.ts +1 -0
  87. package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
  88. package/src/pages/track-lab/track-piece-layout.ts +28 -0
  89. package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
  90. package/src/three/geometry-attribute-aliases.ts +24 -0
  91. package/src/three/geometry-detail.ts +36 -0
  92. package/src/three/homepage-marble-geometry.ts +15 -0
  93. package/src/three/japanese-sakura-appearance.ts +252 -0
  94. package/src/three/jungle-flower-hedge-appearance.ts +7 -0
  95. package/src/three/jungle-temple-appearance.ts +5 -0
  96. package/src/three/low-quality-scene-renderer.ts +132 -0
  97. package/src/three/marble-sphere-geometry.ts +13 -0
  98. package/src/three/mobile-rendering-budget.ts +11 -0
  99. package/src/three/painted-valley-appearance.ts +270 -0
  100. package/src/three/railroad-appearance.ts +71 -0
  101. package/src/three/renderer-resize.ts +32 -0
  102. package/src/three/rope-wall-appearance.ts +91 -0
  103. package/src/three/rugged-track-appearance.ts +2 -0
  104. package/src/three/scene-lighting.ts +183 -0
  105. package/src/three/scene-shader-preparation.ts +64 -0
  106. package/src/three/scene-theme-controller.ts +461 -0
  107. package/src/three/shadow-shader-preparation.ts +162 -0
  108. package/src/three/shared-obstacle-metal.ts +44 -0
  109. package/src/three/soft-additive-particles.ts +57 -0
  110. package/src/three/space-station-appearance.ts +213 -0
  111. package/src/three/static-track-batched-mesh.ts +132 -0
  112. package/src/three/stone-track-shell-appearance.ts +12 -0
  113. package/src/three/theme-ambience-controller.ts +226 -0
  114. package/src/three/theme-asset-cache.ts +120 -0
  115. package/src/three/theme-audio-preference.ts +43 -0
  116. package/src/three/theme-catalog.ts +1457 -0
  117. package/src/three/theme-material-assets.ts +258 -0
  118. package/src/three/theme-material-ktx2-manifest.ts +190 -0
  119. package/src/three/theme-material-registry.ts +1952 -0
  120. package/src/three/theme-world-factory.ts +590 -0
  121. package/src/three/track-connector-batch.ts +125 -0
  122. package/src/three/track-material-continuity.ts +442 -0
  123. package/src/three/track-theme.ts +535 -0
  124. package/src/three/vampire-castle-appearance.ts +258 -0
  125. package/src/three/visual-frame-pacing.ts +19 -0
  126. package/src/three/visual-geometry.ts +120 -0
  127. package/src/three/visual-quality.ts +83 -0
  128. package/src/three/water-appearance.ts +29 -0
  129. package/src/three/wild-west-sand-appearance.ts +2 -0
@@ -0,0 +1,497 @@
1
+ import * as THREE from 'three'
2
+
3
+ import { VisualCircleGeometry } from '../../../three/visual-geometry'
4
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
5
+
6
+ import { TRACK_RAIL_WALL_THICKNESS } from './track-definition'
7
+ import {
8
+ canonicalWallFrameAt,
9
+ type CanonicalWallRun,
10
+ type RawWallFrame,
11
+ } from './track-surface-topology'
12
+
13
+ export type SpaceStationWallPathFrame = RawWallFrame
14
+ export type SpaceStationWallPathRun = CanonicalWallRun
15
+
16
+ export interface SpaceStationWallMaterials {
17
+ chassis: THREE.Material
18
+ panel: THREE.Material
19
+ rib: THREE.Material
20
+ trim: THREE.Material
21
+ light: THREE.Material
22
+ }
23
+
24
+ interface BandBatch {
25
+ name: string
26
+ material: THREE.Material
27
+ geometries: THREE.BufferGeometry[]
28
+ castShadow: boolean
29
+ receiveShadow: boolean
30
+ }
31
+
32
+ const BAY_TARGET = 3.2
33
+ const SAMPLE_SPACING = 0.9
34
+ const SAMPLE_MAXIMUM_TURN = THREE.MathUtils.degToRad(8)
35
+ const RIB_WIDTH = 0.2
36
+ const RIB_DEPTH = 0.03
37
+ const RIB_BACKING_WIDTH = 0.29
38
+ const RIB_BACKING_DEPTH = 0.024
39
+ const MAX_FACADE_LIFT = 0.029
40
+ const WORLD_UP = new THREE.Vector3(0, 1, 0)
41
+
42
+ /** Adds the visual-only, two-sided armored facade over the existing wall collider. */
43
+ export function addSpaceStationWallVisual(
44
+ root: THREE.Group,
45
+ id: string,
46
+ runs: readonly SpaceStationWallPathRun[],
47
+ wallHeight: number,
48
+ materials: SpaceStationWallMaterials,
49
+ ): THREE.Group | undefined {
50
+ if (wallHeight <= 0.18) return undefined
51
+ const group = new THREE.Group()
52
+ group.name = `${id}-space-station-armored-wall`
53
+ group.userData.themeOnly = 'intergalactic-space-station'
54
+ group.userData.decorative = true
55
+ group.userData.visualOnly = true
56
+ group.userData.appearance = 'space-station-armored-wall'
57
+
58
+ const ribMatrices: THREE.Matrix4[] = []
59
+ const ribBackingMatrices: THREE.Matrix4[] = []
60
+ const ribSpineMatrices: THREE.Matrix4[] = []
61
+ const clampMatrices: THREE.Matrix4[] = []
62
+ const headerPlateMatrices: THREE.Matrix4[] = []
63
+ const boltMatrices: THREE.Matrix4[] = []
64
+ const panelBands: BandBatch = {
65
+ name: `${id}-space-wall-panel-batch`,
66
+ material: materials.panel,
67
+ geometries: [],
68
+ castShadow: true,
69
+ receiveShadow: true,
70
+ }
71
+ const chassisBands: BandBatch = {
72
+ name: `${id}-space-wall-chassis-batch`,
73
+ material: materials.chassis,
74
+ geometries: [],
75
+ castShadow: true,
76
+ receiveShadow: true,
77
+ }
78
+ const trimBands: BandBatch = {
79
+ name: `${id}-space-wall-trim-batch`,
80
+ material: materials.trim,
81
+ geometries: [],
82
+ castShadow: true,
83
+ receiveShadow: true,
84
+ }
85
+ const lightBands: BandBatch = {
86
+ name: `${id}-space-wall-light-batch`,
87
+ material: materials.light,
88
+ geometries: [],
89
+ castShadow: false,
90
+ receiveShadow: false,
91
+ }
92
+ let bayCount = 0
93
+ let runCount = 0
94
+
95
+ for (const run of runs) {
96
+ if (run.frames.length < 2) continue
97
+ const canonicalRun = run
98
+ const totalLength = canonicalRun.length
99
+ if (totalLength <= 0.08) continue
100
+ runCount += 1
101
+ const count = Math.max(1, Math.round(totalLength / BAY_TARGET))
102
+ const bayLength = totalLength / count
103
+ bayCount += count
104
+ const chassisFrames = sampleCanonicalRange(canonicalRun, 0, totalLength)
105
+ for (const face of [-1, 1] as const) {
106
+ queueBand(chassisBands, chassisFrames, face, 0.03, wallHeight - 0.01, 0.001)
107
+ }
108
+
109
+ for (let bayIndex = 0; bayIndex < count; bayIndex += 1) {
110
+ const bayStart = bayIndex * bayLength
111
+ const bayEnd = (bayIndex + 1) * bayLength
112
+ const border = Math.min(0.11, bayLength * 0.12)
113
+ const panelFrames = sampleCanonicalRange(canonicalRun, bayStart + border, bayEnd - border)
114
+ const lightInset = Math.min(0.2, bayLength * 0.18)
115
+ const lightFrames = sampleCanonicalRange(
116
+ canonicalRun,
117
+ bayStart + border + lightInset,
118
+ bayEnd - border - lightInset,
119
+ )
120
+ for (const face of [-1, 1] as const) {
121
+ queueBand(
122
+ panelBands,
123
+ panelFrames,
124
+ face,
125
+ 0.16,
126
+ wallHeight - 0.16,
127
+ 0.006,
128
+ 0.07,
129
+ )
130
+ for (const [, bottom, top, lift, material] of [
131
+ ['footer', 0.07, 0.16, 0.008, materials.panel],
132
+ ['lower-bezel', 0.15, 0.245, 0.012, materials.trim],
133
+ ['upper-bezel', wallHeight - 0.245, wallHeight - 0.15, 0.012, materials.trim],
134
+ ['header', wallHeight - 0.16, wallHeight - 0.07, 0.008, materials.panel],
135
+ ] as const) {
136
+ queueBand(
137
+ material === materials.panel ? panelBands : trimBands,
138
+ panelFrames,
139
+ face,
140
+ bottom,
141
+ top,
142
+ lift,
143
+ 0.035,
144
+ )
145
+ }
146
+ for (const center of [0.215, wallHeight - 0.215]) {
147
+ queueBand(
148
+ trimBands,
149
+ lightFrames,
150
+ face,
151
+ center - 0.035,
152
+ center + 0.035,
153
+ 0.018,
154
+ 0.012,
155
+ )
156
+ queueBand(
157
+ lightBands,
158
+ lightFrames,
159
+ face,
160
+ center - 0.014,
161
+ center + 0.014,
162
+ 0.022,
163
+ 0.007,
164
+ )
165
+ }
166
+ queueBand(
167
+ trimBands,
168
+ lightFrames,
169
+ face,
170
+ wallHeight - 0.132,
171
+ wallHeight - 0.118,
172
+ 0.015,
173
+ 0.006,
174
+ )
175
+ queueBand(
176
+ trimBands,
177
+ panelFrames,
178
+ face,
179
+ wallHeight - 0.072,
180
+ wallHeight - 0.035,
181
+ 0.014,
182
+ 0.014,
183
+ )
184
+
185
+ const headerFrame = canonicalWallFrameAt(canonicalRun, (bayStart + bayEnd) / 2)
186
+ const headerOutward = horizontalWallOutward(
187
+ headerFrame,
188
+ headerFrame.outward.clone().multiplyScalar(face),
189
+ )
190
+ const headerRotation = verticalFrameQuaternion(headerFrame, headerOutward)
191
+ const headerTangent = horizontalFrameTangent(headerFrame, headerOutward)
192
+ const headerPosition = headerFrame.base
193
+ .clone()
194
+ .addScaledVector(WORLD_UP, wallHeight - 0.105)
195
+ .addScaledVector(headerOutward, TRACK_RAIL_WALL_THICKNESS / 2 + 0.023)
196
+ headerPlateMatrices.push(
197
+ new THREE.Matrix4().compose(headerPosition, headerRotation, new THREE.Vector3(0.16, 0.052, 0.008)),
198
+ )
199
+ for (const tangentOffset of [-0.052, 0.052]) {
200
+ const boltPosition = headerPosition
201
+ .clone()
202
+ .addScaledVector(headerTangent, tangentOffset)
203
+ .addScaledVector(headerOutward, 0.007)
204
+ const boltRotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), headerOutward)
205
+ boltMatrices.push(
206
+ new THREE.Matrix4().compose(boltPosition, boltRotation, new THREE.Vector3(0.72, 0.72, 0.72)),
207
+ )
208
+ }
209
+ }
210
+ }
211
+
212
+ for (let boundary = 0; boundary <= count; boundary += 1) {
213
+ const frame = canonicalWallFrameAt(canonicalRun, Math.min(totalLength, boundary * bayLength))
214
+ for (const face of [-1, 1] as const) {
215
+ const outward = horizontalWallOutward(frame, frame.outward.clone().multiplyScalar(face))
216
+ const rotation = verticalFrameQuaternion(frame, outward)
217
+ const ribHeight = wallHeight + 0.08
218
+ const position = frame.base
219
+ .clone()
220
+ .addScaledVector(WORLD_UP, ribHeight / 2 - 0.025)
221
+ .addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + RIB_DEPTH / 2 + 0.006)
222
+ ribMatrices.push(
223
+ new THREE.Matrix4().compose(position, rotation, new THREE.Vector3(1, ribHeight / wallHeight, 1)),
224
+ )
225
+ const backingPosition = frame.base
226
+ .clone()
227
+ .addScaledVector(WORLD_UP, ribHeight / 2 - 0.025)
228
+ .addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + RIB_BACKING_DEPTH / 2 + 0.002)
229
+ ribBackingMatrices.push(
230
+ new THREE.Matrix4().compose(backingPosition, rotation, new THREE.Vector3(1, ribHeight / wallHeight, 1)),
231
+ )
232
+ const spinePosition = frame.base
233
+ .clone()
234
+ .addScaledVector(WORLD_UP, ribHeight / 2 - 0.025)
235
+ .addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + MAX_FACADE_LIFT - 0.004)
236
+ ribSpineMatrices.push(
237
+ new THREE.Matrix4().compose(
238
+ spinePosition,
239
+ rotation,
240
+ new THREE.Vector3(0.075, Math.max(0.3, ribHeight - 0.46), 0.008),
241
+ ),
242
+ )
243
+
244
+ for (const clampHeight of [0.2, wallHeight - 0.2]) {
245
+ const clampPosition = frame.base
246
+ .clone()
247
+ .addScaledVector(WORLD_UP, clampHeight)
248
+ .addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + MAX_FACADE_LIFT)
249
+ clampMatrices.push(
250
+ new THREE.Matrix4().compose(clampPosition, rotation, new THREE.Vector3(0.105, 0.12, 0.008)),
251
+ )
252
+ for (const boltOffset of [-0.045, 0.045]) {
253
+ const boltPosition = clampPosition
254
+ .clone()
255
+ .addScaledVector(WORLD_UP, boltOffset)
256
+ .addScaledVector(outward, 0.005)
257
+ const boltRotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), outward)
258
+ boltMatrices.push(new THREE.Matrix4().compose(boltPosition, boltRotation, new THREE.Vector3(1, 1, 1)))
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ if (runCount === 0) return undefined
266
+ addBandBatch(group, chassisBands)
267
+ addBandBatch(group, panelBands)
268
+ addBandBatch(group, trimBands)
269
+ addBandBatch(group, lightBands)
270
+ if (ribBackingMatrices.length > 0)
271
+ group.add(
272
+ instancedBatch(
273
+ `${id}-space-wall-rib-backings`,
274
+ createWrappedRibGeometry(wallHeight, RIB_BACKING_WIDTH, RIB_BACKING_DEPTH),
275
+ materials.trim,
276
+ ribBackingMatrices,
277
+ ),
278
+ )
279
+ if (ribMatrices.length > 0)
280
+ group.add(
281
+ instancedBatch(
282
+ `${id}-space-wall-ribs`,
283
+ createWrappedRibGeometry(wallHeight, RIB_WIDTH, RIB_DEPTH),
284
+ materials.rib,
285
+ ribMatrices,
286
+ ),
287
+ )
288
+ if (ribSpineMatrices.length > 0)
289
+ group.add(
290
+ instancedBatch(`${id}-space-wall-rib-spines`, new THREE.BoxGeometry(1, 1, 1), materials.panel, ribSpineMatrices),
291
+ )
292
+ if (clampMatrices.length > 0)
293
+ group.add(instancedBatch(`${id}-space-wall-clamps`, new THREE.BoxGeometry(1, 1, 1), materials.trim, clampMatrices))
294
+ if (headerPlateMatrices.length > 0)
295
+ group.add(
296
+ instancedBatch(
297
+ `${id}-space-wall-header-service-plates`,
298
+ new THREE.BoxGeometry(1, 1, 1),
299
+ materials.trim,
300
+ headerPlateMatrices,
301
+ ),
302
+ )
303
+ if (boltMatrices.length > 0)
304
+ group.add(
305
+ instancedBatch(
306
+ `${id}-space-wall-bolts`,
307
+ // Six sides are already sub-pixel at the authored bolt radius.
308
+ new VisualCircleGeometry(0.009, 6).rotateX(-Math.PI / 2).translate(0, 0.004, 0),
309
+ materials.trim,
310
+ boltMatrices,
311
+ ),
312
+ )
313
+
314
+ group.userData.runCount = runCount
315
+ group.userData.bayCount = bayCount
316
+ group.userData.ribCount = ribMatrices.length
317
+ root.add(group)
318
+ return group
319
+ }
320
+
321
+ function queueBand(
322
+ batch: BandBatch,
323
+ frames: readonly SpaceStationWallPathFrame[],
324
+ face: -1 | 1,
325
+ bottom: number,
326
+ top: number,
327
+ lift: number,
328
+ chamfer = 0,
329
+ ): void {
330
+ if (frames.length < 2 || top <= bottom) return
331
+ batch.geometries.push(createBandGeometry(frames, face, bottom, top, lift, chamfer))
332
+ }
333
+
334
+ function addBandBatch(group: THREE.Group, batch: BandBatch): void {
335
+ if (batch.geometries.length === 0) return
336
+ const geometry = mergeGeometries(batch.geometries, false)
337
+ batch.geometries.forEach(part => part.dispose())
338
+ if (!geometry) throw new Error(`Could not merge ${batch.name}`)
339
+ geometry.name = 'space-station-armored-wall-batched-band-geometry'
340
+ geometry.computeBoundingBox()
341
+ geometry.computeBoundingSphere()
342
+ const mesh = new THREE.Mesh(geometry, batch.material)
343
+ mesh.name = batch.name
344
+ mesh.userData.decorative = true
345
+ mesh.userData.visualOnly = true
346
+ mesh.userData.bandCount = batch.geometries.length
347
+ mesh.castShadow = batch.castShadow
348
+ mesh.receiveShadow = batch.receiveShadow
349
+ group.add(mesh)
350
+ }
351
+
352
+ function createBandGeometry(
353
+ frames: readonly SpaceStationWallPathFrame[],
354
+ face: -1 | 1,
355
+ bottom: number,
356
+ top: number,
357
+ lift: number,
358
+ chamfer: number,
359
+ ): THREE.BufferGeometry {
360
+ const distances = bandPathDistances(frames)
361
+ const totalLength = distances.at(-1) ?? 1
362
+ const positions: number[] = []
363
+ const uvs: number[] = []
364
+ const metricUvs: number[] = []
365
+ const indices: number[] = []
366
+ frames.forEach((frame, index) => {
367
+ const outward = horizontalWallOutward(frame, frame.outward.clone().multiplyScalar(face))
368
+ const endChamfer = index === 0 || index === frames.length - 1 ? chamfer : 0
369
+ for (const height of [bottom + endChamfer, top - endChamfer]) {
370
+ const point = frame.base
371
+ .clone()
372
+ .addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + lift)
373
+ .addScaledVector(WORLD_UP, height - 0.03)
374
+ positions.push(point.x, point.y, point.z)
375
+ uvs.push(distances[index]! / totalLength, (height - bottom) / Math.max(0.001, top - bottom))
376
+ metricUvs.push(distances[index]!, height, 1, totalLength)
377
+ }
378
+ })
379
+ for (let index = 0; index < frames.length - 1; index += 1) {
380
+ const a = index * 2
381
+ const b = a + 1
382
+ const c = a + 2
383
+ const d = a + 3
384
+ const desired = horizontalWallOutward(frames[index]!, frames[index]!.outward.clone().multiplyScalar(face))
385
+ appendOrientedTriangle(indices, positions, a, b, c, desired)
386
+ appendOrientedTriangle(indices, positions, b, d, c, desired)
387
+ }
388
+ const geometry = new THREE.BufferGeometry()
389
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
390
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
391
+ geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
392
+ geometry.setIndex(indices)
393
+ geometry.computeVertexNormals()
394
+ geometry.computeBoundingBox()
395
+ geometry.computeBoundingSphere()
396
+ geometry.name = 'space-station-armored-wall-band-geometry'
397
+ return geometry
398
+ }
399
+
400
+ function appendOrientedTriangle(
401
+ indices: number[],
402
+ positions: readonly number[],
403
+ a: number,
404
+ b: number,
405
+ c: number,
406
+ desired: THREE.Vector3,
407
+ ): void {
408
+ const point = (index: number) =>
409
+ new THREE.Vector3(positions[index * 3]!, positions[index * 3 + 1]!, positions[index * 3 + 2]!)
410
+ const normal = point(b)
411
+ .sub(point(a))
412
+ .cross(point(c).sub(point(a)))
413
+ if (normal.dot(desired) >= 0) indices.push(a, b, c)
414
+ else indices.push(a, c, b)
415
+ }
416
+
417
+ function createWrappedRibGeometry(wallHeight: number, width: number, depth: number): THREE.BoxGeometry {
418
+ // The repeated ribs are millimetres deep: a closed rectangular strip keeps
419
+ // their silhouette without bevel loops on thousands of instances.
420
+ const geometry = new THREE.BoxGeometry(width, wallHeight, depth)
421
+ geometry.name = 'space-station-wrapped-rib-geometry'
422
+ return geometry
423
+ }
424
+
425
+ function verticalFrameQuaternion(frame: SpaceStationWallPathFrame, outwardHint: THREE.Vector3): THREE.Quaternion {
426
+ const up = WORLD_UP
427
+ const outward = horizontalWallOutward(frame, outwardHint)
428
+ const tangent = new THREE.Vector3().crossVectors(up, outward).normalize()
429
+ outward.crossVectors(tangent, up).normalize()
430
+ return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(tangent, up, outward))
431
+ }
432
+
433
+ function horizontalWallOutward(
434
+ frame: SpaceStationWallPathFrame,
435
+ outwardHint: THREE.Vector3,
436
+ ): THREE.Vector3 {
437
+ const outward = outwardHint.clone().addScaledVector(WORLD_UP, -outwardHint.dot(WORLD_UP))
438
+ if (outward.lengthSq() < 0.000_001) outward.crossVectors(frame.tangent, WORLD_UP)
439
+ return outward.normalize()
440
+ }
441
+
442
+ function horizontalFrameTangent(
443
+ frame: SpaceStationWallPathFrame,
444
+ outwardHint: THREE.Vector3,
445
+ ): THREE.Vector3 {
446
+ const outward = horizontalWallOutward(frame, outwardHint)
447
+ return new THREE.Vector3().crossVectors(WORLD_UP, outward).normalize()
448
+ }
449
+
450
+ function bandPathDistances(frames: readonly SpaceStationWallPathFrame[]): number[] {
451
+ const distances = [0]
452
+ for (let index = 1; index < frames.length; index += 1) {
453
+ distances.push(distances[index - 1]! + frames[index]!.base.distanceTo(frames[index - 1]!.base))
454
+ }
455
+ return distances
456
+ }
457
+
458
+ function sampleCanonicalRange(
459
+ run: CanonicalWallRun,
460
+ start: number,
461
+ end: number,
462
+ ): SpaceStationWallPathFrame[] {
463
+ const rangeStart = Math.max(0, Math.min(start, end))
464
+ const rangeEnd = Math.min(run.length, Math.max(start, end))
465
+ const sampled = [canonicalWallFrameAt(run, rangeStart)]
466
+ let previous = sampled[0]!
467
+
468
+ for (const frame of run.frames) {
469
+ if (frame.distance <= rangeStart || frame.distance >= rangeEnd) continue
470
+ const turn = Math.acos(THREE.MathUtils.clamp(previous.tangent.dot(frame.tangent), -1, 1))
471
+ if (frame.distance - previous.distance < SAMPLE_SPACING && turn < SAMPLE_MAXIMUM_TURN) continue
472
+ sampled.push(frame)
473
+ previous = frame
474
+ }
475
+
476
+ sampled.push(canonicalWallFrameAt(run, rangeEnd))
477
+ return sampled
478
+ }
479
+
480
+ function instancedBatch(
481
+ name: string,
482
+ geometry: THREE.BufferGeometry,
483
+ material: THREE.Material,
484
+ matrices: readonly THREE.Matrix4[],
485
+ ): THREE.InstancedMesh {
486
+ const mesh = new THREE.InstancedMesh(geometry, material, matrices.length)
487
+ mesh.name = name
488
+ mesh.userData.decorative = true
489
+ mesh.userData.visualOnly = true
490
+ mesh.castShadow = true
491
+ mesh.receiveShadow = true
492
+ matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix))
493
+ mesh.instanceMatrix.needsUpdate = true
494
+ mesh.computeBoundingBox()
495
+ mesh.computeBoundingSphere()
496
+ return mesh
497
+ }