@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,443 @@
1
+ export {
2
+ DEFAULT_TRACK_TROUGH_FLAT_FRACTION,
3
+ TRACK_RAIL_CROSS_SECTION_SEGMENTS,
4
+ TRACK_RAIL_WALL_THICKNESS,
5
+ defaultTrackTroughDepth,
6
+ railSurfacePoseAt,
7
+ railTroughCrossSectionOffset,
8
+ railTroughDepthAt,
9
+ railTroughFlatFraction,
10
+ railWidthAt,
11
+ type RailSurfacePose,
12
+ } from '@marble.fun/physics-engine/browser'
13
+ import type {
14
+ PhysicsConveyorPieceDefinition,
15
+ PhysicsDipperPieceDefinition,
16
+ PhysicsFinishPieceDefinition,
17
+ PhysicsFlipperBankPieceDefinition,
18
+ PhysicsJumpPieceDefinition,
19
+ PhysicsPegFieldPieceDefinition,
20
+ PhysicsPortalPieceDefinition,
21
+ PhysicsRailPieceDefinition,
22
+ PhysicsReboundPieceDefinition,
23
+ PhysicsRisingWallForestPieceDefinition,
24
+ PhysicsSplitterPieceDefinition,
25
+ PhysicsTrackDefinition,
26
+ PhysicsTurntablePieceDefinition,
27
+ PhysicsVortexPieceDefinition,
28
+ TrackDocument,
29
+ TrackPieceAppearance,
30
+ } from '@marble.fun/physics-engine/browser'
31
+
32
+ export type TrackVector = readonly [x: number, y: number, z: number]
33
+
34
+ export interface TrackSkin {
35
+ id: string
36
+ label: string
37
+ background: number
38
+ fog: number
39
+ trackTop: number
40
+ trackEdge: number
41
+ rail: number
42
+ structure: number
43
+ signal: number
44
+ hazard: number
45
+ }
46
+
47
+ interface TrackPieceBase {
48
+ id: string
49
+ label: string
50
+ appearance?: TrackPieceAppearance
51
+ /** Visual-only absolute metric offset inherited across connected pieces. */
52
+ materialConnectionPhase?: number
53
+ /** Hidden previews can defer expensive masonry until continuity supplies its final phase. */
54
+ deferStoneArchitectureUntilMaterialContinuity?: boolean
55
+ }
56
+
57
+ export interface RailPieceDefinition extends TrackPieceBase, PhysicsRailPieceDefinition {
58
+ type: 'rail'
59
+ /** Visual-only topology capability. Vortex-owned rails reject roof/canopy wall architecture. */
60
+ supportsAppearanceCanopy?: boolean
61
+ /** Track Lab-only bounds and ray-picking meshes; runtime race worlds omit them. */
62
+ editorProxies?: boolean
63
+ /** Visual-only connector locations in the same coordinate space as `points`, used to inherit exact material frames. */
64
+ materialConnectors?: readonly {
65
+ id: string
66
+ position: TrackVector
67
+ }[]
68
+ /**
69
+ * Visual-only longitudinal sampling for a non-physical overview preview.
70
+ * Runtime rails and colliders always retain the canonical 2.5 cm sampling.
71
+ */
72
+ previewProfileSamplePitch?: number
73
+ /** Visual-only fluid continuity at the first/input terminal. */
74
+ waterFlowStartConnected?: boolean
75
+ /** Visual-only fluid continuity at the last/output terminal. */
76
+ waterFlowEndConnected?: boolean
77
+ /** Keep the selected floor recipe on one continuous carrier mesh instead of material-authored separated boards. */
78
+ continuousFloorMaterialOnly?: boolean
79
+ /** Visual race landmark role. This never changes rail physics. */
80
+ raceRole?: 'start' | 'finish'
81
+ /** Optional visual railway topology for themed raised rails. */
82
+ railroadProfile?: 'routing-split' | 'routing-merge' | 'intersection-merge'
83
+ /**
84
+ * Turns one continuous ribbon into two standard lane troughs without
85
+ * stacking separate floor meshes or colliders. Used by passive routing
86
+ * pieces whose terminal is a pair of side-by-side connectors.
87
+ */
88
+ routingBranchProfile?: {
89
+ mode: 'split' | 'merge'
90
+ branchOffset: number
91
+ branchWidth: number
92
+ startProgress: number
93
+ endProgress: number
94
+ /** Metric radius of each circular half of the branch S-bend. */
95
+ bendRadius?: number
96
+ }
97
+ /**
98
+ * Additional ingress centerlines that are fused with `points` into one
99
+ * junction floor and one floor collider. They are geometry, not separate
100
+ * track pieces, so intersections cannot leave coplanar overlapping shells.
101
+ */
102
+ junctionBranches?: readonly {
103
+ connectorId: string
104
+ /** Visual-only absolute metric phase at this branch input connector. */
105
+ materialConnectionPhase?: number
106
+ /** Visual-only fluid continuity at this branch input connector. */
107
+ waterFlowStartConnected?: boolean
108
+ points: readonly TrackVector[]
109
+ startTangent?: TrackVector
110
+ endTangent?: TrackVector
111
+ startPortWidth?: number
112
+ startPortTroughDepth?: number
113
+ startPortTroughFlatFraction?: number
114
+ /** Whether the branch input wall volumes close at their exposed terminal. */
115
+ capStart?: boolean
116
+ }[]
117
+ /** Connector that owns the first point of the junction's primary route. */
118
+ junctionMainConnectorId?: string
119
+ points: readonly TrackVector[]
120
+ /** Exact forward tangent at the first port. Prevents sampled curves from rotating their terminal cross-section. */
121
+ startTangent?: TrackVector
122
+ /** Exact forward tangent at the last port. Prevents sampled curves from rotating their terminal cross-section. */
123
+ endTangent?: TrackVector
124
+ /** Negotiated width of the first connected port, independent of the rail's interior width. */
125
+ startPortWidth?: number
126
+ /** Negotiated width of the last connected port, independent of the rail's interior width. */
127
+ endPortWidth?: number
128
+ /** Negotiated trough depth of the first connected port. */
129
+ startPortTroughDepth?: number
130
+ /** Negotiated trough depth of the last connected port. */
131
+ endPortTroughDepth?: number
132
+ /** Negotiated flat-bottom fraction of the first connected trough. */
133
+ startPortTroughFlatFraction?: number
134
+ /** Negotiated flat-bottom fraction of the last connected trough. */
135
+ endPortTroughFlatFraction?: number
136
+ /** Whether the floor and wall shells close across the first cross-section. Connected ports leave this open. */
137
+ capStart?: boolean
138
+ /** Whether the floor and wall shells close across the last cross-section. Connected ports leave this open. */
139
+ capEnd?: boolean
140
+ width: number
141
+ startWidth?: number
142
+ pointWidths?: readonly number[]
143
+ widthProfile?: 'linear' | 'smoothstep'
144
+ startTroughDepth?: number
145
+ troughDepth?: number
146
+ pointTroughDepths?: readonly number[]
147
+ /** Optional per-control-point trough profiles, interpolated with the floor geometry. */
148
+ pointTroughFlatFractions?: readonly number[]
149
+ troughProfile?: 'full' | 'fade-in' | 'fade-out' | 'fade-both'
150
+ troughFadeFraction?: number
151
+ troughFlatFraction?: number
152
+ wallHeight?: number
153
+ /** Optional side-wall collider height, independent of the rendered wall height. */
154
+ wallColliderHeight?: number
155
+ verticalWalls?: boolean
156
+ floorFriction?: number
157
+ wallFriction?: number
158
+ openFirstSegment?: boolean
159
+ openLastSegment?: boolean
160
+ openNegativeWallSegments?: readonly number[]
161
+ openPositiveWallSegments?: readonly number[]
162
+ closedStart?: boolean
163
+ closedEnd?: boolean
164
+ /** Optional collider height for a closed start, independent of the side-wall height. */
165
+ closedStartHeight?: number
166
+ /** Optional collider height for a closed end, independent of the side-wall height. */
167
+ closedEndHeight?: number
168
+ centerDivider?: {
169
+ /** A divider is independent geometry; this controls whether it looks like a wall or an obstacle. */
170
+ visualRole?: 'wall' | 'obstacle'
171
+ startProgress: number
172
+ endProgress: number
173
+ startWidth: number
174
+ endWidth: number
175
+ /** Floor-profile span used to anchor the start edge before wall-thickness overhang. */
176
+ startSurfaceWidth?: number
177
+ /** Floor-profile span used to anchor the end edge before wall-thickness overhang. */
178
+ endSurfaceWidth?: number
179
+ /** Total terminal span whose excess over endWidth closes adjacent wall ends. */
180
+ endWallClosureWidth?: number
181
+ startHeight: number
182
+ endHeight: number
183
+ lateralOffset?: number
184
+ distanceBased?: boolean
185
+ }
186
+ }
187
+ export interface VortexPieceDefinition extends TrackPieceBase, PhysicsVortexPieceDefinition {
188
+ type: 'vortex'
189
+ /** The negotiated input is carrying the same fluid style into this Vortex. */
190
+ waterFlowInputConnected?: boolean
191
+ center: TrackVector
192
+ entry: TrackVector
193
+ entryWidth: number
194
+ integratedEntry?: boolean
195
+ dropEntry?: boolean
196
+ outerRadius: number
197
+ innerRadius: number
198
+ depth: number
199
+ profileExponent?: number
200
+ /** Smooths the bowl into its outer apron while preserving an inward drain slope. */
201
+ smoothProfile?: boolean
202
+ floorFriction?: number
203
+ segmentCount: number
204
+ exitFunnel?: {
205
+ mouthRadius: number
206
+ throatRadius: number
207
+ depth: number
208
+ ringCount: number
209
+ friction?: number
210
+ tunnelFriction?: number
211
+ throatLength?: number
212
+ outletRadius?: number
213
+ tunnelExit?: TrackVector
214
+ tunnelDirection?: TrackVector
215
+ }
216
+ }
217
+
218
+ export interface SplitterPieceDefinition extends TrackPieceBase, PhysicsSplitterPieceDefinition {
219
+ type: 'splitter'
220
+ center: TrackVector
221
+ capture: TrackVector
222
+ direction: TrackVector
223
+ surfaceAligned?: boolean
224
+ captureRadius: number
225
+ barLength: number
226
+ barHeight: number
227
+ barDepth: number
228
+ swingRadians: number
229
+ cycleSeconds: number
230
+ phaseIndex: number
231
+ }
232
+
233
+ export interface JumpPieceDefinition extends TrackPieceBase, PhysicsJumpPieceDefinition {
234
+ type: 'jump'
235
+ points: readonly TrackVector[]
236
+ width: number
237
+ wallHeight: number
238
+ }
239
+
240
+ export interface ConveyorPieceDefinition extends TrackPieceBase, PhysicsConveyorPieceDefinition {
241
+ type: 'conveyor'
242
+ bottom: TrackVector
243
+ top: TrackVector
244
+ direction: TrackVector
245
+ width: number
246
+ shelfDepth: number
247
+ shelfCount: number
248
+ speed: number
249
+ friction: number
250
+ shelfTiltRadians: number
251
+ releaseHeight: number
252
+ phaseIndex: number
253
+ }
254
+
255
+ export interface FlipperMountDefinition {
256
+ anchor: TrackVector
257
+ side: 'left' | 'right'
258
+ direction?: TrackVector
259
+ }
260
+
261
+ export interface FlipperBankPieceDefinition extends TrackPieceBase, PhysicsFlipperBankPieceDefinition {
262
+ type: 'flipper-bank'
263
+ direction: TrackVector
264
+ mounts: readonly FlipperMountDefinition[]
265
+ paddleLength: number
266
+ paddleWidth: number
267
+ paddleHeight: number
268
+ swingRadians: number
269
+ strikeSeconds: number
270
+ holdSeconds: number
271
+ returnSeconds: number
272
+ minimumIntervalSeconds: number
273
+ maximumIntervalSeconds: number
274
+ }
275
+
276
+ export interface TurntablePieceDefinition extends TrackPieceBase, PhysicsTurntablePieceDefinition {
277
+ type: 'turntable'
278
+ center: TrackVector
279
+ direction: TrackVector
280
+ radius: number
281
+ channelWidth: number
282
+ wallHeight: number
283
+ routeAngles: readonly number[]
284
+ rotationSeconds: number
285
+ settleSeconds: number
286
+ triggerRadius: number
287
+ }
288
+
289
+ export interface DipperPieceDefinition extends TrackPieceBase, PhysicsDipperPieceDefinition {
290
+ type: 'dipper'
291
+ hinge: TrackVector
292
+ direction: TrackVector
293
+ length: number
294
+ width: number
295
+ wallHeight: number
296
+ dropRadians: number
297
+ lowerSeconds: number
298
+ holdSeconds: number
299
+ resetSeconds: number
300
+ triggerRadius: number
301
+ }
302
+
303
+ export interface PegFieldPieceDefinition extends TrackPieceBase, PhysicsPegFieldPieceDefinition {
304
+ type: 'peg-field'
305
+ direction: TrackVector
306
+ pegs: readonly TrackVector[]
307
+ pegDirections?: readonly TrackVector[]
308
+ pegRadius: number
309
+ pegHeight: number
310
+ colliderShape?: 'cone' | 'cylinder'
311
+ friction?: number
312
+ restitution?: number
313
+ }
314
+
315
+ export interface ReboundMountDefinition {
316
+ anchor: TrackVector
317
+ angle: number
318
+ side: 'left' | 'right'
319
+ }
320
+
321
+ export interface ReboundPieceDefinition extends TrackPieceBase, PhysicsReboundPieceDefinition {
322
+ type: 'rebounds'
323
+ direction: TrackVector
324
+ mounts: readonly ReboundMountDefinition[]
325
+ reboundLength: number
326
+ reboundWidth: number
327
+ reboundHeight: number
328
+ friction: number
329
+ restitution: number
330
+ strikeDistance: number
331
+ strikeSeconds: number
332
+ holdSeconds: number
333
+ returnSeconds: number
334
+ cooldownSeconds: number
335
+ }
336
+
337
+ export interface RisingWallAnchorDefinition {
338
+ position: TrackVector
339
+ row: number
340
+ column: number
341
+ }
342
+
343
+ export interface RisingWallForestPieceDefinition extends TrackPieceBase, PhysicsRisingWallForestPieceDefinition {
344
+ type: 'rising-wall-forest'
345
+ direction: TrackVector
346
+ anchors: readonly RisingWallAnchorDefinition[]
347
+ rows: number
348
+ columns: number
349
+ density: 'sparse' | 'dense'
350
+ wallWidth: number
351
+ wallHeight: number
352
+ wallDepth: number
353
+ cycleSeconds: number
354
+ }
355
+
356
+ export interface PortalPieceDefinition extends TrackPieceBase, PhysicsPortalPieceDefinition {
357
+ type: 'portal'
358
+ entryVisualStyle?: PortalVisualStyle
359
+ exitVisualStyle?: PortalVisualStyle
360
+ color: string
361
+ entry: TrackVector
362
+ entryDirection?: TrackVector
363
+ exit: TrackVector
364
+ exitDirection: TrackVector
365
+ entryVisualCenter: TrackVector
366
+ entryVisualDirection: TrackVector
367
+ exitVisualCenter: TrackVector
368
+ exitVisualDirection: TrackVector
369
+ captureRadius?: number
370
+ captureHalfExtents?: TrackVector
371
+ releaseSpeed: number
372
+ }
373
+
374
+ export type PortalVisualStyle = 'live-window'
375
+
376
+ export interface FinishPieceDefinition extends TrackPieceBase, PhysicsFinishPieceDefinition {
377
+ type: 'finish'
378
+ center: TrackVector
379
+ direction: TrackVector
380
+ width: number
381
+ channelEnd: TrackVector
382
+ rankingSlots: number
383
+ rankingSpacing: number
384
+ }
385
+
386
+ export type TrackPieceDefinition =
387
+ | RailPieceDefinition
388
+ | VortexPieceDefinition
389
+ | SplitterPieceDefinition
390
+ | JumpPieceDefinition
391
+ | ConveyorPieceDefinition
392
+ | FlipperBankPieceDefinition
393
+ | TurntablePieceDefinition
394
+ | DipperPieceDefinition
395
+ | PegFieldPieceDefinition
396
+ | ReboundPieceDefinition
397
+ | RisingWallForestPieceDefinition
398
+ | PortalPieceDefinition
399
+ | FinishPieceDefinition
400
+
401
+ export interface TrackGuidePoint {
402
+ position: TrackVector
403
+ progress: number
404
+ pieceId: string
405
+ }
406
+
407
+ export interface TrackGuidePath {
408
+ id: string
409
+ points: readonly TrackGuidePoint[]
410
+ }
411
+
412
+ export interface TrackDefinition extends PhysicsTrackDefinition {
413
+ id: string
414
+ version: number
415
+ label: string
416
+ description: string
417
+ targetDurationSeconds: readonly [number, number]
418
+ spawnProgress: number
419
+ spawnWidth: number
420
+ themeId?: string
421
+ skin: TrackSkin
422
+ pieces: readonly TrackPieceDefinition[]
423
+ cameraPath: TrackGuidePath
424
+ projectionPaths: readonly TrackGuidePath[]
425
+ /**
426
+ * Authored source for the canonical Lab visual assembly. This is runtime
427
+ * context rather than simulation identity and is omitted by legacy tracks.
428
+ */
429
+ sourceDocument?: TrackDocument
430
+ }
431
+
432
+ export const NEUTRAL_STUDIO_SKIN: TrackSkin = {
433
+ id: 'neutral-studio',
434
+ label: 'Neutral Studio',
435
+ background: 0x080b10,
436
+ fog: 0x0b1016,
437
+ trackTop: 0xaeb9be,
438
+ trackEdge: 0x27313a,
439
+ rail: 0xe3eaeb,
440
+ structure: 0x35414a,
441
+ signal: 0xdfff65,
442
+ hazard: 0xff765f,
443
+ }
@@ -0,0 +1,33 @@
1
+ /** Shared visual proportions for open timber fences, independent of theme materials. */
2
+ export const WOODEN_FENCE_HEIGHT_RATIO = 0.65
3
+ export const WOODEN_FENCE_POST_SPAN_RATIO = 1.82
4
+ export const WOODEN_FENCE_MINIMUM_POST_GAP = 0.08
5
+ /** Visual breathing room beyond the physical wall's outer face. */
6
+ export const WOODEN_FENCE_EDGE_ALLOWANCE = 0.025
7
+
8
+ export function woodenFenceHeight(wallHeight: number): number {
9
+ return Math.max(0, wallHeight) * WOODEN_FENCE_HEIGHT_RATIO
10
+ }
11
+
12
+ export function woodenFencePostCount(totalLength: number, fenceHeight: number): number {
13
+ if (totalLength <= 0 || fenceHeight <= 0) return 0
14
+ return Math.max(2, Math.ceil(totalLength / (fenceHeight * WOODEN_FENCE_POST_SPAN_RATIO)) + 1)
15
+ }
16
+
17
+ /**
18
+ * Spatial exclusion shared by path-following fence schedulers. Junctions can
19
+ * contain several boundary runs whose independent arc-length stations meet in
20
+ * world space; this keeps their post surfaces separated by a real gap.
21
+ */
22
+ export function woodenFenceMinimumPostCenterDistance(maximumPostWidth: number): number {
23
+ return Math.max(0, maximumPostWidth) + WOODEN_FENCE_MINIMUM_POST_GAP
24
+ }
25
+
26
+ /**
27
+ * Canonical wall runs describe the middle of a physical wall. Fence members
28
+ * live just outside its exterior face, matching the placement used by normal
29
+ * ribbon lanes.
30
+ */
31
+ export function woodenFenceOutwardPlacementOffset(wallThickness: number): number {
32
+ return Math.max(0, wallThickness) / 2 + WOODEN_FENCE_EDGE_ALLOWANCE
33
+ }
@@ -0,0 +1,51 @@
1
+ import type { ThemeMaterialRegistry } from '../../../three/theme-material-registry'
2
+ import { TRACK_THEMES } from '../../../three/theme-catalog'
3
+ import { resolvedFloorMaterialIdForTheme } from '../../../three/railroad-appearance'
4
+ import type { MaterialRecipe, TrackThemeDefinition } from '../../../three/track-theme'
5
+ import type { FloorSurfaceTreatment, TrackMaterials } from './track-mechanisms'
6
+
7
+ function floorRecipeForTheme(theme: TrackThemeDefinition, floorMaterialId: string | undefined): MaterialRecipe {
8
+ const resolvedId = resolvedFloorMaterialIdForTheme(theme, floorMaterialId)
9
+ if (!resolvedId) return theme.materials['lane.surface']
10
+ return (
11
+ theme.detailMaterials?.[resolvedId] ??
12
+ theme.appearancePresets?.floor?.find(preset => preset.id === resolvedId)?.material ??
13
+ theme.materials['lane.surface']
14
+ )
15
+ }
16
+
17
+ export function resolveFloorSurfaceTreatment(
18
+ registry: ThemeMaterialRegistry,
19
+ floorMaterialId: string | undefined,
20
+ ): TrackMaterials['floorSurfaceTreatment'] {
21
+ const treatment = registry.getAppearanceRecipe(floorMaterialId, 'lane.surface').surfaceTreatment
22
+ return treatment
23
+ ? {
24
+ ...treatment,
25
+ material: registry.getAppearance(treatment.materialId, 'lane.edge'),
26
+ themeId: registry.activeTheme.id,
27
+ }
28
+ : undefined
29
+ }
30
+
31
+ export function resolveFloorSurfaceTreatments(
32
+ registry: ThemeMaterialRegistry,
33
+ floorMaterialId: string | undefined,
34
+ ): readonly FloorSurfaceTreatment[] {
35
+ const grouped = new Map<string, { treatment: NonNullable<MaterialRecipe['surfaceTreatment']>; themeIds: string[] }>()
36
+ for (const theme of TRACK_THEMES) {
37
+ const treatment = floorRecipeForTheme(theme, floorMaterialId).surfaceTreatment
38
+ if (!treatment) continue
39
+ const key = `${treatment.kind}:${treatment.materialId}:${treatment.bandWidth}:${treatment.featherWidth}:${treatment.normalOffset}`
40
+ const group = grouped.get(key) ?? { treatment, themeIds: [] }
41
+ group.themeIds.push(theme.id)
42
+ grouped.set(key, group)
43
+ }
44
+ return [...grouped.values()].map(({ treatment, themeIds }) => ({
45
+ ...treatment,
46
+ get material() {
47
+ return registry.getAppearance(treatment.materialId, 'lane.edge')
48
+ },
49
+ themeId: themeIds.length === 1 ? themeIds[0]! : themeIds,
50
+ }))
51
+ }