@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,545 @@
1
+ import * as THREE from 'three'
2
+
3
+ import { TRACK_RAIL_WALL_THICKNESS } from './track-definition'
4
+ import { canonicalWallFrameAt, type CanonicalWallRun } from './track-surface-topology'
5
+
6
+ export const VAMPIRE_CARVED_WALL_MAXIMUM_BAY_RATIO = 0.86
7
+ export const VAMPIRE_CARVED_WALL_RECESS_DEPTH_RATIO = 0.02
8
+ export const VAMPIRE_CARVED_WALL_RECESS_TO_THICKNESS = 0.125
9
+ export const VAMPIRE_CARVED_WALL_PROJECTION_RATIO = 0.026
10
+ export const VAMPIRE_CARVED_WALL_PROJECTION_TO_THICKNESS = 0.18
11
+
12
+ const MINIMUM_WALL_HEIGHT = 0.36
13
+ const WALL_BASE_SINK = 0.03
14
+
15
+ interface GeometryAccumulator {
16
+ positions: number[]
17
+ uvs: number[]
18
+ metricUvs: number[]
19
+ indices: number[]
20
+ }
21
+
22
+ interface SurfacePoint {
23
+ distance: number
24
+ height: number
25
+ across: number
26
+ }
27
+
28
+ /**
29
+ * Builds the complete visible wall as one solid, path-following batch. The
30
+ * panel field replaces the outer face and steps inward through its bevel; no
31
+ * backing facade or applied trim is part of this visual construction.
32
+ * Rapier continues to use the unchanged canonical wall collider.
33
+ */
34
+ export function addVampireCarvedWallVisual(
35
+ host: THREE.Group,
36
+ id: string,
37
+ runs: readonly CanonicalWallRun[],
38
+ wallHeight: number,
39
+ material: THREE.Material,
40
+ ): THREE.Group | undefined {
41
+ if (wallHeight < MINIMUM_WALL_HEIGHT || runs.length === 0) return undefined
42
+
43
+ const group = new THREE.Group()
44
+ group.name = `${id}-vampire-carved-wall`
45
+ group.userData.themeOnly = 'vampire-castle'
46
+ group.userData.visualOnly = true
47
+ group.userData.decorative = true
48
+ group.userData.appearance = 'vampire-castle-solid-carved-oak-wall-v2'
49
+ group.userData.representation = 'single-batched-solid-recessed-panel-wall'
50
+ group.userData.frontSideOnly = true
51
+ group.userData.drawCallBudget = 1
52
+ group.userData.runMetricLengths = runs.map(run => run.length)
53
+ group.userData.runTerminalOwnership = runs.map(run => ({
54
+ start: run.startOwnership,
55
+ end: run.endOwnership,
56
+ startPosition: run.frames[0]!.base.toArray(),
57
+ endPosition: run.frames.at(-1)!.base.toArray(),
58
+ }))
59
+ group.userData.wallHeight = wallHeight
60
+ group.userData.wallThickness = TRACK_RAIL_WALL_THICKNESS
61
+
62
+ const built = createSolidCarvedWallGeometry(runs, wallHeight)
63
+ const mesh = new THREE.Mesh(built.geometry, material)
64
+ mesh.name = `${id}-vampire-carved-wall-solid-batch`
65
+ mesh.castShadow = true
66
+ mesh.receiveShadow = true
67
+ mesh.userData.visualOnly = true
68
+ mesh.userData.decorative = true
69
+ mesh.userData.drawCallBudget = 1
70
+ mesh.userData.editorBoundsSource = 'canonical-wall-envelope'
71
+ group.userData.panelCount = built.panelCount
72
+ group.userData.recessDepth = built.recessDepth
73
+ group.userData.recessToThickness = built.recessDepth / TRACK_RAIL_WALL_THICKNESS
74
+ group.userData.maximumProjection = built.maximumProjection
75
+ group.userData.panelConstruction = 'outer-face-holes-plus-double-bevel-and-inset-fields'
76
+ group.userData.faceRule = 'both-broad-faces'
77
+ group.userData.terminalRule = 'canonical-connection-ownership'
78
+ group.userData.connectedTerminalCaps = 'open-neighbor-owned'
79
+ group.add(mesh)
80
+ host.add(group)
81
+ return group
82
+ }
83
+
84
+ function createSolidCarvedWallGeometry(
85
+ runs: readonly CanonicalWallRun[],
86
+ wallHeight: number,
87
+ ): {
88
+ geometry: THREE.BufferGeometry
89
+ panelCount: number
90
+ recessDepth: number
91
+ maximumProjection: number
92
+ } {
93
+ const output: GeometryAccumulator = { positions: [], uvs: [], metricUvs: [], indices: [] }
94
+ const maximumBaySpacing = wallHeight * VAMPIRE_CARVED_WALL_MAXIMUM_BAY_RATIO
95
+ const recessDepth = Math.min(
96
+ wallHeight * VAMPIRE_CARVED_WALL_RECESS_DEPTH_RATIO,
97
+ TRACK_RAIL_WALL_THICKNESS * VAMPIRE_CARVED_WALL_RECESS_TO_THICKNESS,
98
+ )
99
+ const maximumProjection = Math.min(
100
+ wallHeight * VAMPIRE_CARVED_WALL_PROJECTION_RATIO,
101
+ TRACK_RAIL_WALL_THICKNESS * VAMPIRE_CARVED_WALL_PROJECTION_TO_THICKNESS,
102
+ )
103
+ let panelCount = 0
104
+
105
+ for (const run of runs) {
106
+ for (const [start, end] of carvedBayIntervals(run, maximumBaySpacing)) {
107
+ for (const face of [-1, 1] as const) {
108
+ panelCount += appendCarvedBayFace(
109
+ output,
110
+ run,
111
+ face,
112
+ start,
113
+ end,
114
+ wallHeight,
115
+ recessDepth,
116
+ maximumProjection,
117
+ )
118
+ }
119
+ }
120
+ appendRunTopAndBottom(output, run, wallHeight, maximumProjection)
121
+ if (run.capStart) appendTerminalCap(output, run, 0, wallHeight, -1, maximumProjection)
122
+ if (run.capEnd) appendTerminalCap(output, run, run.length, wallHeight, 1, maximumProjection)
123
+ }
124
+
125
+ const geometry = new THREE.BufferGeometry()
126
+ geometry.name = 'vampire-castle-solid-carved-oak-wall-v2-geometry'
127
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(output.positions, 3))
128
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(output.uvs, 2))
129
+ geometry.setAttribute('uv1', new THREE.Float32BufferAttribute(output.uvs, 2))
130
+ geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(output.metricUvs, 4))
131
+ geometry.setIndex(output.indices)
132
+ geometry.computeVertexNormals()
133
+ geometry.computeBoundingBox()
134
+ geometry.computeBoundingSphere()
135
+ geometry.userData.wallThickness = TRACK_RAIL_WALL_THICKNESS
136
+ geometry.userData.wallTopologySource = 'canonical-wall-run'
137
+ geometry.userData.themeMaterialLongitudinalAxis = 'x'
138
+ geometry.userData.solidWall = true
139
+ geometry.userData.openings = 0
140
+ geometry.userData.recessDepth = recessDepth
141
+ geometry.userData.recessedFaces = 2
142
+ geometry.userData.panelFieldsPerBay = 4
143
+ geometry.userData.bevelSteps = 2
144
+ geometry.userData.maximumProjection = maximumProjection
145
+ geometry.userData.hybridRelief = 'raised-architecture-plus-recessed-panel-fields'
146
+ return { geometry, panelCount, recessDepth, maximumProjection }
147
+ }
148
+
149
+ function carvedBayIntervals(run: CanonicalWallRun, maximumBaySpacing: number): [number, number][] {
150
+ const sectionBoundaries = [0]
151
+ for (let index = 1; index < run.frames.length - 1; index += 1) {
152
+ const previous = run.frames[index - 1]!.base
153
+ const current = run.frames[index]!.base
154
+ const next = run.frames[index + 1]!.base
155
+ const incoming = current.clone().sub(previous)
156
+ const outgoing = next.clone().sub(current)
157
+ incoming.y = 0
158
+ outgoing.y = 0
159
+ if (incoming.lengthSq() <= 0.000_001 || outgoing.lengthSq() <= 0.000_001) continue
160
+ if (incoming.angleTo(outgoing) >= THREE.MathUtils.degToRad(18)) {
161
+ sectionBoundaries.push(run.frames[index]!.distance)
162
+ }
163
+ }
164
+ sectionBoundaries.push(run.length)
165
+
166
+ const intervals: [number, number][] = []
167
+ for (let section = 0; section < sectionBoundaries.length - 1; section += 1) {
168
+ const start = sectionBoundaries[section]!
169
+ const end = sectionBoundaries[section + 1]!
170
+ const length = end - start
171
+ if (length <= 0.000_001) continue
172
+ const intervalCount = Math.max(1, Math.ceil(length / maximumBaySpacing))
173
+ for (let bay = 0; bay < intervalCount; bay += 1) {
174
+ intervals.push([
175
+ THREE.MathUtils.lerp(start, end, bay / intervalCount),
176
+ THREE.MathUtils.lerp(start, end, (bay + 1) / intervalCount),
177
+ ])
178
+ }
179
+ }
180
+ return intervals
181
+ }
182
+
183
+ function appendCarvedBayFace(
184
+ output: GeometryAccumulator,
185
+ run: CanonicalWallRun,
186
+ face: -1 | 1,
187
+ start: number,
188
+ end: number,
189
+ wallHeight: number,
190
+ recessDepth: number,
191
+ maximumProjection: number,
192
+ ): number {
193
+ const bayWidth = end - start
194
+ const sideInset = Math.min(wallHeight * 0.075, bayWidth * 0.22)
195
+ const left = start + sideInset
196
+ const right = end - sideInset
197
+ const upperGap = Math.min(wallHeight * 0.014, bayWidth * 0.032)
198
+ const upperWidth = (right - left - upperGap * 2) / 3
199
+ const panels: THREE.Vector2[][] = [
200
+ gothicLancetContour(left, right, wallHeight * 0.12, wallHeight * 0.46, wallHeight * 0.625, 7),
201
+ ...Array.from({ length: 3 }, (_unused, index) => {
202
+ const panelLeft = left + index * (upperWidth + upperGap)
203
+ return gothicLancetContour(
204
+ panelLeft,
205
+ panelLeft + upperWidth,
206
+ wallHeight * 0.66,
207
+ wallHeight * 0.79,
208
+ wallHeight * 0.91,
209
+ 5,
210
+ )
211
+ }),
212
+ ].map(clockwiseContour)
213
+ const bevelWidth = Math.min(wallHeight * 0.024, bayWidth * 0.055)
214
+ const halfThickness = TRACK_RAIL_WALL_THICKNESS * 0.5
215
+ const longitudinalSegments = 26
216
+ const verticalSegments = 32
217
+ const vertexOffset = output.positions.length / 3
218
+ for (let vertical = 0; vertical <= verticalSegments; vertical += 1) {
219
+ const height = (vertical / verticalSegments) * wallHeight
220
+ for (let longitudinal = 0; longitudinal <= longitudinalSegments; longitudinal += 1) {
221
+ const distance = THREE.MathUtils.lerp(start, end, longitudinal / longitudinalSegments)
222
+ const depth = architecturalDepthAt(
223
+ new THREE.Vector2(distance, height),
224
+ panels,
225
+ start,
226
+ end,
227
+ wallHeight,
228
+ bevelWidth,
229
+ recessDepth,
230
+ maximumProjection,
231
+ )
232
+ appendGridVertex(output, run, {
233
+ distance,
234
+ height,
235
+ across: face * (halfThickness - depth),
236
+ })
237
+ }
238
+ }
239
+ const stride = longitudinalSegments + 1
240
+ for (let vertical = 0; vertical < verticalSegments; vertical += 1) {
241
+ for (let longitudinal = 0; longitudinal < longitudinalSegments; longitudinal += 1) {
242
+ const a = vertexOffset + vertical * stride + longitudinal
243
+ const b = a + 1
244
+ const c = a + stride
245
+ const d = c + 1
246
+ const distance = THREE.MathUtils.lerp(start, end, (longitudinal + 0.5) / longitudinalSegments)
247
+ const expected = canonicalWallFrameAt(run, distance).outward.clone().multiplyScalar(face)
248
+ appendFacingIndex(output, a, b, c, expected)
249
+ appendFacingIndex(output, b, d, c, expected)
250
+ }
251
+ }
252
+ return panels.length
253
+ }
254
+
255
+ function appendGridVertex(output: GeometryAccumulator, run: CanonicalWallRun, point: SurfacePoint): void {
256
+ const world = mapSurfacePoint(run, point)
257
+ output.positions.push(world.x, world.y, world.z)
258
+ output.uvs.push(point.distance, point.height)
259
+ output.metricUvs.push(point.height, point.distance, 1, run.length)
260
+ }
261
+
262
+ function appendFacingIndex(
263
+ output: GeometryAccumulator,
264
+ a: number,
265
+ b: number,
266
+ c: number,
267
+ expectedNormal: THREE.Vector3,
268
+ ): void {
269
+ const pointAt = (index: number) =>
270
+ new THREE.Vector3(
271
+ output.positions[index * 3]!,
272
+ output.positions[index * 3 + 1]!,
273
+ output.positions[index * 3 + 2]!,
274
+ )
275
+ const origin = pointAt(a)
276
+ const normal = pointAt(b).sub(origin).cross(pointAt(c).sub(origin))
277
+ if (normal.dot(expectedNormal) >= 0) output.indices.push(a, b, c)
278
+ else output.indices.push(a, c, b)
279
+ }
280
+
281
+ function carvedDepthAt(
282
+ point: THREE.Vector2,
283
+ panels: readonly THREE.Vector2[][],
284
+ bevelWidth: number,
285
+ recessDepth: number,
286
+ ): number {
287
+ for (const panel of panels) {
288
+ if (!pointInsideContour(point, panel)) continue
289
+ const edgeDistance = distanceToContour(point, panel)
290
+ const firstStep = bevelWidth
291
+ const secondStep = bevelWidth * 2.15
292
+ if (edgeDistance <= firstStep) {
293
+ return recessDepth * 0.44 * THREE.MathUtils.smoothstep(edgeDistance, 0, firstStep)
294
+ }
295
+ if (edgeDistance <= secondStep) {
296
+ return THREE.MathUtils.lerp(
297
+ recessDepth * 0.44,
298
+ recessDepth,
299
+ THREE.MathUtils.smoothstep(edgeDistance, firstStep, secondStep),
300
+ )
301
+ }
302
+ return recessDepth
303
+ }
304
+ return 0
305
+ }
306
+
307
+ function architecturalDepthAt(
308
+ point: THREE.Vector2,
309
+ panels: readonly THREE.Vector2[][],
310
+ start: number,
311
+ end: number,
312
+ wallHeight: number,
313
+ bevelWidth: number,
314
+ recessDepth: number,
315
+ maximumProjection: number,
316
+ ): number {
317
+ const bayWidth = end - start
318
+ const pilasterWidth = Math.min(wallHeight * 0.04, bayWidth * 0.075)
319
+ const edgeDistance = Math.min(Math.abs(point.x - start), Math.abs(end - point.x))
320
+ let projection =
321
+ edgeDistance <= pilasterWidth
322
+ ? maximumProjection * (0.62 + 0.38 * (1 - THREE.MathUtils.smoothstep(edgeDistance, 0, pilasterWidth)))
323
+ : 0
324
+
325
+ const crownStart = wallHeight * 0.91
326
+ const plinthEnd = wallHeight * 0.105
327
+ if (point.y >= crownStart) {
328
+ projection = Math.max(
329
+ projection,
330
+ maximumProjection * (0.72 + 0.28 * THREE.MathUtils.smoothstep(point.y, crownStart, wallHeight)),
331
+ )
332
+ }
333
+ if (point.y <= plinthEnd) {
334
+ projection = Math.max(
335
+ projection,
336
+ maximumProjection * (0.72 + 0.28 * (1 - THREE.MathUtils.smoothstep(point.y, 0, plinthEnd))),
337
+ )
338
+ }
339
+
340
+ const moldingWidth = bevelWidth * 1.45
341
+ for (const panel of panels) {
342
+ const contourDistance = distanceToContour(point, panel)
343
+ if (contourDistance > moldingWidth) continue
344
+ const moldingProjection =
345
+ maximumProjection * 0.82 * (1 - THREE.MathUtils.smoothstep(contourDistance, 0, moldingWidth))
346
+ projection = Math.max(projection, moldingProjection)
347
+ }
348
+ return projection > 0.000_001 ? -projection : carvedDepthAt(point, panels, bevelWidth, recessDepth)
349
+ }
350
+
351
+ function distanceToContour(point: THREE.Vector2, contour: readonly THREE.Vector2[]): number {
352
+ let closestDistanceSquared = Number.POSITIVE_INFINITY
353
+ for (let index = 0; index < contour.length; index += 1) {
354
+ const start = contour[index]!
355
+ const end = contour[(index + 1) % contour.length]!
356
+ const dx = end.x - start.x
357
+ const dy = end.y - start.y
358
+ const lengthSquared = dx * dx + dy * dy
359
+ const progress =
360
+ lengthSquared <= 0.000_001
361
+ ? 0
362
+ : THREE.MathUtils.clamp(((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared, 0, 1)
363
+ const offsetX = point.x - (start.x + dx * progress)
364
+ const offsetY = point.y - (start.y + dy * progress)
365
+ closestDistanceSquared = Math.min(closestDistanceSquared, offsetX * offsetX + offsetY * offsetY)
366
+ }
367
+ return Math.sqrt(closestDistanceSquared)
368
+ }
369
+
370
+ function pointInsideContour(point: THREE.Vector2, contour: readonly THREE.Vector2[]): boolean {
371
+ let inside = false
372
+ for (let index = 0, previous = contour.length - 1; index < contour.length; previous = index, index += 1) {
373
+ const currentPoint = contour[index]!
374
+ const previousPoint = contour[previous]!
375
+ const crosses =
376
+ currentPoint.y > point.y !== previousPoint.y > point.y &&
377
+ point.x <
378
+ ((previousPoint.x - currentPoint.x) * (point.y - currentPoint.y)) /
379
+ (previousPoint.y - currentPoint.y) +
380
+ currentPoint.x
381
+ if (crosses) inside = !inside
382
+ }
383
+ return inside
384
+ }
385
+
386
+ function gothicLancetContour(
387
+ left: number,
388
+ right: number,
389
+ bottom: number,
390
+ shoulder: number,
391
+ apex: number,
392
+ curveSegments: number,
393
+ ): THREE.Vector2[] {
394
+ const center = (left + right) * 0.5
395
+ const width = right - left
396
+ const rise = apex - shoulder
397
+ const points = [new THREE.Vector2(left, bottom), new THREE.Vector2(left, shoulder)]
398
+ appendCubicArc(
399
+ points,
400
+ new THREE.Vector2(left, shoulder),
401
+ new THREE.Vector2(left + width * 0.05, shoulder + rise * 0.62),
402
+ new THREE.Vector2(center - width * 0.13, apex - rise * 0.16),
403
+ new THREE.Vector2(center, apex),
404
+ curveSegments,
405
+ )
406
+ appendCubicArc(
407
+ points,
408
+ new THREE.Vector2(center, apex),
409
+ new THREE.Vector2(center + width * 0.13, apex - rise * 0.16),
410
+ new THREE.Vector2(right - width * 0.05, shoulder + rise * 0.62),
411
+ new THREE.Vector2(right, shoulder),
412
+ curveSegments,
413
+ )
414
+ points.push(new THREE.Vector2(right, bottom))
415
+ return points
416
+ }
417
+
418
+ function appendCubicArc(
419
+ target: THREE.Vector2[],
420
+ start: THREE.Vector2,
421
+ controlA: THREE.Vector2,
422
+ controlB: THREE.Vector2,
423
+ end: THREE.Vector2,
424
+ segments: number,
425
+ ): void {
426
+ for (let segment = 1; segment <= segments; segment += 1) {
427
+ const progress = segment / segments
428
+ const inverse = 1 - progress
429
+ target.push(
430
+ new THREE.Vector2(
431
+ inverse ** 3 * start.x +
432
+ 3 * inverse ** 2 * progress * controlA.x +
433
+ 3 * inverse * progress ** 2 * controlB.x +
434
+ progress ** 3 * end.x,
435
+ inverse ** 3 * start.y +
436
+ 3 * inverse ** 2 * progress * controlA.y +
437
+ 3 * inverse * progress ** 2 * controlB.y +
438
+ progress ** 3 * end.y,
439
+ ),
440
+ )
441
+ }
442
+ }
443
+
444
+ function clockwiseContour(points: THREE.Vector2[]): THREE.Vector2[] {
445
+ return THREE.ShapeUtils.isClockWise(points) ? points : [...points].reverse()
446
+ }
447
+
448
+ function appendRunTopAndBottom(
449
+ output: GeometryAccumulator,
450
+ run: CanonicalWallRun,
451
+ wallHeight: number,
452
+ maximumProjection: number,
453
+ ): void {
454
+ const halfThickness = TRACK_RAIL_WALL_THICKNESS * 0.5 + maximumProjection
455
+ for (let index = 0; index < run.frames.length - 1; index += 1) {
456
+ const start = run.frames[index]!.distance
457
+ const end = run.frames[index + 1]!.distance
458
+ const midpoint = canonicalWallFrameAt(run, (start + end) * 0.5)
459
+ const bottom: [SurfacePoint, SurfacePoint, SurfacePoint, SurfacePoint] = [
460
+ { distance: start, height: 0, across: -halfThickness },
461
+ { distance: end, height: 0, across: -halfThickness },
462
+ { distance: end, height: 0, across: halfThickness },
463
+ { distance: start, height: 0, across: halfThickness },
464
+ ]
465
+ const narrowSurfaceUv = (point: SurfacePoint): readonly [number, number] => [
466
+ point.across + halfThickness,
467
+ point.distance,
468
+ ]
469
+ appendMappedQuad(output, run, bottom, midpoint.up.clone().negate(), narrowSurfaceUv)
470
+ const top = bottom.map(point => ({ ...point, height: wallHeight })) as typeof bottom
471
+ appendMappedQuad(output, run, top, midpoint.up, narrowSurfaceUv)
472
+ }
473
+ }
474
+
475
+ function appendTerminalCap(
476
+ output: GeometryAccumulator,
477
+ run: CanonicalWallRun,
478
+ distance: number,
479
+ wallHeight: number,
480
+ direction: -1 | 1,
481
+ maximumProjection: number,
482
+ ): void {
483
+ const halfThickness = TRACK_RAIL_WALL_THICKNESS * 0.5 + maximumProjection
484
+ const cap: [SurfacePoint, SurfacePoint, SurfacePoint, SurfacePoint] = [
485
+ { distance, height: 0, across: -halfThickness },
486
+ { distance, height: 0, across: halfThickness },
487
+ { distance, height: wallHeight, across: halfThickness },
488
+ { distance, height: wallHeight, across: -halfThickness },
489
+ ]
490
+ const terminalPathNormal = (() => {
491
+ if (run.frames.length < 2) return canonicalWallFrameAt(run, distance).tangent.clone().multiplyScalar(direction)
492
+ if (direction < 0) {
493
+ return run.frames[0]!.base.clone().sub(run.frames[1]!.base).setY(0).normalize()
494
+ }
495
+ return run.frames.at(-1)!.base.clone().sub(run.frames.at(-2)!.base).setY(0).normalize()
496
+ })()
497
+ appendMappedQuad(
498
+ output,
499
+ run,
500
+ cap,
501
+ terminalPathNormal,
502
+ point => [point.across + halfThickness, point.height],
503
+ )
504
+ }
505
+
506
+ function appendMappedQuad(
507
+ output: GeometryAccumulator,
508
+ run: CanonicalWallRun,
509
+ points: [SurfacePoint, SurfacePoint, SurfacePoint, SurfacePoint],
510
+ expectedNormal: THREE.Vector3,
511
+ uvForPoint?: (point: SurfacePoint) => readonly [number, number],
512
+ ): void {
513
+ appendMappedTriangle(output, run, [points[0], points[1], points[2]], expectedNormal, uvForPoint)
514
+ appendMappedTriangle(output, run, [points[0], points[2], points[3]], expectedNormal, uvForPoint)
515
+ }
516
+
517
+ function appendMappedTriangle(
518
+ output: GeometryAccumulator,
519
+ run: CanonicalWallRun,
520
+ points: [SurfacePoint, SurfacePoint, SurfacePoint],
521
+ expectedNormal: THREE.Vector3,
522
+ uvForPoint?: (point: SurfacePoint) => readonly [number, number],
523
+ ): void {
524
+ const mapped = points.map(point => mapSurfacePoint(run, point)) as [THREE.Vector3, THREE.Vector3, THREE.Vector3]
525
+ const normal = mapped[1].clone().sub(mapped[0]).cross(mapped[2].clone().sub(mapped[0]))
526
+ const order = normal.dot(expectedNormal) >= 0 ? [0, 1, 2] : [0, 2, 1]
527
+ const baseIndex = output.positions.length / 3
528
+ for (const index of order) {
529
+ const point = points[index]!
530
+ const world = mapped[index]!
531
+ const uv = uvForPoint?.(point) ?? [point.distance, point.height]
532
+ output.positions.push(world.x, world.y, world.z)
533
+ output.uvs.push(uv[0], uv[1])
534
+ output.metricUvs.push(point.height, point.distance, 1, run.length)
535
+ }
536
+ output.indices.push(baseIndex, baseIndex + 1, baseIndex + 2)
537
+ }
538
+
539
+ function mapSurfacePoint(run: CanonicalWallRun, point: SurfacePoint): THREE.Vector3 {
540
+ const frame = canonicalWallFrameAt(run, point.distance)
541
+ return frame.base
542
+ .clone()
543
+ .addScaledVector(frame.outward, point.across)
544
+ .addScaledVector(frame.up, point.height - WALL_BASE_SINK)
545
+ }