@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
+ import * as THREE from 'three'
2
+
3
+ import { VisualTorusGeometry, VisualTubeGeometry } from '../../../three/visual-geometry'
4
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
5
+
6
+ import { ROPE_WALL_SUPPORTED_THEME_IDS } from '../../../three/rope-wall-appearance'
7
+ import { TRACK_RAIL_WALL_THICKNESS } from './track-definition'
8
+ import {
9
+ canonicalWallFrameAt,
10
+ type CanonicalWallRun,
11
+ type RawWallFrame,
12
+ } from './track-surface-topology'
13
+ import { woodenFenceHeight, woodenFencePostCount, WOODEN_FENCE_HEIGHT_RATIO } from './track-fence-architecture'
14
+
15
+ export type RopeWallPathFrame = RawWallFrame
16
+ export type RopeWallPathRun = CanonicalWallRun
17
+
18
+ export interface RopeWallMaterials {
19
+ rope: THREE.Material
20
+ timber: THREE.Material
21
+ timberEndGrain: THREE.Material
22
+ }
23
+
24
+ const HANGER_SPACING = 0.58
25
+ const CABLE_SEGMENTS = 8
26
+ const HANDRAIL_HEIGHT = 0.79
27
+ const LOWER_CABLE_HEIGHT = 0.2
28
+ const OUTWARD_OFFSET = 0.075
29
+
30
+ /**
31
+ * Builds the side wall of a hand-lashed rope bridge: substantial timber
32
+ * uprights, wrapped rope collars, sagging handrail and lower cables, dense
33
+ * vertical suspension ties, knots, and irregular loose ends. The continuous
34
+ * wall created by the track builder remains the only collision geometry.
35
+ */
36
+ export function addRopeWallVisual(
37
+ root: THREE.Group,
38
+ id: string,
39
+ runs: readonly RopeWallPathRun[],
40
+ wallHeight: number,
41
+ materials: RopeWallMaterials,
42
+ ): THREE.Group | undefined {
43
+ const visualHeight = woodenFenceHeight(wallHeight)
44
+ const postMatrices: THREE.Matrix4[] = []
45
+ const handrailMatrices: THREE.Matrix4[] = []
46
+ const lowerCableMatrices: THREE.Matrix4[] = []
47
+ const hangerMatrices: THREE.Matrix4[] = []
48
+ const lashingMatrices: THREE.Matrix4[] = []
49
+ const crossLashingMatrices: THREE.Matrix4[] = []
50
+ const knotMatrices: THREE.Matrix4[] = []
51
+ const tailMatrices: THREE.Matrix4[] = []
52
+
53
+ for (const [runIndex, run] of runs.entries()) {
54
+ if (run.frames.length < 2) continue
55
+ // Rope dressing has a small authored stand-off from the canonical wall
56
+ // boundary. The topology itself remains shared with every other theme.
57
+ const canonicalRun: CanonicalWallRun = { ...run, frames: run.frames.map(frame => ({
58
+ ...frame,
59
+ base: frame.base
60
+ .clone()
61
+ .addScaledVector(frame.outward, TRACK_RAIL_WALL_THICKNESS / 2 + 0.025)
62
+ .addScaledVector(frame.up, -0.03),
63
+ })) }
64
+ const totalLength = canonicalRun.length
65
+ if (totalLength <= 0.001) continue
66
+ const postCount = woodenFencePostCount(totalLength, visualHeight)
67
+ const posts = Array.from({ length: postCount }, (_, postIndex) =>
68
+ canonicalWallFrameAt(canonicalRun, (postIndex / (postCount - 1)) * totalLength),
69
+ )
70
+
71
+ posts.forEach((frame, postIndex) => {
72
+ const seed = deterministic(`${id}:${runIndex}:${postIndex}`)
73
+ const postHeight = visualHeight
74
+ const lean = (seed - 0.5) * 0.11
75
+ const postRotation = frameQuaternion(frame.tangent, frame.up)
76
+ postRotation.premultiply(new THREE.Quaternion().setFromAxisAngle(frame.tangent, lean))
77
+ postMatrices.push(
78
+ new THREE.Matrix4().compose(
79
+ frame.base.clone().addScaledVector(frame.up, postHeight / 2 - 0.025),
80
+ postRotation,
81
+ new THREE.Vector3(0.96 + seed * 0.16, postHeight, 0.96 + seed * 0.16),
82
+ ),
83
+ )
84
+
85
+ for (const [anchorHeight, scaleY] of [
86
+ [visualHeight * HANDRAIL_HEIGHT, 1],
87
+ [visualHeight * LOWER_CABLE_HEIGHT, 0.78],
88
+ ] as const) {
89
+ lashingMatrices.push(
90
+ new THREE.Matrix4().compose(
91
+ frame.base.clone().addScaledVector(frame.up, anchorHeight),
92
+ postRotation,
93
+ new THREE.Vector3(1.02 + seed * 0.08, scaleY, 1.02 + seed * 0.08),
94
+ ),
95
+ )
96
+ }
97
+ appendCrossLashings(crossLashingMatrices, frame, visualHeight * LOWER_CABLE_HEIGHT)
98
+ })
99
+
100
+ for (let postIndex = 0; postIndex < posts.length - 1; postIndex += 1) {
101
+ const start = posts[postIndex]!
102
+ const end = posts[postIndex + 1]!
103
+ const spanLength = start.base.distanceTo(end.base)
104
+ appendSaggingCable(handrailMatrices, start, end, visualHeight * HANDRAIL_HEIGHT, visualHeight * 0.12)
105
+ appendSaggingCable(lowerCableMatrices, start, end, visualHeight * LOWER_CABLE_HEIGHT, visualHeight * 0.035)
106
+
107
+ const hangerCount = Math.max(3, Math.ceil(spanLength / HANGER_SPACING))
108
+ for (let hangerIndex = 1; hangerIndex < hangerCount; hangerIndex += 1) {
109
+ const progress = hangerIndex / hangerCount
110
+ const upper = cablePoint(start, end, progress, visualHeight * HANDRAIL_HEIGHT, visualHeight * 0.12)
111
+ const lower = cablePoint(start, end, progress, visualHeight * LOWER_CABLE_HEIGHT, visualHeight * 0.035)
112
+ appendCylinderMatrix(hangerMatrices, upper, lower, 0.015)
113
+
114
+ const frame = interpolateFrame(start, end, progress)
115
+ const knotRotation = frameQuaternion(frame.tangent, frame.up).multiply(
116
+ new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2),
117
+ )
118
+ for (const point of [upper, lower]) {
119
+ knotMatrices.push(new THREE.Matrix4().compose(point, knotRotation, new THREE.Vector3(1, 1, 1)))
120
+ }
121
+
122
+ if ((hangerIndex + postIndex + runIndex) % 2 === 0) {
123
+ const tailLength = 0.13 + deterministic(`${id}:${runIndex}:${postIndex}:${hangerIndex}:tail`) * 0.19
124
+ const tailEnd = lower
125
+ .clone()
126
+ .addScaledVector(frame.up, -tailLength)
127
+ .addScaledVector(frame.tangent, (deterministic(`${id}:${hangerIndex}:sway`) - 0.5) * 0.09)
128
+ appendCylinderMatrix(tailMatrices, lower, tailEnd, 0.016)
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ if (postMatrices.length === 0 || handrailMatrices.length === 0) return undefined
135
+ const group = new THREE.Group()
136
+ group.name = `${id}-rope-wall`
137
+ group.userData.themeOnly = [...ROPE_WALL_SUPPORTED_THEME_IDS]
138
+ group.userData.decorative = true
139
+ group.userData.visualOnly = true
140
+ group.userData.construction = 'hand-lashed-suspension-bridge-wall'
141
+ group.userData.wallHeightRatio = WOODEN_FENCE_HEIGHT_RATIO
142
+
143
+ group.add(
144
+ instancedBatch(`${id}-rope-wall-timber-posts`, createHandHewnPostGeometry(), materials.timber, postMatrices),
145
+ instancedBatch(
146
+ `${id}-rope-wall-timber-post-caps`,
147
+ createHandHewnPostCapGeometry(),
148
+ materials.timberEndGrain,
149
+ postMatrices,
150
+ ),
151
+ instancedBatch(
152
+ `${id}-rope-wall-handrail-cable`,
153
+ createBraidedRopeSegmentGeometry(0.028, 0.024),
154
+ materials.rope,
155
+ handrailMatrices,
156
+ ),
157
+ instancedBatch(
158
+ `${id}-rope-wall-lower-cable`,
159
+ createBraidedRopeSegmentGeometry(0.023, 0.019),
160
+ materials.rope,
161
+ lowerCableMatrices,
162
+ ),
163
+ instancedBatch(
164
+ `${id}-rope-wall-vertical-hangers`,
165
+ createBraidedRopeSegmentGeometry(0.011, 0.009, 2),
166
+ materials.rope,
167
+ hangerMatrices,
168
+ ),
169
+ instancedBatch(`${id}-rope-wall-post-lashings`, createRopeLashingGeometry(), materials.rope, lashingMatrices),
170
+ instancedBatch(
171
+ `${id}-rope-wall-cross-lashings`,
172
+ createBraidedRopeSegmentGeometry(0.013, 0.011, 2),
173
+ materials.rope,
174
+ crossLashingMatrices,
175
+ ),
176
+ instancedBatch(
177
+ `${id}-rope-wall-hanger-knots`,
178
+ withAuthoredMaterialUv(new VisualTorusGeometry(0.057, 0.016, 5, 9)),
179
+ materials.rope,
180
+ knotMatrices,
181
+ ),
182
+ instancedBatch(
183
+ `${id}-rope-wall-loose-ends`,
184
+ createBraidedRopeSegmentGeometry(0.0085, 0.0075, 2),
185
+ materials.rope,
186
+ tailMatrices,
187
+ ),
188
+ )
189
+ root.add(group)
190
+ return group
191
+ }
192
+
193
+ function createHandHewnPostGeometry(): THREE.BufferGeometry {
194
+ const sides = 12
195
+ const levels = [-0.5, -0.34, 0.3, 0.5] as const
196
+ const positions: number[] = []
197
+ const uvs: number[] = []
198
+ const metricUvs: number[] = []
199
+ const indices: number[] = []
200
+
201
+ for (const [levelIndex, y] of levels.entries()) {
202
+ for (let side = 0; side < sides; side += 1) {
203
+ const angle = (side / sides) * Math.PI * 2
204
+ const irregularity = 1 + Math.sin(side * 4.37 + levelIndex * 2.11) * 0.055
205
+ const taper = levelIndex === 0 ? 1.08 : levelIndex === levels.length - 1 ? 0.91 : 1
206
+ const topChip = 0
207
+ positions.push(
208
+ Math.cos(angle) * 0.225 * irregularity * taper,
209
+ y + topChip,
210
+ Math.sin(angle) * 0.225 * irregularity * taper,
211
+ )
212
+ uvs.push(side / sides, levelIndex / (levels.length - 1))
213
+ metricUvs.push(side / sides, levelIndex / (levels.length - 1), 1, 1)
214
+ }
215
+ }
216
+
217
+ for (let level = 0; level < levels.length - 1; level += 1) {
218
+ for (let side = 0; side < sides; side += 1) {
219
+ const next = (side + 1) % sides
220
+ const lower = level * sides
221
+ const upper = (level + 1) * sides
222
+ indices.push(lower + side, upper + side, lower + next, lower + next, upper + side, upper + next)
223
+ }
224
+ }
225
+
226
+ const bottomCenter = positions.length / 3
227
+ positions.push(0, levels[0], 0)
228
+ uvs.push(0.5, 0.5)
229
+ metricUvs.push(0.5, 0.5, 1, 1)
230
+ for (let side = 0; side < sides; side += 1) {
231
+ indices.push(bottomCenter, side, (side + 1) % sides)
232
+ }
233
+
234
+ const geometry = new THREE.BufferGeometry()
235
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
236
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
237
+ geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
238
+ geometry.setIndex(indices)
239
+ geometry.computeVertexNormals()
240
+ geometry.computeBoundingBox()
241
+ geometry.computeBoundingSphere()
242
+ geometry.name = 'hand-hewn-rope-wall-post-geometry'
243
+ return geometry
244
+ }
245
+
246
+ function createHandHewnPostCapGeometry(): THREE.BufferGeometry {
247
+ const sides = 12
248
+ const positions: number[] = []
249
+ const uvs: number[] = []
250
+ const metricUvs: number[] = []
251
+ const indices: number[] = []
252
+
253
+ for (let side = 0; side < sides; side += 1) {
254
+ const angle = (side / sides) * Math.PI * 2
255
+ const irregularity = 1 + Math.sin(side * 4.37 + 3 * 2.11) * 0.055
256
+ const topChip = 0
257
+ const x = Math.cos(angle) * 0.225 * irregularity * 0.91
258
+ const z = Math.sin(angle) * 0.225 * irregularity * 0.91
259
+ positions.push(x, 0.5008 + topChip, z)
260
+ const u = x / 0.51 + 0.5
261
+ const v = z / 0.51 + 0.5
262
+ uvs.push(u, v)
263
+ metricUvs.push(u, v, 1, 1)
264
+ }
265
+ const center = positions.length / 3
266
+ positions.push(0, 0.5008, 0)
267
+ uvs.push(0.5, 0.5)
268
+ metricUvs.push(0.5, 0.5, 1, 1)
269
+ for (let side = 0; side < sides; side += 1) {
270
+ indices.push(center, (side + 1) % sides, side)
271
+ }
272
+
273
+ const geometry = new THREE.BufferGeometry()
274
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
275
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
276
+ geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
277
+ geometry.setIndex(indices)
278
+ geometry.computeVertexNormals()
279
+ geometry.computeBoundingBox()
280
+ geometry.computeBoundingSphere()
281
+ geometry.name = 'hand-hewn-rope-wall-post-cap-geometry'
282
+ return geometry
283
+ }
284
+
285
+ function createRopeLashingGeometry(): THREE.TubeGeometry {
286
+ const turns = 4.25
287
+ const points = Array.from({ length: 38 }, (_, index) => {
288
+ const progress = index / 37
289
+ const angle = progress * Math.PI * 2 * turns
290
+ const radius = 0.247 + Math.sin(progress * Math.PI * 6) * 0.006
291
+ return new THREE.Vector3(Math.cos(angle) * radius, (progress - 0.5) * 0.2, Math.sin(angle) * radius)
292
+ })
293
+ const geometry = withAuthoredMaterialUv(
294
+ new VisualTubeGeometry(new THREE.CatmullRomCurve3(points), 48, 0.032, 7, false),
295
+ )
296
+ geometry.name = 'hand-wrapped-rope-lashing-geometry'
297
+ return geometry
298
+ }
299
+
300
+ function createBraidedRopeSegmentGeometry(helixRadius: number, strandRadius: number, turns = 3): THREE.BufferGeometry {
301
+ const segments = 30
302
+ const strands = Array.from({ length: 3 }, (_, strandIndex) => {
303
+ const phase = (strandIndex / 3) * Math.PI * 2
304
+ const points = Array.from({ length: segments + 1 }, (_, index) => {
305
+ const progress = index / segments
306
+ const angle = phase + progress * Math.PI * 2 * turns
307
+ return new THREE.Vector3(Math.cos(angle) * helixRadius, progress - 0.5, Math.sin(angle) * helixRadius)
308
+ })
309
+ return new VisualTubeGeometry(new THREE.CatmullRomCurve3(points), segments, strandRadius, 6, false)
310
+ })
311
+ const geometry = mergeGeometries(strands, false)
312
+ strands.forEach(strand => strand.dispose())
313
+ if (!geometry) throw new Error('Failed to build braided rope geometry.')
314
+ geometry.name = 'three-strand-braided-rope-segment-geometry'
315
+ return withAuthoredMaterialUv(geometry)
316
+ }
317
+
318
+ function appendCrossLashings(matrices: THREE.Matrix4[], frame: RopeWallPathFrame, anchorHeight: number): void {
319
+ for (const face of [-1, 1] as const) {
320
+ const center = frame.base
321
+ .clone()
322
+ .addScaledVector(frame.up, anchorHeight - 0.12)
323
+ .addScaledVector(frame.outward, face * 0.235)
324
+ const upperLeft = center.clone().addScaledVector(frame.tangent, -0.18).addScaledVector(frame.up, 0.15)
325
+ const upperRight = center.clone().addScaledVector(frame.tangent, 0.18).addScaledVector(frame.up, 0.15)
326
+ const lowerLeft = center.clone().addScaledVector(frame.tangent, -0.18).addScaledVector(frame.up, -0.15)
327
+ const lowerRight = center.clone().addScaledVector(frame.tangent, 0.18).addScaledVector(frame.up, -0.15)
328
+ appendCylinderMatrix(matrices, upperLeft, lowerRight, 0.035)
329
+ appendCylinderMatrix(matrices, upperRight, lowerLeft, 0.035)
330
+ }
331
+ for (const face of [-1, 1] as const) {
332
+ const center = frame.base
333
+ .clone()
334
+ .addScaledVector(frame.up, anchorHeight - 0.12)
335
+ .addScaledVector(frame.tangent, face * 0.235)
336
+ const upperLeft = center.clone().addScaledVector(frame.outward, -0.18).addScaledVector(frame.up, 0.15)
337
+ const upperRight = center.clone().addScaledVector(frame.outward, 0.18).addScaledVector(frame.up, 0.15)
338
+ const lowerLeft = center.clone().addScaledVector(frame.outward, -0.18).addScaledVector(frame.up, -0.15)
339
+ const lowerRight = center.clone().addScaledVector(frame.outward, 0.18).addScaledVector(frame.up, -0.15)
340
+ appendCylinderMatrix(matrices, upperLeft, lowerRight, 0.035)
341
+ appendCylinderMatrix(matrices, upperRight, lowerLeft, 0.035)
342
+ }
343
+ }
344
+
345
+ function withAuthoredMaterialUv<T extends THREE.BufferGeometry>(geometry: T): T {
346
+ const uv = geometry.getAttribute('uv')
347
+ if (!uv) return geometry
348
+ const materialUvs: number[] = []
349
+ for (let index = 0; index < uv.count; index += 1) {
350
+ materialUvs.push(uv.getX(index), uv.getY(index), 1, 1)
351
+ }
352
+ geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(materialUvs, 4))
353
+ return geometry
354
+ }
355
+
356
+ function appendSaggingCable(
357
+ matrices: THREE.Matrix4[],
358
+ start: RopeWallPathFrame,
359
+ end: RopeWallPathFrame,
360
+ height: number,
361
+ sag: number,
362
+ ): void {
363
+ let previous = cablePoint(start, end, 0, height, sag)
364
+ for (let segment = 1; segment <= CABLE_SEGMENTS; segment += 1) {
365
+ const progress = segment / CABLE_SEGMENTS
366
+ const next = cablePoint(start, end, progress, height, sag)
367
+ appendCylinderMatrix(matrices, previous, next, 0.014)
368
+ previous = next
369
+ }
370
+ }
371
+
372
+ function cablePoint(
373
+ start: RopeWallPathFrame,
374
+ end: RopeWallPathFrame,
375
+ progress: number,
376
+ height: number,
377
+ sag: number,
378
+ ): THREE.Vector3 {
379
+ const frame = interpolateFrame(start, end, progress)
380
+ return frame.base
381
+ .clone()
382
+ .addScaledVector(frame.up, height - Math.sin(progress * Math.PI) * sag)
383
+ .addScaledVector(frame.outward, OUTWARD_OFFSET)
384
+ }
385
+
386
+ function appendCylinderMatrix(
387
+ matrices: THREE.Matrix4[],
388
+ start: THREE.Vector3,
389
+ end: THREE.Vector3,
390
+ overlap: number,
391
+ ): void {
392
+ const direction = end.clone().sub(start)
393
+ const length = direction.length()
394
+ if (length <= 0.001) return
395
+ matrices.push(
396
+ new THREE.Matrix4().compose(
397
+ start.clone().lerp(end, 0.5),
398
+ new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.normalize()),
399
+ new THREE.Vector3(1, length + overlap, 1),
400
+ ),
401
+ )
402
+ }
403
+
404
+ function instancedBatch(
405
+ name: string,
406
+ geometry: THREE.BufferGeometry,
407
+ material: THREE.Material | THREE.Material[],
408
+ matrices: readonly THREE.Matrix4[],
409
+ ): THREE.InstancedMesh {
410
+ const mesh = new THREE.InstancedMesh(geometry, material, matrices.length)
411
+ mesh.name = name
412
+ mesh.userData.decorative = true
413
+ mesh.userData.visualOnly = true
414
+ mesh.castShadow = true
415
+ mesh.receiveShadow = true
416
+ matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix))
417
+ mesh.instanceMatrix.needsUpdate = true
418
+ return mesh
419
+ }
420
+
421
+ function interpolateFrame(start: RopeWallPathFrame, end: RopeWallPathFrame, progress: number): RopeWallPathFrame {
422
+ const tangent = start.tangent.clone().lerp(end.tangent, progress).normalize()
423
+ const up = start.up.clone().lerp(end.up, progress).normalize()
424
+ const outward = start.outward.clone().lerp(end.outward, progress)
425
+ outward.addScaledVector(tangent, -outward.dot(tangent)).addScaledVector(up, -outward.dot(up)).normalize()
426
+ return { base: start.base.clone().lerp(end.base, progress), tangent, up, outward }
427
+ }
428
+
429
+ function frameQuaternion(tangent: THREE.Vector3, up: THREE.Vector3): THREE.Quaternion {
430
+ const y = up.clone().normalize()
431
+ const x = tangent.clone().addScaledVector(y, -tangent.dot(y)).normalize()
432
+ const z = new THREE.Vector3().crossVectors(x, y).normalize()
433
+ return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(x, y, z))
434
+ }
435
+
436
+ function deterministic(seed: string): number {
437
+ let hash = 2_166_136_261
438
+ for (const character of seed) {
439
+ hash ^= character.charCodeAt(0)
440
+ hash = Math.imul(hash, 16_777_619)
441
+ }
442
+ return (hash >>> 0) / 4_294_967_296
443
+ }