@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.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/assets.json +346 -0
- package/package.json +32 -0
- package/release.json +135 -0
- package/src/audio/game-audio-preference.ts +9 -0
- package/src/authoritative.ts +6 -0
- package/src/catalog.ts +1 -0
- package/src/index.ts +14 -0
- package/src/legacy-stock-catalog.ts +130 -0
- package/src/pages/lobby/lobby-types.ts +140 -0
- package/src/pages/lobby/marble-factory.ts +931 -0
- package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
- package/src/pages/play/race/egypt-portal-frame.ts +382 -0
- package/src/pages/play/race/finish-detection.ts +1 -0
- package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
- package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
- package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
- package/src/pages/play/race/jungle-portal-frame.ts +308 -0
- package/src/pages/play/race/jungle-temple-lift.ts +366 -0
- package/src/pages/play/race/marble-entity.ts +257 -0
- package/src/pages/play/race/marble-material.ts +20 -0
- package/src/pages/play/race/marble-race-engine.ts +2652 -0
- package/src/pages/play/race/path-pattern-layout.ts +142 -0
- package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
- package/src/pages/play/race/physics-debug.ts +1 -0
- package/src/pages/play/race/physics-dry-run.ts +90 -0
- package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
- package/src/pages/play/race/portal-pair-color.ts +20 -0
- package/src/pages/play/race/portal-proportions.ts +25 -0
- package/src/pages/play/race/portal-visual.ts +1108 -0
- package/src/pages/play/race/race-camera-director.ts +586 -0
- package/src/pages/play/race/race-camera-obstruction.ts +37 -0
- package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
- package/src/pages/play/race/race-camera.ts +206 -0
- package/src/pages/play/race/race-completion.ts +5 -0
- package/src/pages/play/race/race-configuration.ts +57 -0
- package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
- package/src/pages/play/race/race-physics-profile.ts +7 -0
- package/src/pages/play/race/race-physics.ts +16 -0
- package/src/pages/play/race/race-playback.ts +41 -0
- package/src/pages/play/race/race-rolling-audio.ts +616 -0
- package/src/pages/play/race/race-simulation-renderer.ts +221 -0
- package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
- package/src/pages/play/race/race-transform-interpolation.ts +50 -0
- package/src/pages/play/race/race-types.ts +12 -0
- package/src/pages/play/race/rebound-visual.ts +70 -0
- package/src/pages/play/race/seeded-random.ts +23 -0
- package/src/pages/play/race/space-station-portal-frame.ts +275 -0
- package/src/pages/play/race/track-appearance-canopy.ts +946 -0
- package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
- package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
- package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
- package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
- package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
- package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
- package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
- package/src/pages/play/race/track-appearance-support.ts +983 -0
- package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
- package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
- package/src/pages/play/race/track-course.ts +271 -0
- package/src/pages/play/race/track-definition.ts +443 -0
- package/src/pages/play/race/track-fence-architecture.ts +33 -0
- package/src/pages/play/race/track-floor-material-rules.ts +51 -0
- package/src/pages/play/race/track-mechanisms.ts +1350 -0
- package/src/pages/play/race/track-physics-surface.ts +6 -0
- package/src/pages/play/race/track-route-network.ts +9 -0
- package/src/pages/play/race/track-shadow-batching.ts +760 -0
- package/src/pages/play/race/track-surface-basis.ts +1 -0
- package/src/pages/play/race/track-surface-topology.ts +326 -0
- package/src/pages/play/race/track-world.ts +10181 -0
- package/src/pages/play/race/under-track-support-architecture.ts +122 -0
- package/src/pages/play/race/vortex-profile.ts +5 -0
- package/src/pages/play/race/vortex-water-effect.ts +863 -0
- package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
- package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
- package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
- package/src/pages/track-lab/cone-field-layout.ts +1 -0
- package/src/pages/track-lab/conveyor-layout.ts +1 -0
- package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
- package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
- package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
- package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
- package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
- package/src/pages/track-lab/flipper-component.ts +127 -0
- package/src/pages/track-lab/rising-wall-layout.ts +1 -0
- package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
- package/src/pages/track-lab/track-piece-layout.ts +28 -0
- package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
- package/src/three/geometry-attribute-aliases.ts +24 -0
- package/src/three/geometry-detail.ts +36 -0
- package/src/three/homepage-marble-geometry.ts +15 -0
- package/src/three/japanese-sakura-appearance.ts +252 -0
- package/src/three/jungle-flower-hedge-appearance.ts +7 -0
- package/src/three/jungle-temple-appearance.ts +5 -0
- package/src/three/low-quality-scene-renderer.ts +132 -0
- package/src/three/marble-sphere-geometry.ts +13 -0
- package/src/three/mobile-rendering-budget.ts +11 -0
- package/src/three/painted-valley-appearance.ts +270 -0
- package/src/three/railroad-appearance.ts +71 -0
- package/src/three/renderer-resize.ts +32 -0
- package/src/three/rope-wall-appearance.ts +91 -0
- package/src/three/rugged-track-appearance.ts +2 -0
- package/src/three/scene-lighting.ts +183 -0
- package/src/three/scene-shader-preparation.ts +64 -0
- package/src/three/scene-theme-controller.ts +461 -0
- package/src/three/shadow-shader-preparation.ts +162 -0
- package/src/three/shared-obstacle-metal.ts +44 -0
- package/src/three/soft-additive-particles.ts +57 -0
- package/src/three/space-station-appearance.ts +213 -0
- package/src/three/static-track-batched-mesh.ts +132 -0
- package/src/three/stone-track-shell-appearance.ts +12 -0
- package/src/three/theme-ambience-controller.ts +226 -0
- package/src/three/theme-asset-cache.ts +120 -0
- package/src/three/theme-audio-preference.ts +43 -0
- package/src/three/theme-catalog.ts +1457 -0
- package/src/three/theme-material-assets.ts +258 -0
- package/src/three/theme-material-ktx2-manifest.ts +190 -0
- package/src/three/theme-material-registry.ts +1952 -0
- package/src/three/theme-world-factory.ts +590 -0
- package/src/three/track-connector-batch.ts +125 -0
- package/src/three/track-material-continuity.ts +442 -0
- package/src/three/track-theme.ts +535 -0
- package/src/three/vampire-castle-appearance.ts +258 -0
- package/src/three/visual-frame-pacing.ts +19 -0
- package/src/three/visual-geometry.ts +120 -0
- package/src/three/visual-quality.ts +83 -0
- package/src/three/water-appearance.ts +29 -0
- package/src/three/wild-west-sand-appearance.ts +2 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualCylinderGeometry, VisualBoxGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
|
|
5
|
+
import type { TrackAppearanceCanopyStyle } from '../../../three/track-theme'
|
|
6
|
+
|
|
7
|
+
export interface AppearanceCanopyFrame {
|
|
8
|
+
point: THREE.Vector3
|
|
9
|
+
tangent: THREE.Vector3
|
|
10
|
+
right: THREE.Vector3
|
|
11
|
+
up: THREE.Vector3
|
|
12
|
+
width: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AppearanceCanopyMaterials {
|
|
16
|
+
wall: THREE.Material
|
|
17
|
+
timber: THREE.Material
|
|
18
|
+
metal: THREE.Material
|
|
19
|
+
sakuraKawara?: THREE.Material
|
|
20
|
+
sakuraPlaster?: THREE.Material
|
|
21
|
+
sakuraMasonry?: THREE.Material
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Beam {
|
|
25
|
+
start: THREE.Vector3
|
|
26
|
+
end: THREE.Vector3
|
|
27
|
+
width: number
|
|
28
|
+
depth: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Fastener {
|
|
32
|
+
point: THREE.Vector3
|
|
33
|
+
outward: THREE.Vector3
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CanopySection {
|
|
37
|
+
frame: AppearanceCanopyFrame
|
|
38
|
+
leftEave: THREE.Vector3
|
|
39
|
+
ridge: THREE.Vector3
|
|
40
|
+
rightEave: THREE.Vector3
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CoveredRun {
|
|
44
|
+
frames: AppearanceCanopyFrame[]
|
|
45
|
+
touchesStart: boolean
|
|
46
|
+
touchesEnd: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AppearanceCanopyOptions {
|
|
50
|
+
capStart?: boolean
|
|
51
|
+
capEnd?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const LOCAL_UP = new THREE.Vector3(0, 1, 0)
|
|
55
|
+
export const WILD_WEST_COVERED_MINE_CANOPY_ID = 'wild-west.covered-mine-canopy'
|
|
56
|
+
export const JAPANESE_SAKURA_ROOFED_GATE_CANOPY_ID = 'japanese-sakura.roofed-gate-canopy'
|
|
57
|
+
/** The standard six-unit covered-mine roof rises 0.92 wu over a 3.32 wu half-span. */
|
|
58
|
+
export const WILD_WEST_COVERED_MINE_ROOF_PITCH = 0.92 / 3.32
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Intersections and Vortices have no single covered corridor. Start and finish
|
|
62
|
+
* pieces intentionally remain plain race terminals. Their walls still accept
|
|
63
|
+
* ordinary visual appearances, but roof presets would add architecture where
|
|
64
|
+
* none should exist.
|
|
65
|
+
*/
|
|
66
|
+
export function supportsTrackAppearanceCanopy(runtimeKind: string): boolean {
|
|
67
|
+
return runtimeKind !== 'intersection' && runtimeKind !== 'vortex' && runtimeKind !== 'start' && runtimeKind !== 'finish'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function appearanceCanopyStyle(
|
|
71
|
+
material: THREE.Material,
|
|
72
|
+
appearanceId?: string,
|
|
73
|
+
): TrackAppearanceCanopyStyle | undefined {
|
|
74
|
+
if (appearanceId === WILD_WEST_COVERED_MINE_CANOPY_ID) return 'wild-west-covered-mine'
|
|
75
|
+
if (appearanceId === JAPANESE_SAKURA_ROOFED_GATE_CANOPY_ID) return 'japanese-sakura-roofed-gate'
|
|
76
|
+
const style = material.userData.themeAppearanceCanopyStyle
|
|
77
|
+
return style === 'wild-west-covered-mine' || style === 'japanese-sakura-roofed-gate'
|
|
78
|
+
? style
|
|
79
|
+
: undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function addTrackAppearanceCanopy(
|
|
83
|
+
root: THREE.Group,
|
|
84
|
+
id: string,
|
|
85
|
+
frames: readonly AppearanceCanopyFrame[],
|
|
86
|
+
negativeWallSegments: readonly boolean[],
|
|
87
|
+
positiveWallSegments: readonly boolean[],
|
|
88
|
+
materials: AppearanceCanopyMaterials,
|
|
89
|
+
style: TrackAppearanceCanopyStyle | undefined,
|
|
90
|
+
options: AppearanceCanopyOptions = {},
|
|
91
|
+
): void {
|
|
92
|
+
if (!style || frames.length < 2) return
|
|
93
|
+
const runs = collectCoveredRuns(frames, negativeWallSegments, positiveWallSegments)
|
|
94
|
+
if (runs.length === 0) return
|
|
95
|
+
|
|
96
|
+
if (style === 'wild-west-covered-mine') {
|
|
97
|
+
runs.forEach((run, index) =>
|
|
98
|
+
addWildWestCoveredMine(root, `${id}-canopy-${index + 1}`, run.frames, materials, {
|
|
99
|
+
includeStartSupport: !run.touchesStart || options.capStart !== false,
|
|
100
|
+
includeEndSupport: !run.touchesEnd || options.capEnd !== false,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (style === 'japanese-sakura-roofed-gate') {
|
|
106
|
+
runs.forEach((run, index) =>
|
|
107
|
+
addSakuraRoofedGate(root, `${id}-canopy-${index + 1}`, run.frames, materials, {
|
|
108
|
+
includeStartSupport: !run.touchesStart || options.capStart !== false,
|
|
109
|
+
includeEndSupport: !run.touchesEnd || options.capEnd !== false,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function collectCoveredRuns(
|
|
116
|
+
frames: readonly AppearanceCanopyFrame[],
|
|
117
|
+
negativeWallSegments: readonly boolean[],
|
|
118
|
+
positiveWallSegments: readonly boolean[],
|
|
119
|
+
): CoveredRun[] {
|
|
120
|
+
const runs: CoveredRun[] = []
|
|
121
|
+
let runStart: number | undefined
|
|
122
|
+
for (let segment = 0; segment <= frames.length - 1; segment += 1) {
|
|
123
|
+
const covered = Boolean(negativeWallSegments[segment] && positiveWallSegments[segment])
|
|
124
|
+
if (covered && runStart === undefined) runStart = segment
|
|
125
|
+
if (covered || runStart === undefined) continue
|
|
126
|
+
const runFrames = frames.slice(runStart, segment + 1)
|
|
127
|
+
if (runFrames.length >= 2) {
|
|
128
|
+
runs.push({
|
|
129
|
+
frames: runFrames.map(cloneFrame),
|
|
130
|
+
touchesStart: runStart === 0,
|
|
131
|
+
touchesEnd: segment === frames.length - 1,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
runStart = undefined
|
|
135
|
+
}
|
|
136
|
+
return runs
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cloneFrame(frame: AppearanceCanopyFrame): AppearanceCanopyFrame {
|
|
140
|
+
return {
|
|
141
|
+
point: frame.point.clone(),
|
|
142
|
+
tangent: frame.tangent.clone(),
|
|
143
|
+
right: frame.right.clone(),
|
|
144
|
+
up: frame.up.clone(),
|
|
145
|
+
width: frame.width,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function sampleFrames(frames: readonly AppearanceCanopyFrame[], spacing: number): AppearanceCanopyFrame[] {
|
|
150
|
+
const distances = [0]
|
|
151
|
+
for (let index = 0; index < frames.length - 1; index += 1) {
|
|
152
|
+
distances.push(distances.at(-1)! + frames[index]!.point.distanceTo(frames[index + 1]!.point))
|
|
153
|
+
}
|
|
154
|
+
const length = distances.at(-1) ?? 0
|
|
155
|
+
const count = Math.max(2, Math.ceil(length / spacing) + 1)
|
|
156
|
+
return Array.from({ length: count }, (_, sampleIndex) => {
|
|
157
|
+
const target = (sampleIndex / (count - 1)) * length
|
|
158
|
+
let segment = 0
|
|
159
|
+
while (segment < distances.length - 2 && distances[segment + 1]! < target) segment += 1
|
|
160
|
+
const segmentLength = distances[segment + 1]! - distances[segment]!
|
|
161
|
+
const progress = segmentLength <= 0.000_001 ? 0 : (target - distances[segment]!) / segmentLength
|
|
162
|
+
return interpolateFrame(frames[segment]!, frames[segment + 1]!, progress)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function interpolateFrame(
|
|
167
|
+
start: AppearanceCanopyFrame,
|
|
168
|
+
end: AppearanceCanopyFrame,
|
|
169
|
+
progress: number,
|
|
170
|
+
): AppearanceCanopyFrame {
|
|
171
|
+
const tangent = start.tangent.clone().lerp(end.tangent, progress).normalize()
|
|
172
|
+
const rightHint = start.right.clone().lerp(end.right, progress)
|
|
173
|
+
rightHint.addScaledVector(tangent, -rightHint.dot(tangent))
|
|
174
|
+
if (rightHint.lengthSq() < 0.000_001) rightHint.crossVectors(start.up, tangent)
|
|
175
|
+
const right = rightHint.normalize()
|
|
176
|
+
const up = new THREE.Vector3().crossVectors(tangent, right).normalize()
|
|
177
|
+
const upHint = start.up.clone().lerp(end.up, progress)
|
|
178
|
+
if (up.dot(upHint) < 0) {
|
|
179
|
+
right.negate()
|
|
180
|
+
up.negate()
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
point: start.point.clone().lerp(end.point, progress),
|
|
184
|
+
tangent,
|
|
185
|
+
right,
|
|
186
|
+
up,
|
|
187
|
+
width: THREE.MathUtils.lerp(start.width, end.width, progress),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface SupportOptions {
|
|
192
|
+
includeStartSupport: boolean
|
|
193
|
+
includeEndSupport: boolean
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function supportSections<T>(sections: readonly T[], options: SupportOptions): { section: T; index: number }[] {
|
|
197
|
+
return sections.flatMap((section, index) => {
|
|
198
|
+
if (index === 0 && !options.includeStartSupport) return []
|
|
199
|
+
if (index === sections.length - 1 && !options.includeEndSupport) return []
|
|
200
|
+
return [{ section, index }]
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function addOpenPortSupportInsets(
|
|
205
|
+
frames: readonly AppearanceCanopyFrame[],
|
|
206
|
+
options: SupportOptions,
|
|
207
|
+
): AppearanceCanopyFrame[] {
|
|
208
|
+
const inset = 0.32
|
|
209
|
+
const result = [...frames]
|
|
210
|
+
if (!options.includeStartSupport) {
|
|
211
|
+
const start = result[0]!
|
|
212
|
+
const next = result[1]!
|
|
213
|
+
const distance = start.point.distanceTo(next.point)
|
|
214
|
+
if (distance > inset * 1.25) result.splice(1, 0, interpolateFrame(start, next, inset / distance))
|
|
215
|
+
}
|
|
216
|
+
if (!options.includeEndSupport) {
|
|
217
|
+
const previous = result.at(-2)!
|
|
218
|
+
const end = result.at(-1)!
|
|
219
|
+
const distance = previous.point.distanceTo(end.point)
|
|
220
|
+
if (distance > inset * 1.25) {
|
|
221
|
+
result.splice(result.length - 1, 0, interpolateFrame(previous, end, 1 - inset / distance))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return result
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function addWildWestCoveredMine(
|
|
228
|
+
root: THREE.Group,
|
|
229
|
+
id: string,
|
|
230
|
+
runFrames: readonly AppearanceCanopyFrame[],
|
|
231
|
+
materials: AppearanceCanopyMaterials,
|
|
232
|
+
supportOptions: SupportOptions,
|
|
233
|
+
): void {
|
|
234
|
+
const eaveHeight = 2.8
|
|
235
|
+
const makeSection = (frame: AppearanceCanopyFrame): CanopySection => {
|
|
236
|
+
const halfSpan = frame.width / 2 + 0.32
|
|
237
|
+
const ridgeHeight = eaveHeight + halfSpan * WILD_WEST_COVERED_MINE_ROOF_PITCH
|
|
238
|
+
return {
|
|
239
|
+
frame,
|
|
240
|
+
leftEave: frame.point.clone().addScaledVector(frame.right, -halfSpan).addScaledVector(LOCAL_UP, eaveHeight),
|
|
241
|
+
ridge: frame.point.clone().addScaledVector(LOCAL_UP, ridgeHeight),
|
|
242
|
+
rightEave: frame.point.clone().addScaledVector(frame.right, halfSpan).addScaledVector(LOCAL_UP, eaveHeight),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const sections = addOpenPortSupportInsets(sampleFrames(runFrames, 3.35), supportOptions).map(makeSection)
|
|
246
|
+
const roofSections = sampleFrames(runFrames, 0.55).map(makeSection)
|
|
247
|
+
const timbers: Beam[] = []
|
|
248
|
+
const braces: Beam[] = []
|
|
249
|
+
const eaveFascias: Beam[] = []
|
|
250
|
+
const ridgeCaps: Beam[] = []
|
|
251
|
+
const ironStraps: Beam[] = []
|
|
252
|
+
const hardwarePlates: Beam[] = []
|
|
253
|
+
const fasteners: Fastener[] = []
|
|
254
|
+
const supported = supportSections(sections, supportOptions)
|
|
255
|
+
let portalFrameCount = 0
|
|
256
|
+
for (const { section, index } of supported) {
|
|
257
|
+
const halfSpan = section.frame.width / 2 + 0.32
|
|
258
|
+
const leftBase = section.frame.point.clone().addScaledVector(section.frame.right, -halfSpan).addScaledVector(LOCAL_UP, -0.15)
|
|
259
|
+
const rightBase = section.frame.point.clone().addScaledVector(section.frame.right, halfSpan).addScaledVector(LOCAL_UP, -0.15)
|
|
260
|
+
const isPortalBent =
|
|
261
|
+
(!supportOptions.includeStartSupport && index === 1) ||
|
|
262
|
+
(!supportOptions.includeEndSupport && index === sections.length - 2)
|
|
263
|
+
if (isPortalBent) portalFrameCount += 1
|
|
264
|
+
const postWidth = 0.38 + (isPortalBent ? 0.035 : 0)
|
|
265
|
+
const rafterWidth = 0.28 + (isPortalBent ? 0.025 : 0)
|
|
266
|
+
timbers.push(
|
|
267
|
+
beam(leftBase, section.leftEave, postWidth, postWidth),
|
|
268
|
+
beam(rightBase, section.rightEave, postWidth, postWidth),
|
|
269
|
+
beam(section.leftEave, section.ridge, rafterWidth, rafterWidth * 0.9),
|
|
270
|
+
beam(section.ridge, section.rightEave, rafterWidth, rafterWidth * 0.9),
|
|
271
|
+
beam(section.leftEave, section.rightEave, 0.34, 0.3),
|
|
272
|
+
)
|
|
273
|
+
for (const side of [-1, 1] as const) {
|
|
274
|
+
const postCenter = section.frame.point.clone().addScaledVector(section.frame.right, side * halfSpan)
|
|
275
|
+
ironStraps.push(
|
|
276
|
+
beam(
|
|
277
|
+
postCenter.clone().addScaledVector(section.frame.tangent, -0.14).addScaledVector(LOCAL_UP, 0.42),
|
|
278
|
+
postCenter.clone().addScaledVector(section.frame.tangent, 0.14).addScaledVector(LOCAL_UP, 0.42),
|
|
279
|
+
postWidth + 0.1,
|
|
280
|
+
postWidth + 0.1,
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
hardwarePlates.push(
|
|
284
|
+
beam(
|
|
285
|
+
postCenter.clone().addScaledVector(section.frame.tangent, -0.15).addScaledVector(LOCAL_UP, eaveHeight - 0.12),
|
|
286
|
+
postCenter.clone().addScaledVector(section.frame.tangent, 0.15).addScaledVector(LOCAL_UP, eaveHeight - 0.12),
|
|
287
|
+
postWidth + 0.12,
|
|
288
|
+
0.065,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
const outward = section.frame.right.clone().multiplyScalar(side).normalize()
|
|
292
|
+
for (const tangentOffset of [-0.11, 0.11]) {
|
|
293
|
+
for (const height of [eaveHeight - 0.23, eaveHeight - 0.01]) {
|
|
294
|
+
fasteners.push({
|
|
295
|
+
point: postCenter
|
|
296
|
+
.clone()
|
|
297
|
+
.addScaledVector(section.frame.tangent, tangentOffset)
|
|
298
|
+
.addScaledVector(LOCAL_UP, height)
|
|
299
|
+
.addScaledVector(outward, postWidth / 2 + 0.04),
|
|
300
|
+
outward,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (let index = 0; index < sections.length - 1; index += 1) {
|
|
307
|
+
const current = sections[index]!
|
|
308
|
+
const next = sections[index + 1]!
|
|
309
|
+
eaveFascias.push(
|
|
310
|
+
beam(current.leftEave, next.leftEave, 0.36, 0.34),
|
|
311
|
+
beam(current.rightEave, next.rightEave, 0.36, 0.34),
|
|
312
|
+
)
|
|
313
|
+
ridgeCaps.push(beam(current.ridge, next.ridge, 0.3, 0.24))
|
|
314
|
+
for (const side of [-1, 1] as const) {
|
|
315
|
+
const startEave = side < 0 ? current.leftEave : current.rightEave
|
|
316
|
+
const endEave = side < 0 ? next.leftEave : next.rightEave
|
|
317
|
+
braces.push(
|
|
318
|
+
beam(
|
|
319
|
+
startEave.clone().addScaledVector(LOCAL_UP, -0.92),
|
|
320
|
+
startEave.clone().lerp(endEave, 0.28).addScaledVector(LOCAL_UP, -0.12),
|
|
321
|
+
0.145,
|
|
322
|
+
0.13,
|
|
323
|
+
),
|
|
324
|
+
beam(
|
|
325
|
+
endEave.clone().addScaledVector(LOCAL_UP, -0.92),
|
|
326
|
+
endEave.clone().lerp(startEave, 0.28).addScaledVector(LOCAL_UP, -0.12),
|
|
327
|
+
0.145,
|
|
328
|
+
0.13,
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const timberMesh = addBeamBatch(
|
|
335
|
+
root,
|
|
336
|
+
`${id}-mine-timbers`,
|
|
337
|
+
timbers,
|
|
338
|
+
materials.timber,
|
|
339
|
+
'rocky-mountain-wild-west',
|
|
340
|
+
false,
|
|
341
|
+
true,
|
|
342
|
+
)
|
|
343
|
+
const braceMesh = addBeamBatch(
|
|
344
|
+
root,
|
|
345
|
+
`${id}-mine-braces`,
|
|
346
|
+
braces,
|
|
347
|
+
materials.timber,
|
|
348
|
+
'rocky-mountain-wild-west',
|
|
349
|
+
false,
|
|
350
|
+
true,
|
|
351
|
+
)
|
|
352
|
+
addBeamBatch(
|
|
353
|
+
root,
|
|
354
|
+
`${id}-mine-longitudinal-timbers`,
|
|
355
|
+
eaveFascias,
|
|
356
|
+
materials.timber,
|
|
357
|
+
'rocky-mountain-wild-west',
|
|
358
|
+
false,
|
|
359
|
+
true,
|
|
360
|
+
)
|
|
361
|
+
addBeamBatch(
|
|
362
|
+
root,
|
|
363
|
+
`${id}-mine-ridge-cap`,
|
|
364
|
+
ridgeCaps,
|
|
365
|
+
materials.metal,
|
|
366
|
+
'rocky-mountain-wild-west',
|
|
367
|
+
false,
|
|
368
|
+
true,
|
|
369
|
+
)
|
|
370
|
+
addBeamBatch(
|
|
371
|
+
root,
|
|
372
|
+
`${id}-mine-iron-straps`,
|
|
373
|
+
ironStraps,
|
|
374
|
+
materials.metal,
|
|
375
|
+
'rocky-mountain-wild-west',
|
|
376
|
+
false,
|
|
377
|
+
true,
|
|
378
|
+
)
|
|
379
|
+
for (const mesh of [timberMesh, braceMesh]) {
|
|
380
|
+
if (!mesh) continue
|
|
381
|
+
mesh.userData.canopySectionCount = sections.length
|
|
382
|
+
mesh.userData.canopySupportSectionCount = supported.length
|
|
383
|
+
mesh.userData.canopyPortalFrameCount = portalFrameCount
|
|
384
|
+
}
|
|
385
|
+
addBeamBatch(
|
|
386
|
+
root,
|
|
387
|
+
`${id}-mine-iron-brackets`,
|
|
388
|
+
hardwarePlates,
|
|
389
|
+
materials.metal,
|
|
390
|
+
'rocky-mountain-wild-west',
|
|
391
|
+
false,
|
|
392
|
+
true,
|
|
393
|
+
)
|
|
394
|
+
addFastenerBatch(root, `${id}-mine-iron-fasteners`, fasteners, materials.metal, 'rocky-mountain-wild-west')
|
|
395
|
+
addRoofSheets(root, `${id}-corrugated-roof`, roofSections, materials.metal, materials.metal)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function addRoofSheets(
|
|
399
|
+
root: THREE.Group,
|
|
400
|
+
id: string,
|
|
401
|
+
sections: readonly CanopySection[],
|
|
402
|
+
material: THREE.Material,
|
|
403
|
+
patchMaterial: THREE.Material,
|
|
404
|
+
): void {
|
|
405
|
+
const vertices: number[] = []
|
|
406
|
+
const indices: number[] = []
|
|
407
|
+
const uvs: number[] = []
|
|
408
|
+
const edgeCaps: Beam[] = []
|
|
409
|
+
const corrugationRibs: Beam[] = []
|
|
410
|
+
const distances = [0]
|
|
411
|
+
for (let index = 0; index < sections.length - 1; index += 1) {
|
|
412
|
+
distances.push(distances.at(-1)! + sections[index]!.frame.point.distanceTo(sections[index + 1]!.frame.point))
|
|
413
|
+
}
|
|
414
|
+
const roofThickness = 0.1
|
|
415
|
+
const topLift = 0.055
|
|
416
|
+
const undersideDrop = roofThickness - topLift
|
|
417
|
+
const addFace = (
|
|
418
|
+
points: readonly [THREE.Vector3, THREE.Vector3, THREE.Vector3, THREE.Vector3],
|
|
419
|
+
startDistance: number,
|
|
420
|
+
endDistance: number,
|
|
421
|
+
reverse: boolean,
|
|
422
|
+
): void => {
|
|
423
|
+
const offset = vertices.length / 3
|
|
424
|
+
points.forEach(point => vertices.push(point.x, point.y, point.z))
|
|
425
|
+
uvs.push(0, startDistance, 1, startDistance, 0, endDistance, 1, endDistance)
|
|
426
|
+
if (reverse) indices.push(offset, offset + 1, offset + 2, offset + 1, offset + 3, offset + 2)
|
|
427
|
+
else indices.push(offset, offset + 2, offset + 1, offset + 1, offset + 2, offset + 3)
|
|
428
|
+
}
|
|
429
|
+
for (let index = 0; index < sections.length - 1; index += 1) {
|
|
430
|
+
const current = sections[index]!
|
|
431
|
+
const next = sections[index + 1]!
|
|
432
|
+
for (const [startA, startB, endA, endB] of [
|
|
433
|
+
[current.leftEave, current.ridge, next.leftEave, next.ridge],
|
|
434
|
+
[current.ridge, current.rightEave, next.ridge, next.rightEave],
|
|
435
|
+
] as const) {
|
|
436
|
+
const topPoints = [startA, startB, endA, endB].map(point =>
|
|
437
|
+
point.clone().addScaledVector(LOCAL_UP, topLift),
|
|
438
|
+
) as unknown as readonly [THREE.Vector3, THREE.Vector3, THREE.Vector3, THREE.Vector3]
|
|
439
|
+
const undersidePoints = [startA, startB, endA, endB].map(point =>
|
|
440
|
+
point.clone().addScaledVector(LOCAL_UP, -undersideDrop),
|
|
441
|
+
) as unknown as readonly [THREE.Vector3, THREE.Vector3, THREE.Vector3, THREE.Vector3]
|
|
442
|
+
addFace(topPoints, distances[index]!, distances[index + 1]!, false)
|
|
443
|
+
addFace(undersidePoints, distances[index]!, distances[index + 1]!, true)
|
|
444
|
+
}
|
|
445
|
+
const leftTopStart = current.leftEave.clone().addScaledVector(LOCAL_UP, topLift)
|
|
446
|
+
const leftTopEnd = next.leftEave.clone().addScaledVector(LOCAL_UP, topLift)
|
|
447
|
+
const leftBottomStart = current.leftEave.clone().addScaledVector(LOCAL_UP, -undersideDrop)
|
|
448
|
+
const leftBottomEnd = next.leftEave.clone().addScaledVector(LOCAL_UP, -undersideDrop)
|
|
449
|
+
const rightTopStart = current.rightEave.clone().addScaledVector(LOCAL_UP, topLift)
|
|
450
|
+
const rightTopEnd = next.rightEave.clone().addScaledVector(LOCAL_UP, topLift)
|
|
451
|
+
const rightBottomStart = current.rightEave.clone().addScaledVector(LOCAL_UP, -undersideDrop)
|
|
452
|
+
const rightBottomEnd = next.rightEave.clone().addScaledVector(LOCAL_UP, -undersideDrop)
|
|
453
|
+
addFace(
|
|
454
|
+
[leftBottomStart, leftTopStart, leftBottomEnd, leftTopEnd],
|
|
455
|
+
distances[index]!,
|
|
456
|
+
distances[index + 1]!,
|
|
457
|
+
false,
|
|
458
|
+
)
|
|
459
|
+
addFace(
|
|
460
|
+
[rightTopStart, rightBottomStart, rightTopEnd, rightBottomEnd],
|
|
461
|
+
distances[index]!,
|
|
462
|
+
distances[index + 1]!,
|
|
463
|
+
false,
|
|
464
|
+
)
|
|
465
|
+
edgeCaps.push(
|
|
466
|
+
beam(leftTopStart, leftTopEnd, 0.055, 0.06),
|
|
467
|
+
beam(
|
|
468
|
+
current.ridge.clone().addScaledVector(LOCAL_UP, topLift + 0.02),
|
|
469
|
+
next.ridge.clone().addScaledVector(LOCAL_UP, topLift + 0.02),
|
|
470
|
+
0.085,
|
|
471
|
+
0.075,
|
|
472
|
+
),
|
|
473
|
+
beam(rightTopStart, rightTopEnd, 0.055, 0.06),
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
for (const section of sections) {
|
|
477
|
+
corrugationRibs.push(
|
|
478
|
+
beam(
|
|
479
|
+
section.leftEave.clone().addScaledVector(LOCAL_UP, topLift + 0.015),
|
|
480
|
+
section.ridge.clone().addScaledVector(LOCAL_UP, topLift + 0.015),
|
|
481
|
+
0.038,
|
|
482
|
+
0.045,
|
|
483
|
+
),
|
|
484
|
+
beam(
|
|
485
|
+
section.ridge.clone().addScaledVector(LOCAL_UP, topLift + 0.015),
|
|
486
|
+
section.rightEave.clone().addScaledVector(LOCAL_UP, topLift + 0.015),
|
|
487
|
+
0.038,
|
|
488
|
+
0.045,
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
const geometry = new THREE.BufferGeometry()
|
|
493
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
|
|
494
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
495
|
+
geometry.setIndex(indices)
|
|
496
|
+
geometry.computeVertexNormals()
|
|
497
|
+
geometry.userData.canopyLayout = 'layered-continuous-gabled-ribbon'
|
|
498
|
+
geometry.userData.canopySectionCount = sections.length
|
|
499
|
+
geometry.userData.roofThickness = roofThickness
|
|
500
|
+
const roof = new THREE.Mesh(geometry, material)
|
|
501
|
+
roof.name = id
|
|
502
|
+
decorate(roof, 'rocky-mountain-wild-west', 'covered-mine-roof')
|
|
503
|
+
root.add(roof)
|
|
504
|
+
addBeamBatch(
|
|
505
|
+
root,
|
|
506
|
+
`${id}-edge-caps`,
|
|
507
|
+
edgeCaps,
|
|
508
|
+
material,
|
|
509
|
+
'rocky-mountain-wild-west',
|
|
510
|
+
false,
|
|
511
|
+
true,
|
|
512
|
+
)
|
|
513
|
+
addBeamBatch(
|
|
514
|
+
root,
|
|
515
|
+
`${id}-raised-seams`,
|
|
516
|
+
corrugationRibs,
|
|
517
|
+
material,
|
|
518
|
+
'rocky-mountain-wild-west',
|
|
519
|
+
false,
|
|
520
|
+
true,
|
|
521
|
+
)
|
|
522
|
+
addRoofPatchPanels(root, `${id}-weathered-patch-panels`, sections, patchMaterial, topLift + 0.025)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function addRoofPatchPanels(
|
|
526
|
+
root: THREE.Group,
|
|
527
|
+
name: string,
|
|
528
|
+
sections: readonly CanopySection[],
|
|
529
|
+
material: THREE.Material,
|
|
530
|
+
lift: number,
|
|
531
|
+
): void {
|
|
532
|
+
const vertices: number[] = []
|
|
533
|
+
const indices: number[] = []
|
|
534
|
+
const uvs: number[] = []
|
|
535
|
+
const thickness = 0.035
|
|
536
|
+
let panelCount = 0
|
|
537
|
+
|
|
538
|
+
const appendPanel = (corners: readonly [THREE.Vector3, THREE.Vector3, THREE.Vector3, THREE.Vector3]): void => {
|
|
539
|
+
const top = corners.map(point => point.clone().addScaledVector(LOCAL_UP, lift)) as unknown as readonly [
|
|
540
|
+
THREE.Vector3,
|
|
541
|
+
THREE.Vector3,
|
|
542
|
+
THREE.Vector3,
|
|
543
|
+
THREE.Vector3,
|
|
544
|
+
]
|
|
545
|
+
const bottom = top.map(point => point.clone().addScaledVector(LOCAL_UP, -thickness)) as unknown as readonly [
|
|
546
|
+
THREE.Vector3,
|
|
547
|
+
THREE.Vector3,
|
|
548
|
+
THREE.Vector3,
|
|
549
|
+
THREE.Vector3,
|
|
550
|
+
]
|
|
551
|
+
const faces = [
|
|
552
|
+
[top[0], top[1], top[2], top[3]],
|
|
553
|
+
[bottom[1], bottom[0], bottom[3], bottom[2]],
|
|
554
|
+
[bottom[0], top[0], bottom[2], top[2]],
|
|
555
|
+
[top[1], bottom[1], top[3], bottom[3]],
|
|
556
|
+
[bottom[0], bottom[1], top[0], top[1]],
|
|
557
|
+
[top[2], top[3], bottom[2], bottom[3]],
|
|
558
|
+
] as const
|
|
559
|
+
for (const face of faces) {
|
|
560
|
+
const offset = vertices.length / 3
|
|
561
|
+
face.forEach(point => vertices.push(point.x, point.y, point.z))
|
|
562
|
+
uvs.push(0, 0, 1, 0, 0, 1, 1, 1)
|
|
563
|
+
indices.push(offset, offset + 2, offset + 1, offset + 1, offset + 2, offset + 3)
|
|
564
|
+
}
|
|
565
|
+
panelCount += 1
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const segmentCount = sections.length - 1
|
|
569
|
+
const selectedSegments = Array.from(
|
|
570
|
+
new Set(
|
|
571
|
+
segmentCount >= 8
|
|
572
|
+
? [Math.floor(segmentCount * 0.28), Math.floor(segmentCount * 0.72)]
|
|
573
|
+
: [Math.floor(segmentCount * 0.5)],
|
|
574
|
+
),
|
|
575
|
+
)
|
|
576
|
+
for (const index of selectedSegments) {
|
|
577
|
+
const current = sections[index]!
|
|
578
|
+
const next = sections[index + 1]!
|
|
579
|
+
const seed = deterministic(`${name}:${index}`)
|
|
580
|
+
const side = (index + (seed > 0.5 ? 1 : 0)) % 2 === 0 ? 'left' : 'right'
|
|
581
|
+
const startProgress = 0.08 + seed * 0.12
|
|
582
|
+
const endProgress = 0.72 + deterministic(`${name}:${index}:end`) * 0.18
|
|
583
|
+
const startOuter = side === 'left' ? current.leftEave : current.ridge
|
|
584
|
+
const startInner = side === 'left' ? current.ridge : current.rightEave
|
|
585
|
+
const endOuter = side === 'left' ? next.leftEave : next.ridge
|
|
586
|
+
const endInner = side === 'left' ? next.ridge : next.rightEave
|
|
587
|
+
appendPanel([
|
|
588
|
+
startOuter.clone().lerp(startInner, startProgress),
|
|
589
|
+
startOuter.clone().lerp(startInner, endProgress),
|
|
590
|
+
endOuter.clone().lerp(endInner, startProgress),
|
|
591
|
+
endOuter.clone().lerp(endInner, endProgress),
|
|
592
|
+
])
|
|
593
|
+
}
|
|
594
|
+
if (panelCount === 0) return
|
|
595
|
+
const geometry = new THREE.BufferGeometry()
|
|
596
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
|
|
597
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
598
|
+
geometry.setIndex(indices)
|
|
599
|
+
geometry.computeVertexNormals()
|
|
600
|
+
geometry.userData.canopyPatchPanelCount = panelCount
|
|
601
|
+
geometry.userData.canopyPatchThickness = thickness
|
|
602
|
+
const patches = new THREE.Mesh(geometry, material)
|
|
603
|
+
patches.name = name
|
|
604
|
+
decorate(patches, 'rocky-mountain-wild-west', 'covered-mine-roof-patches')
|
|
605
|
+
root.add(patches)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function addSakuraRoofedGate(
|
|
609
|
+
root: THREE.Group,
|
|
610
|
+
id: string,
|
|
611
|
+
runFrames: readonly AppearanceCanopyFrame[],
|
|
612
|
+
materials: AppearanceCanopyMaterials,
|
|
613
|
+
supportOptions: SupportOptions,
|
|
614
|
+
): void {
|
|
615
|
+
const themeId = 'japanese-sakura-lake'
|
|
616
|
+
const vermilion = materials.wall
|
|
617
|
+
const kawara = materials.sakuraKawara ?? materials.metal
|
|
618
|
+
const masonry = materials.sakuraMasonry ?? materials.metal
|
|
619
|
+
const supportFrames = addOpenPortSupportInsets(sampleFrames(runFrames, 1.86), supportOptions)
|
|
620
|
+
const supported = supportSections(supportFrames, supportOptions)
|
|
621
|
+
const roofFrames = sampleFrames(runFrames, 0.34)
|
|
622
|
+
const supports: Beam[] = []
|
|
623
|
+
const lowerRails: Beam[] = []
|
|
624
|
+
const feet: Beam[] = []
|
|
625
|
+
const bolts: THREE.Vector3[] = []
|
|
626
|
+
|
|
627
|
+
for (const { section: frame } of supported) {
|
|
628
|
+
const halfSpan = frame.width / 2 + 0.18
|
|
629
|
+
const leftBase = frame.point.clone().addScaledVector(frame.right, -halfSpan).addScaledVector(LOCAL_UP, -0.08)
|
|
630
|
+
const rightBase = frame.point.clone().addScaledVector(frame.right, halfSpan).addScaledVector(LOCAL_UP, -0.08)
|
|
631
|
+
const leftFootBase = frame.point.clone().addScaledVector(frame.right, -halfSpan).addScaledVector(frame.up, -0.08)
|
|
632
|
+
const rightFootBase = frame.point.clone().addScaledVector(frame.right, halfSpan).addScaledVector(frame.up, -0.08)
|
|
633
|
+
const leftTop = frame.point.clone().addScaledVector(frame.right, -halfSpan).addScaledVector(LOCAL_UP, 2.58)
|
|
634
|
+
const rightTop = frame.point.clone().addScaledVector(frame.right, halfSpan).addScaledVector(LOCAL_UP, 2.58)
|
|
635
|
+
supports.push(
|
|
636
|
+
beam(leftBase, leftTop, 0.34, 0.38),
|
|
637
|
+
beam(rightBase, rightTop, 0.34, 0.38),
|
|
638
|
+
beam(
|
|
639
|
+
frame.point.clone().addScaledVector(frame.right, -(halfSpan + 0.34)).addScaledVector(LOCAL_UP, 2.58),
|
|
640
|
+
frame.point.clone().addScaledVector(frame.right, halfSpan + 0.34).addScaledVector(LOCAL_UP, 2.58),
|
|
641
|
+
0.28,
|
|
642
|
+
0.42,
|
|
643
|
+
),
|
|
644
|
+
beam(
|
|
645
|
+
frame.point.clone().addScaledVector(frame.right, -halfSpan).addScaledVector(LOCAL_UP, 2.32),
|
|
646
|
+
frame.point.clone().addScaledVector(frame.right, halfSpan).addScaledVector(LOCAL_UP, 2.32),
|
|
647
|
+
0.18,
|
|
648
|
+
0.34,
|
|
649
|
+
),
|
|
650
|
+
)
|
|
651
|
+
feet.push(
|
|
652
|
+
beam(leftFootBase, leftFootBase.clone().addScaledVector(frame.up, 0.22), 0.64, 0.62),
|
|
653
|
+
beam(rightFootBase, rightFootBase.clone().addScaledVector(frame.up, 0.22), 0.64, 0.62),
|
|
654
|
+
)
|
|
655
|
+
bolts.push(
|
|
656
|
+
leftBase.clone().addScaledVector(LOCAL_UP, 0.76).addScaledVector(frame.tangent, 0.22),
|
|
657
|
+
leftTop.clone().addScaledVector(LOCAL_UP, -0.42).addScaledVector(frame.tangent, 0.22),
|
|
658
|
+
rightBase.clone().addScaledVector(LOCAL_UP, 0.76).addScaledVector(frame.tangent, 0.22),
|
|
659
|
+
rightTop.clone().addScaledVector(LOCAL_UP, -0.42).addScaledVector(frame.tangent, 0.22),
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (let index = 0; index < supportFrames.length - 1; index += 1) {
|
|
664
|
+
const current = supportFrames[index]!
|
|
665
|
+
const next = supportFrames[index + 1]!
|
|
666
|
+
for (const side of [-1, 1] as const) {
|
|
667
|
+
const currentHalfSpan = current.width / 2 + 0.18
|
|
668
|
+
const nextHalfSpan = next.width / 2 + 0.18
|
|
669
|
+
lowerRails.push(
|
|
670
|
+
beam(
|
|
671
|
+
current.point
|
|
672
|
+
.clone()
|
|
673
|
+
.addScaledVector(current.right, side * currentHalfSpan)
|
|
674
|
+
.addScaledVector(LOCAL_UP, 0.56),
|
|
675
|
+
next.point
|
|
676
|
+
.clone()
|
|
677
|
+
.addScaledVector(next.right, side * nextHalfSpan)
|
|
678
|
+
.addScaledVector(LOCAL_UP, 0.56),
|
|
679
|
+
0.18,
|
|
680
|
+
0.18,
|
|
681
|
+
),
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const supportMesh = addBeamBatch(root, `${id}-sakura-vermilion-frames`, supports, vermilion, themeId, false)
|
|
687
|
+
const lowerRailMesh = addBeamBatch(
|
|
688
|
+
root,
|
|
689
|
+
`${id}-sakura-vermilion-lower-rails`,
|
|
690
|
+
lowerRails,
|
|
691
|
+
vermilion,
|
|
692
|
+
themeId,
|
|
693
|
+
false,
|
|
694
|
+
true,
|
|
695
|
+
)
|
|
696
|
+
const footMesh = addBeamBatch(root, `${id}-sakura-masonry-feet`, feet, masonry, themeId, false)
|
|
697
|
+
addJointBatch(root, `${id}-sakura-gunmetal-bolts`, bolts, materials.metal, themeId, 'sakura-gate-hardware', 0.065)
|
|
698
|
+
if (supportMesh) {
|
|
699
|
+
supportMesh.userData.themeRole = 'sakura-roofed-gate-frame'
|
|
700
|
+
supportMesh.userData.canopySectionCount = roofFrames.length
|
|
701
|
+
supportMesh.userData.canopySupportSectionCount = supported.length
|
|
702
|
+
}
|
|
703
|
+
if (lowerRailMesh) {
|
|
704
|
+
lowerRailMesh.userData.themeRole = 'sakura-roofed-gate-lower-rail'
|
|
705
|
+
lowerRailMesh.userData.canopyLowerRailSegmentCount = lowerRails.length
|
|
706
|
+
}
|
|
707
|
+
if (footMesh) footMesh.userData.themeRole = 'sakura-roofed-gate-surface-aligned-foot'
|
|
708
|
+
|
|
709
|
+
const roofSections = roofFrames.map(frame => {
|
|
710
|
+
const halfSpan = frame.width / 2 + 0.82
|
|
711
|
+
return {
|
|
712
|
+
frame,
|
|
713
|
+
leftEave: frame.point.clone().addScaledVector(frame.right, -halfSpan).addScaledVector(LOCAL_UP, 2.6),
|
|
714
|
+
ridge: frame.point.clone().addScaledVector(LOCAL_UP, 3.5),
|
|
715
|
+
rightEave: frame.point.clone().addScaledVector(frame.right, halfSpan).addScaledVector(LOCAL_UP, 2.6),
|
|
716
|
+
}
|
|
717
|
+
})
|
|
718
|
+
addSakuraRoofBacking(root, `${id}-sakura-kawara-backing`, roofSections, kawara, themeId)
|
|
719
|
+
addSakuraRoofTiles(root, `${id}-sakura-kawara-tiles`, roofSections, kawara, themeId)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function addSakuraRoofBacking(
|
|
723
|
+
root: THREE.Group,
|
|
724
|
+
name: string,
|
|
725
|
+
sections: readonly CanopySection[],
|
|
726
|
+
material: THREE.Material,
|
|
727
|
+
themeId: string,
|
|
728
|
+
): void {
|
|
729
|
+
const positions: number[] = []
|
|
730
|
+
const uvs: number[] = []
|
|
731
|
+
const indices: number[] = []
|
|
732
|
+
const distances = [0]
|
|
733
|
+
for (let index = 0; index < sections.length - 1; index += 1) {
|
|
734
|
+
distances.push(distances.at(-1)! + sections[index]!.frame.point.distanceTo(sections[index + 1]!.frame.point))
|
|
735
|
+
}
|
|
736
|
+
const append = (points: readonly THREE.Vector3[], startDistance: number, endDistance: number, reverse: boolean) => {
|
|
737
|
+
const offset = positions.length / 3
|
|
738
|
+
points.forEach(point => positions.push(point.x, point.y, point.z))
|
|
739
|
+
uvs.push(0, startDistance, 1, startDistance, 0, endDistance, 1, endDistance)
|
|
740
|
+
indices.push(
|
|
741
|
+
...(reverse
|
|
742
|
+
? [offset, offset + 1, offset + 2, offset + 1, offset + 3, offset + 2]
|
|
743
|
+
: [offset, offset + 2, offset + 1, offset + 1, offset + 2, offset + 3]),
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
for (let index = 0; index < sections.length - 1; index += 1) {
|
|
747
|
+
const current = sections[index]!
|
|
748
|
+
const next = sections[index + 1]!
|
|
749
|
+
for (const [a, b, c, d] of [
|
|
750
|
+
[current.leftEave, current.ridge, next.leftEave, next.ridge],
|
|
751
|
+
[current.ridge, current.rightEave, next.ridge, next.rightEave],
|
|
752
|
+
] as const) {
|
|
753
|
+
append(
|
|
754
|
+
[a, b, c, d].map(point => point.clone().addScaledVector(LOCAL_UP, 0.02)),
|
|
755
|
+
distances[index]!,
|
|
756
|
+
distances[index + 1]!,
|
|
757
|
+
false,
|
|
758
|
+
)
|
|
759
|
+
append(
|
|
760
|
+
[a, b, c, d].map(point => point.clone().addScaledVector(LOCAL_UP, -0.09)),
|
|
761
|
+
distances[index]!,
|
|
762
|
+
distances[index + 1]!,
|
|
763
|
+
true,
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const geometry = new THREE.BufferGeometry()
|
|
768
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
769
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
770
|
+
geometry.setIndex(indices)
|
|
771
|
+
geometry.computeVertexNormals()
|
|
772
|
+
geometry.userData.canopyLayout = 'continuous-sakura-kawara-backing'
|
|
773
|
+
geometry.userData.canopySectionCount = sections.length
|
|
774
|
+
const roof = new THREE.Mesh(geometry, material)
|
|
775
|
+
roof.name = name
|
|
776
|
+
decorate(roof, themeId, 'sakura-kawara-roof-backing')
|
|
777
|
+
root.add(roof)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function addSakuraRoofTiles(
|
|
781
|
+
root: THREE.Group,
|
|
782
|
+
name: string,
|
|
783
|
+
sections: readonly CanopySection[],
|
|
784
|
+
material: THREE.Material,
|
|
785
|
+
themeId: string,
|
|
786
|
+
): void {
|
|
787
|
+
// The roof backing supplies the solid surface; tiles only need a curved outer shell.
|
|
788
|
+
const tileGeometry = new VisualCylinderGeometry(0.5, 0.5, 1, 4, 1, true, 0, Math.PI)
|
|
789
|
+
const tilesPerSection = 8
|
|
790
|
+
const tiles = new THREE.InstancedMesh(tileGeometry, material, sections.length * tilesPerSection)
|
|
791
|
+
tiles.name = name
|
|
792
|
+
let tileIndex = 0
|
|
793
|
+
for (const section of sections) {
|
|
794
|
+
for (const side of [-1, 1] as const) {
|
|
795
|
+
const eave = side < 0 ? section.leftEave : section.rightEave
|
|
796
|
+
for (let segment = 0; segment < 4; segment += 1) {
|
|
797
|
+
const startProgress = Math.max(0, segment / 4 - 0.018)
|
|
798
|
+
const endProgress = Math.min(1, (segment + 1) / 4 + 0.035)
|
|
799
|
+
const start = section.ridge.clone().lerp(eave, startProgress).addScaledVector(LOCAL_UP, 0.09)
|
|
800
|
+
const end = section.ridge.clone().lerp(eave, endProgress).addScaledVector(LOCAL_UP, 0.09)
|
|
801
|
+
const direction = end.clone().sub(start)
|
|
802
|
+
const length = direction.length()
|
|
803
|
+
const align = new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, direction.normalize())
|
|
804
|
+
align.multiply(new THREE.Quaternion().setFromAxisAngle(LOCAL_UP, side < 0 ? Math.PI : 0))
|
|
805
|
+
tiles.setMatrixAt(
|
|
806
|
+
tileIndex++,
|
|
807
|
+
new THREE.Matrix4().compose(
|
|
808
|
+
start.clone().lerp(end, 0.5),
|
|
809
|
+
align,
|
|
810
|
+
new THREE.Vector3(0.21, length, 0.21),
|
|
811
|
+
),
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
tiles.instanceMatrix.needsUpdate = true
|
|
817
|
+
tiles.userData.canopySectionCount = sections.length
|
|
818
|
+
tiles.userData.kawaraTileCount = tiles.count
|
|
819
|
+
tiles.userData.mirroredRoofSides = true
|
|
820
|
+
decorate(tiles, themeId, 'sakura-kawara-tile-shell')
|
|
821
|
+
root.add(tiles)
|
|
822
|
+
|
|
823
|
+
const eaveGeometry = new VisualCylinderGeometry(0.5, 0.5, 1, 12)
|
|
824
|
+
const eaveCaps = new THREE.InstancedMesh(eaveGeometry, material, sections.length * 2)
|
|
825
|
+
eaveCaps.name = `${name}-scalloped-eaves`
|
|
826
|
+
let capIndex = 0
|
|
827
|
+
for (const section of sections) {
|
|
828
|
+
for (const eave of [section.leftEave, section.rightEave]) {
|
|
829
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, section.frame.tangent)
|
|
830
|
+
eaveCaps.setMatrixAt(
|
|
831
|
+
capIndex++,
|
|
832
|
+
new THREE.Matrix4().compose(
|
|
833
|
+
eave.clone().addScaledVector(LOCAL_UP, 0.075),
|
|
834
|
+
quaternion,
|
|
835
|
+
new THREE.Vector3(0.27, 0.12, 0.27),
|
|
836
|
+
),
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
eaveCaps.instanceMatrix.needsUpdate = true
|
|
841
|
+
decorate(eaveCaps, themeId, 'sakura-kawara-eave-cap')
|
|
842
|
+
root.add(eaveCaps)
|
|
843
|
+
|
|
844
|
+
const ridge: Beam[] = []
|
|
845
|
+
for (let index = 0; index < sections.length - 1; index += 1) {
|
|
846
|
+
ridge.push(beam(sections[index]!.ridge, sections[index + 1]!.ridge, 0.3, 0.3))
|
|
847
|
+
}
|
|
848
|
+
const ridgeMesh = addBeamBatch(root, `${name}-ridge`, ridge, material, themeId, true)
|
|
849
|
+
if (ridgeMesh) ridgeMesh.userData.themeRole = 'sakura-kawara-ridge'
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function beam(start: THREE.Vector3, end: THREE.Vector3, width: number, depth: number): Beam {
|
|
853
|
+
return { start, end, width, depth }
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function addBeamBatch(
|
|
857
|
+
root: THREE.Group,
|
|
858
|
+
name: string,
|
|
859
|
+
beams: readonly Beam[],
|
|
860
|
+
material: THREE.Material,
|
|
861
|
+
themeId: string,
|
|
862
|
+
rounded: boolean,
|
|
863
|
+
beveled = false,
|
|
864
|
+
): THREE.InstancedMesh | undefined {
|
|
865
|
+
if (beams.length === 0) return undefined
|
|
866
|
+
const geometry = rounded
|
|
867
|
+
? new VisualCylinderGeometry(0.5, 0.62, 1, 7)
|
|
868
|
+
: beveled
|
|
869
|
+
? new VisualBoxGeometry(1, 1, 1, 2, 0.08)
|
|
870
|
+
: new THREE.BoxGeometry(1, 1, 1)
|
|
871
|
+
const mesh = new THREE.InstancedMesh(geometry, material, beams.length)
|
|
872
|
+
mesh.name = name
|
|
873
|
+
beams.forEach((item, index) => {
|
|
874
|
+
const direction = item.end.clone().sub(item.start)
|
|
875
|
+
const length = direction.length()
|
|
876
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, direction.normalize())
|
|
877
|
+
mesh.setMatrixAt(
|
|
878
|
+
index,
|
|
879
|
+
new THREE.Matrix4().compose(
|
|
880
|
+
item.start.clone().lerp(item.end, 0.5),
|
|
881
|
+
quaternion,
|
|
882
|
+
new THREE.Vector3(item.width, length, item.depth),
|
|
883
|
+
),
|
|
884
|
+
)
|
|
885
|
+
})
|
|
886
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
887
|
+
decorate(mesh, themeId, rounded ? 'sakura-kawara-ridge' : 'covered-mine-timber')
|
|
888
|
+
root.add(mesh)
|
|
889
|
+
return mesh
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function addFastenerBatch(
|
|
893
|
+
root: THREE.Group,
|
|
894
|
+
name: string,
|
|
895
|
+
fasteners: readonly Fastener[],
|
|
896
|
+
material: THREE.Material,
|
|
897
|
+
themeId: string,
|
|
898
|
+
): THREE.InstancedMesh | undefined {
|
|
899
|
+
if (fasteners.length === 0) return undefined
|
|
900
|
+
const geometry = new VisualCylinderGeometry(0.045, 0.045, 0.045, 12)
|
|
901
|
+
const mesh = new THREE.InstancedMesh(geometry, material, fasteners.length)
|
|
902
|
+
mesh.name = name
|
|
903
|
+
fasteners.forEach(({ point, outward }, index) => {
|
|
904
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, outward.clone().normalize())
|
|
905
|
+
mesh.setMatrixAt(index, new THREE.Matrix4().compose(point, quaternion, new THREE.Vector3(1, 1, 1)))
|
|
906
|
+
})
|
|
907
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
908
|
+
decorate(mesh, themeId, 'covered-mine-fasteners')
|
|
909
|
+
root.add(mesh)
|
|
910
|
+
return mesh
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function addJointBatch(
|
|
914
|
+
root: THREE.Group,
|
|
915
|
+
name: string,
|
|
916
|
+
points: readonly THREE.Vector3[],
|
|
917
|
+
material: THREE.Material,
|
|
918
|
+
themeId: string,
|
|
919
|
+
role: string,
|
|
920
|
+
radius: number,
|
|
921
|
+
): void {
|
|
922
|
+
if (points.length === 0) return
|
|
923
|
+
const mesh = new THREE.InstancedMesh(new THREE.DodecahedronGeometry(radius, 0), material, points.length)
|
|
924
|
+
mesh.name = name
|
|
925
|
+
points.forEach((point, index) => mesh.setMatrixAt(index, new THREE.Matrix4().makeTranslation(...point.toArray())))
|
|
926
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
927
|
+
decorate(mesh, themeId, role)
|
|
928
|
+
root.add(mesh)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function decorate(object: THREE.Object3D, themeId: string, role: string): void {
|
|
932
|
+
object.userData.themeOnly = themeId
|
|
933
|
+
object.userData.themeRole = role
|
|
934
|
+
object.userData.decorative = true
|
|
935
|
+
object.castShadow = true
|
|
936
|
+
object.receiveShadow = true
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function deterministic(seed: string): number {
|
|
940
|
+
let hash = 2_166_136_261
|
|
941
|
+
for (const character of seed) {
|
|
942
|
+
hash ^= character.charCodeAt(0)
|
|
943
|
+
hash = Math.imul(hash, 16_777_619)
|
|
944
|
+
}
|
|
945
|
+
return (hash >>> 0) / 4_294_967_296
|
|
946
|
+
}
|