@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,1420 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initialTrackCatalog,
|
|
3
|
+
laneGradeDrop,
|
|
4
|
+
portalPairNumber,
|
|
5
|
+
resolveTemplateConnectors,
|
|
6
|
+
TRACK_MODULE_LENGTH,
|
|
7
|
+
type JsonObject,
|
|
8
|
+
type TrackConnectorTemplate,
|
|
9
|
+
type TrackPieceInstance,
|
|
10
|
+
type TrackPieceTemplate,
|
|
11
|
+
type TrackVector,
|
|
12
|
+
} from '@marble.fun/physics-engine'
|
|
13
|
+
import * as THREE from 'three'
|
|
14
|
+
|
|
15
|
+
import { VisualSphereGeometry, VisualCylinderGeometry } from '../../../three/visual-geometry'
|
|
16
|
+
|
|
17
|
+
import type { TrackMaterials } from '../../play/race/track-mechanisms'
|
|
18
|
+
import { portalPairColor } from '../../play/race/portal-pair-color'
|
|
19
|
+
import {
|
|
20
|
+
createPortalEndpointVisual,
|
|
21
|
+
PORTAL_RENDER_TARGET_TEXTURE,
|
|
22
|
+
portalVisualStyleFromId,
|
|
23
|
+
type PortalEndpointVisual,
|
|
24
|
+
} from '../../play/race/portal-visual'
|
|
25
|
+
import { trackSurfaceBasis } from '../../play/race/track-surface-basis'
|
|
26
|
+
import {
|
|
27
|
+
defaultTrackTroughDepth,
|
|
28
|
+
type RailPieceDefinition,
|
|
29
|
+
type VortexPieceDefinition,
|
|
30
|
+
} from '../../play/race/track-definition'
|
|
31
|
+
import { addFinishGate, addRebounds, addRailPiece, addVortex } from '../../play/race/track-world'
|
|
32
|
+
import type { ThemeMaterialRegistry } from '../../../three/theme-material-registry'
|
|
33
|
+
import type { ThemeMaterialSlot } from '../../../three/track-theme'
|
|
34
|
+
import {
|
|
35
|
+
ANCIENT_EGYPT_PORTAL_AGED_GOLD_ID,
|
|
36
|
+
ANCIENT_EGYPT_PORTAL_CARVED_SANDSTONE_ID,
|
|
37
|
+
ANCIENT_EGYPT_PORTAL_SANDSTONE_ID,
|
|
38
|
+
ANCIENT_EGYPT_PYRAMID_LIMESTONE_ID,
|
|
39
|
+
WILD_WEST_COVERED_MINE_IRON_ID,
|
|
40
|
+
WILD_WEST_COVERED_MINE_TIMBER_ID,
|
|
41
|
+
} from '../../../three/theme-catalog'
|
|
42
|
+
import {
|
|
43
|
+
resolvedFloorMaterialIdForTheme,
|
|
44
|
+
resolvedRailMaterialId,
|
|
45
|
+
WILD_WEST_RAIL_MATERIAL_ID,
|
|
46
|
+
WILD_WEST_RAIL_TIE_MATERIAL_ID,
|
|
47
|
+
} from '../../../three/railroad-appearance'
|
|
48
|
+
import { WILD_WEST_TRESTLE_METAL_MATERIAL_ID } from '../../play/race/track-appearance-support'
|
|
49
|
+
import { resolveFloorSurfaceTreatment, resolveFloorSurfaceTreatments } from '../../play/race/track-floor-material-rules'
|
|
50
|
+
import {
|
|
51
|
+
SHARED_ROPE_WALL_BACKING_MATERIAL_ID,
|
|
52
|
+
SHARED_ROPE_WALL_END_GRAIN_MATERIAL_ID,
|
|
53
|
+
SHARED_ROPE_WALL_TIMBER_MATERIAL_ID,
|
|
54
|
+
} from '../../../three/rope-wall-appearance'
|
|
55
|
+
import {
|
|
56
|
+
JUNGLE_TEMPLE_BRONZE_MATERIAL_ID,
|
|
57
|
+
JUNGLE_TEMPLE_FACADE_MATERIAL_ID,
|
|
58
|
+
JUNGLE_TEMPLE_PLATFORM_MATERIAL_ID,
|
|
59
|
+
JUNGLE_TEMPLE_STONE_MATERIAL_ID,
|
|
60
|
+
JUNGLE_TEMPLE_TIMBER_MATERIAL_ID,
|
|
61
|
+
} from '../../../three/jungle-temple-appearance'
|
|
62
|
+
import {
|
|
63
|
+
JAPANESE_SAKURA_BRIDGE_HARDWARE_ID,
|
|
64
|
+
JAPANESE_SAKURA_BRIDGE_TIMBER_ID,
|
|
65
|
+
JAPANESE_SAKURA_CASTLE_KAWARA_ID,
|
|
66
|
+
JAPANESE_SAKURA_CASTLE_MASONRY_ID,
|
|
67
|
+
JAPANESE_SAKURA_PORTAL_FOUNDATION_STONE_ID,
|
|
68
|
+
JAPANESE_SAKURA_CASTLE_TIMBER_ID,
|
|
69
|
+
} from '../../../three/japanese-sakura-appearance'
|
|
70
|
+
import {
|
|
71
|
+
SPACE_STATION_ARMORED_WALL_ID,
|
|
72
|
+
SPACE_STATION_CENTER_GUIDE_ID,
|
|
73
|
+
SPACE_STATION_WALL_LIGHT_ID,
|
|
74
|
+
SPACE_STATION_WALL_PANEL_ID,
|
|
75
|
+
SPACE_STATION_WALL_RIB_ID,
|
|
76
|
+
SPACE_STATION_WALL_TRIM_ID,
|
|
77
|
+
} from '../../../three/space-station-appearance'
|
|
78
|
+
import {
|
|
79
|
+
CONE_FIELD_PEG_HEIGHT,
|
|
80
|
+
CONE_FIELD_PEG_RADIUS,
|
|
81
|
+
createConeFieldDeckProfile,
|
|
82
|
+
createConeFieldLayout,
|
|
83
|
+
} from '../cone-field-layout'
|
|
84
|
+
import {
|
|
85
|
+
TRACK_LAB_CONVEYOR_BOTTOM,
|
|
86
|
+
TRACK_LAB_CONVEYOR_INPUT,
|
|
87
|
+
TRACK_LAB_CONVEYOR_LOADING,
|
|
88
|
+
TRACK_LAB_CONVEYOR_RELEASE,
|
|
89
|
+
TRACK_LAB_CONVEYOR_SHELF_COUNT,
|
|
90
|
+
TRACK_LAB_CONVEYOR_SHELF_DEPTH,
|
|
91
|
+
TRACK_LAB_CONVEYOR_SHELF_TILT_RADIANS,
|
|
92
|
+
TRACK_LAB_CONVEYOR_TOP,
|
|
93
|
+
TRACK_LAB_CONVEYOR_WIDTH,
|
|
94
|
+
} from '../conveyor-layout'
|
|
95
|
+
import { createFlipperBankDeck, createFlipperBankLayout } from '../flipper-bank-layout'
|
|
96
|
+
import {
|
|
97
|
+
REBOUND_HEIGHT,
|
|
98
|
+
REBOUND_HOLD_SECONDS,
|
|
99
|
+
REBOUND_LENGTH,
|
|
100
|
+
REBOUND_COOLDOWN_SECONDS,
|
|
101
|
+
REBOUND_RESTITUTION,
|
|
102
|
+
REBOUND_RETURN_SECONDS,
|
|
103
|
+
REBOUND_STRIKE_DISTANCE,
|
|
104
|
+
REBOUND_STRIKE_SECONDS,
|
|
105
|
+
REBOUND_WIDTH,
|
|
106
|
+
createReboundDeck,
|
|
107
|
+
createReboundLayout,
|
|
108
|
+
createSingleFileSqueezeRail,
|
|
109
|
+
type ReboundPattern,
|
|
110
|
+
} from '../simple-obstacle-layout'
|
|
111
|
+
import {
|
|
112
|
+
RISING_WALL_DEPTH,
|
|
113
|
+
RISING_WALL_HEIGHT,
|
|
114
|
+
RISING_WALL_WIDTH,
|
|
115
|
+
createRisingWallDeck,
|
|
116
|
+
createRisingWallLayout,
|
|
117
|
+
createRisingWallSchedule,
|
|
118
|
+
risingWallExposedHeight,
|
|
119
|
+
risingWallHeightFraction,
|
|
120
|
+
type RisingWallDensity,
|
|
121
|
+
} from '../rising-wall-layout'
|
|
122
|
+
import {
|
|
123
|
+
ATLAS_VORTEX_CENTER,
|
|
124
|
+
ATLAS_VORTEX_DRAIN_OUTPUT_WIDTH,
|
|
125
|
+
ATLAS_VORTEX_DRAIN_CHANNEL,
|
|
126
|
+
ATLAS_VORTEX_DRAIN_PATH,
|
|
127
|
+
ATLAS_VORTEX_ENTRY,
|
|
128
|
+
ATLAS_VORTEX_ENTRY_APPROACH,
|
|
129
|
+
ATLAS_VORTEX_INPUT,
|
|
130
|
+
ATLAS_VORTEX_OUTPUT,
|
|
131
|
+
createAtlasVortexDefinition,
|
|
132
|
+
atlasVortexDrainProfile,
|
|
133
|
+
} from '../atlas-vortex-layout'
|
|
134
|
+
import {
|
|
135
|
+
applyRailConnectorFrames,
|
|
136
|
+
createIntersectionRailDefinitions,
|
|
137
|
+
createPublicLaneRailDefinition,
|
|
138
|
+
createRoutingMergeDeck,
|
|
139
|
+
createRoutingSplitterDeck,
|
|
140
|
+
splitterPivotLocalPosition,
|
|
141
|
+
TRACK_LAB_FINISH_WIDTH,
|
|
142
|
+
TRACK_LAB_SPLITTER_ARM_DEPTH,
|
|
143
|
+
TRACK_LAB_SPLITTER_ARM_HEIGHT,
|
|
144
|
+
TRACK_LAB_SPLITTER_ARM_LENGTH,
|
|
145
|
+
TRACK_LAB_WALL_HEIGHT,
|
|
146
|
+
type RailConnectorFrame,
|
|
147
|
+
} from '../track-piece-layout'
|
|
148
|
+
import { collectEditorSelectables, computeEditorBounds } from './editor-object-geometry'
|
|
149
|
+
import {
|
|
150
|
+
CANONICAL_FLIPPER_DIMENSIONS,
|
|
151
|
+
createEndPivotedFlipperPaddleVisual,
|
|
152
|
+
createFlipperHubVisual,
|
|
153
|
+
createFlipperPaddleVisual,
|
|
154
|
+
} from '../flipper-component'
|
|
155
|
+
import { createPegObstacleVisualFactory } from '../../play/race/peg-obstacle-visual'
|
|
156
|
+
import {
|
|
157
|
+
WILD_WEST_ANTIQUE_BRASS_MATERIAL_ID,
|
|
158
|
+
WILD_WEST_GUNMETAL_FLIPPER_MATERIAL_ID,
|
|
159
|
+
} from '../../../three/rugged-track-appearance'
|
|
160
|
+
import {
|
|
161
|
+
addWildWestMineLiftFrame,
|
|
162
|
+
decorateWildWestMineLiftShelf,
|
|
163
|
+
WILD_WEST_THEME_ID,
|
|
164
|
+
} from '../../play/race/wild-west-mine-lift'
|
|
165
|
+
import {
|
|
166
|
+
addJungleTempleLiftSkin,
|
|
167
|
+
decorateJungleTempleShelf,
|
|
168
|
+
JUNGLE_TEMPLE_THEME_ID,
|
|
169
|
+
} from '../../play/race/jungle-temple-lift'
|
|
170
|
+
import {
|
|
171
|
+
addPortalDerivedElevatorSkins,
|
|
172
|
+
decoratePortalDerivedElevatorShelf,
|
|
173
|
+
EGYPT_ELEVATOR_THEME_ID,
|
|
174
|
+
JAPAN_ELEVATOR_THEME_ID,
|
|
175
|
+
SPACE_ELEVATOR_THEME_ID,
|
|
176
|
+
} from '../../play/race/portal-derived-elevator-skins'
|
|
177
|
+
import { markDynamicTrackShadowRoot } from '../../play/race/track-shadow-batching'
|
|
178
|
+
export interface TrackPieceVisual {
|
|
179
|
+
root: THREE.Group
|
|
180
|
+
bounds: THREE.Box3
|
|
181
|
+
selectableObjects: readonly THREE.Object3D[]
|
|
182
|
+
decorationAnchors: readonly THREE.Object3D[]
|
|
183
|
+
portalPreview?: {
|
|
184
|
+
color: string
|
|
185
|
+
endpoint: PortalEndpointVisual
|
|
186
|
+
pairId: string
|
|
187
|
+
role: 'entry' | 'exit'
|
|
188
|
+
}
|
|
189
|
+
dispose(): void
|
|
190
|
+
adoptMaterialRegistry(materials: ThemeMaterialRegistry): void
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface TrackPieceVisualOptions {
|
|
194
|
+
connectedConnectorIds?: ReadonlySet<string>
|
|
195
|
+
continuousFluidConnectorIds?: ReadonlySet<string>
|
|
196
|
+
continuousSupportConnectorIds?: ReadonlySet<string>
|
|
197
|
+
connectorFrames?: ReadonlyMap<string, RailConnectorFrame>
|
|
198
|
+
overviewLod?: boolean
|
|
199
|
+
/** Hidden live tracks need only their authored theme; the editor keeps instant theme switching. */
|
|
200
|
+
prebuildThemeVariants?: boolean
|
|
201
|
+
materialConnectionPhase?: number
|
|
202
|
+
deferSupportUntilMaterialContinuity?: boolean
|
|
203
|
+
/** Editor-only docking markers. Runtime/race visuals keep these hidden. */
|
|
204
|
+
connectorMarkersVisible?: boolean
|
|
205
|
+
/** Skip editor-only bounds, ray-picking meshes, and connector markers for hidden race preparation. */
|
|
206
|
+
editorMetadata?: boolean
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function createTrackPieceVisual(
|
|
210
|
+
piece: TrackPieceInstance,
|
|
211
|
+
materials: ThemeMaterialRegistry,
|
|
212
|
+
options: TrackPieceVisualOptions = {},
|
|
213
|
+
): TrackPieceVisual {
|
|
214
|
+
const template = initialTrackCatalog.require(piece)
|
|
215
|
+
const resolvedConnectors = resolveTemplateConnectors(template, piece.parameters)
|
|
216
|
+
const editorMetadata = options.editorMetadata !== false
|
|
217
|
+
const root = new THREE.Group()
|
|
218
|
+
root.name = piece.id
|
|
219
|
+
root.userData.pieceInstanceId = piece.id
|
|
220
|
+
root.position.set(...piece.transform.position)
|
|
221
|
+
root.rotation.y = (piece.transform.yawSteps * -Math.PI) / 2
|
|
222
|
+
root.userData.railConnectors = resolvedConnectors
|
|
223
|
+
root.userData.connectedConnectorIds = options.connectedConnectorIds
|
|
224
|
+
root.userData.continuousFluidConnectorIds = options.continuousFluidConnectorIds
|
|
225
|
+
root.userData.continuousSupportConnectorIds = options.continuousSupportConnectorIds
|
|
226
|
+
root.userData.connectorFrames = options.connectorFrames
|
|
227
|
+
root.userData.overviewLod = options.overviewLod === true
|
|
228
|
+
root.userData.prebuildThemeVariants = options.prebuildThemeVariants !== false
|
|
229
|
+
root.userData.editorBoundsSource = 'canonical-track-geometry'
|
|
230
|
+
root.userData.editorMetadata = editorMetadata
|
|
231
|
+
root.userData.materialConnectionPhase = options.deferSupportUntilMaterialContinuity
|
|
232
|
+
? Number.NaN
|
|
233
|
+
: options.materialConnectionPhase
|
|
234
|
+
root.userData.deferStoneArchitectureUntilMaterialContinuity = options.deferSupportUntilMaterialContinuity === true
|
|
235
|
+
const resolvedFloorMaterialId = resolvedFloorMaterialIdForTheme(
|
|
236
|
+
materials.activeTheme,
|
|
237
|
+
piece.appearance?.floorMaterialId,
|
|
238
|
+
)
|
|
239
|
+
const resolvedRailId = resolvedRailMaterialId(piece.appearance)
|
|
240
|
+
const appearance = piece.appearance
|
|
241
|
+
? {
|
|
242
|
+
...piece.appearance,
|
|
243
|
+
...(resolvedFloorMaterialId ? { floorMaterialId: resolvedFloorMaterialId } : { floorMaterialId: undefined }),
|
|
244
|
+
...(resolvedRailId ? { railMaterialId: resolvedRailId } : {}),
|
|
245
|
+
}
|
|
246
|
+
: undefined
|
|
247
|
+
root.userData.pieceAppearance = appearance
|
|
248
|
+
let portalPreview: TrackPieceVisual['portalPreview']
|
|
249
|
+
const addMesh = (
|
|
250
|
+
geometry: THREE.BufferGeometry,
|
|
251
|
+
slot: ThemeMaterialSlot,
|
|
252
|
+
name: string,
|
|
253
|
+
configure?: (mesh: THREE.Mesh) => void,
|
|
254
|
+
appearanceMaterialId?: string,
|
|
255
|
+
): THREE.Mesh => {
|
|
256
|
+
const mesh = new THREE.Mesh(geometry)
|
|
257
|
+
mesh.name = `${piece.id}-${name}`
|
|
258
|
+
mesh.userData.pieceInstanceId = piece.id
|
|
259
|
+
mesh.castShadow = true
|
|
260
|
+
mesh.receiveShadow = true
|
|
261
|
+
if (appearanceMaterialId) materials.bindAppearance(mesh, appearanceMaterialId, slot)
|
|
262
|
+
else materials.bind(mesh, slot)
|
|
263
|
+
configure?.(mesh)
|
|
264
|
+
root.add(mesh)
|
|
265
|
+
return mesh
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const publicLane = createPublicLaneRailDefinition({
|
|
269
|
+
id: piece.id,
|
|
270
|
+
templateId: template.id,
|
|
271
|
+
runtimeKind: template.runtimeKind,
|
|
272
|
+
parameters: piece.parameters,
|
|
273
|
+
})
|
|
274
|
+
if (publicLane) {
|
|
275
|
+
addRailPreview(root, materials, piece.id, publicLane)
|
|
276
|
+
} else if (template.runtimeKind === 'finish') {
|
|
277
|
+
addFinishPreview(root, materials, piece.id)
|
|
278
|
+
} else if (template.runtimeKind === 'vortex') {
|
|
279
|
+
root.userData.editorOutlineMode = 'bounds'
|
|
280
|
+
addAtlasVortexPreview(root, materials, piece.id)
|
|
281
|
+
} else {
|
|
282
|
+
portalPreview = addMechanismVisual(
|
|
283
|
+
root,
|
|
284
|
+
materials,
|
|
285
|
+
piece.id,
|
|
286
|
+
template,
|
|
287
|
+
piece.parameters,
|
|
288
|
+
appearance,
|
|
289
|
+
options.connectedConnectorIds,
|
|
290
|
+
addMesh,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (editorMetadata) {
|
|
295
|
+
const connectorMarkerGeometry = new VisualSphereGeometry(0.16, 10, 8)
|
|
296
|
+
const connectorMarkerMaterial = new THREE.MeshBasicMaterial({ color: '#ffffff', toneMapped: false })
|
|
297
|
+
// A render batch may clone this material while the course is faded out.
|
|
298
|
+
// Preserve its authored state so the clone reveals with the same opacity.
|
|
299
|
+
connectorMarkerMaterial.userData.themeBaseOpacity = connectorMarkerMaterial.opacity
|
|
300
|
+
connectorMarkerMaterial.userData.themeBaseTransparent = connectorMarkerMaterial.transparent
|
|
301
|
+
connectorMarkerMaterial.userData.themeBaseDepthWrite = connectorMarkerMaterial.depthWrite
|
|
302
|
+
for (const connector of resolvedConnectors) {
|
|
303
|
+
const mesh = new THREE.Mesh(connectorMarkerGeometry, connectorMarkerMaterial)
|
|
304
|
+
mesh.name = `${piece.id}-connector-${connector.id}`
|
|
305
|
+
mesh.position.set(...connector.localPosition)
|
|
306
|
+
mesh.userData.pieceInstanceId = piece.id
|
|
307
|
+
mesh.userData.materialSlot = 'editor.connector'
|
|
308
|
+
mesh.userData.excludeFromEditorBounds = true
|
|
309
|
+
mesh.userData.isTrackConnectorMarker = true
|
|
310
|
+
mesh.userData.ownsMaterial = true
|
|
311
|
+
// A connection always docks an output to an input at the same point.
|
|
312
|
+
// Keep the output-side marker as the single owner so the two spheres do
|
|
313
|
+
// not z-fight into a striped "gumdrop".
|
|
314
|
+
mesh.userData.connectorMarkerOwner =
|
|
315
|
+
!connector.kind.endsWith('input') || !options.connectedConnectorIds?.has(connector.id)
|
|
316
|
+
mesh.visible = options.connectorMarkersVisible === true && mesh.userData.connectorMarkerOwner
|
|
317
|
+
root.add(mesh)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
materials.rebindFloorAppearanceForTheme(root, piece.appearance?.floorMaterialId)
|
|
322
|
+
let bindingRegistry = materials
|
|
323
|
+
|
|
324
|
+
if (editorMetadata) root.updateMatrixWorld(true)
|
|
325
|
+
const bounds = editorMetadata ? computeEditorBounds(root) : new THREE.Box3()
|
|
326
|
+
const selectableObjects = editorMetadata ? collectEditorSelectables(root) : []
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
root,
|
|
330
|
+
bounds,
|
|
331
|
+
selectableObjects,
|
|
332
|
+
decorationAnchors: [],
|
|
333
|
+
...(portalPreview ? { portalPreview } : {}),
|
|
334
|
+
adoptMaterialRegistry(nextMaterials) {
|
|
335
|
+
if (bindingRegistry === nextMaterials) return
|
|
336
|
+
nextMaterials.adoptBindingsFrom(bindingRegistry, root)
|
|
337
|
+
bindingRegistry = nextMaterials
|
|
338
|
+
},
|
|
339
|
+
dispose() {
|
|
340
|
+
bindingRegistry.releaseBindings(root)
|
|
341
|
+
const geometries = new Set<THREE.BufferGeometry>()
|
|
342
|
+
const ownedMaterials = new Set<THREE.Material>()
|
|
343
|
+
const ownedTextures = new Set<THREE.Texture>()
|
|
344
|
+
root.traverse(object => {
|
|
345
|
+
if (object instanceof THREE.Mesh || object instanceof THREE.Line || object instanceof THREE.Points) {
|
|
346
|
+
if (!object.userData.sharedGeometry) geometries.add(object.geometry)
|
|
347
|
+
if (object.userData.ownsMaterial) {
|
|
348
|
+
const meshMaterials = Array.isArray(object.material) ? object.material : [object.material]
|
|
349
|
+
meshMaterials.forEach(material => ownedMaterials.add(material))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
geometries.forEach(geometry => geometry.dispose())
|
|
354
|
+
ownedMaterials.forEach(material => {
|
|
355
|
+
Object.values(material).forEach(value => {
|
|
356
|
+
if (value instanceof THREE.Texture && value.userData[PORTAL_RENDER_TARGET_TEXTURE] !== true) {
|
|
357
|
+
ownedTextures.add(value)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
material.dispose()
|
|
361
|
+
})
|
|
362
|
+
ownedTextures.forEach(texture => texture.dispose())
|
|
363
|
+
root.removeFromParent()
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function addAtlasVortexPreview(root: THREE.Group, materials: ThemeMaterialRegistry, pieceId: string): void {
|
|
369
|
+
const appearance = root.userData.pieceAppearance as TrackPieceInstance['appearance'] | undefined
|
|
370
|
+
const definition: VortexPieceDefinition = {
|
|
371
|
+
...createAtlasVortexDefinition(pieceId, ATLAS_VORTEX_CENTER, ATLAS_VORTEX_ENTRY, ATLAS_VORTEX_DRAIN_CHANNEL, [
|
|
372
|
+
ATLAS_VORTEX_OUTPUT[0] - ATLAS_VORTEX_DRAIN_CHANNEL[0],
|
|
373
|
+
ATLAS_VORTEX_OUTPUT[1] - ATLAS_VORTEX_DRAIN_CHANNEL[1],
|
|
374
|
+
ATLAS_VORTEX_OUTPUT[2] - ATLAS_VORTEX_DRAIN_CHANNEL[2],
|
|
375
|
+
]),
|
|
376
|
+
...(appearance ? { appearance } : {}),
|
|
377
|
+
deferStoneArchitectureUntilMaterialContinuity: root.userData.deferStoneArchitectureUntilMaterialContinuity === true,
|
|
378
|
+
waterFlowInputConnected: (root.userData.continuousFluidConnectorIds as ReadonlySet<string> | undefined)?.has(
|
|
379
|
+
'input',
|
|
380
|
+
),
|
|
381
|
+
}
|
|
382
|
+
const entryRail: RailPieceDefinition = {
|
|
383
|
+
id: `${pieceId}-entry`,
|
|
384
|
+
label: 'Vortex tangential entry',
|
|
385
|
+
type: 'rail',
|
|
386
|
+
points: [ATLAS_VORTEX_INPUT, ...ATLAS_VORTEX_ENTRY_APPROACH],
|
|
387
|
+
width: 4,
|
|
388
|
+
// The mechanism owns this inlet, but its open terminal is still a
|
|
389
|
+
// standard lane connector. Reuse the complete canonical trough profile
|
|
390
|
+
// so its top, underside, and both wall bases dock vertex-for-vertex with
|
|
391
|
+
// a snapped straight lane.
|
|
392
|
+
troughDepth: defaultTrackTroughDepth(4),
|
|
393
|
+
troughProfile: 'full',
|
|
394
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
395
|
+
continuousFloorMaterialOnly: true,
|
|
396
|
+
verticalWalls: true,
|
|
397
|
+
supportsAppearanceCanopy: false,
|
|
398
|
+
...(appearance ? { appearance } : {}),
|
|
399
|
+
}
|
|
400
|
+
const vortexMaterials = createPreviewTrackMaterials(
|
|
401
|
+
materials,
|
|
402
|
+
appearance,
|
|
403
|
+
root.userData.prebuildThemeVariants !== false,
|
|
404
|
+
)
|
|
405
|
+
const framedEntryRail = addRailPreview(root, materials, pieceId, entryRail, definition)
|
|
406
|
+
addVortex(root, undefined, definition, vortexMaterials, undefined, framedEntryRail)
|
|
407
|
+
addRailPreview(root, materials, pieceId, {
|
|
408
|
+
id: `${pieceId}-drain`,
|
|
409
|
+
label: 'Continuous Atlas vortex catch and single-file drain',
|
|
410
|
+
type: 'rail',
|
|
411
|
+
points: [...ATLAS_VORTEX_DRAIN_PATH],
|
|
412
|
+
width: ATLAS_VORTEX_DRAIN_OUTPUT_WIDTH,
|
|
413
|
+
pointWidths: atlasVortexDrainProfile(),
|
|
414
|
+
widthProfile: 'smoothstep',
|
|
415
|
+
troughDepth: defaultTrackTroughDepth(ATLAS_VORTEX_DRAIN_OUTPUT_WIDTH),
|
|
416
|
+
pointTroughDepths: atlasVortexDrainProfile().map(defaultTrackTroughDepth),
|
|
417
|
+
troughProfile: 'full',
|
|
418
|
+
troughFlatFraction: 0,
|
|
419
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
420
|
+
continuousFloorMaterialOnly: true,
|
|
421
|
+
supportsAppearanceCanopy: false,
|
|
422
|
+
// The tube drops above this receiving slope. Keep the floor and side-wall
|
|
423
|
+
// solids capped, but leave the rear open so no cross-wall is visible
|
|
424
|
+
// through the funnel bore.
|
|
425
|
+
capStart: true,
|
|
426
|
+
wallFriction: 0,
|
|
427
|
+
waterFlowStartConnected: Boolean(appearance?.vortexWaterId),
|
|
428
|
+
...(appearance?.vortexWaterId ? { appearance: { ...appearance, waterOverlayId: appearance.vortexWaterId } } : {}),
|
|
429
|
+
})
|
|
430
|
+
root.traverse(object => {
|
|
431
|
+
object.userData.pieceInstanceId = pieceId
|
|
432
|
+
})
|
|
433
|
+
materials.captureBindings(root)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function addRailPreview(
|
|
437
|
+
root: THREE.Group,
|
|
438
|
+
materials: ThemeMaterialRegistry,
|
|
439
|
+
pieceId: string,
|
|
440
|
+
definition: RailPieceDefinition,
|
|
441
|
+
vortex?: VortexPieceDefinition,
|
|
442
|
+
): RailPieceDefinition {
|
|
443
|
+
const connectors = (root.userData.railConnectors as readonly TrackConnectorTemplate[] | undefined) ?? []
|
|
444
|
+
const connectedConnectorIds = root.userData.connectedConnectorIds as ReadonlySet<string> | undefined
|
|
445
|
+
const continuousFluidConnectorIds = root.userData.continuousFluidConnectorIds as ReadonlySet<string> | undefined
|
|
446
|
+
const continuousSupportConnectorIds = root.userData.continuousSupportConnectorIds as ReadonlySet<string> | undefined
|
|
447
|
+
const connectorFrames = root.userData.connectorFrames as ReadonlyMap<string, RailConnectorFrame> | undefined
|
|
448
|
+
const framedDefinition = applyRailConnectorFrames(definition, connectors, connectedConnectorIds, connectorFrames)
|
|
449
|
+
const appearance = root.userData.pieceAppearance as TrackPieceInstance['appearance'] | undefined
|
|
450
|
+
const junctionBranches = framedDefinition.junctionBranches?.map(branch => ({
|
|
451
|
+
...branch,
|
|
452
|
+
waterFlowStartConnected: continuousFluidConnectorIds?.has(branch.connectorId) === true,
|
|
453
|
+
points: connectorFrames?.get(branch.connectorId)?.position
|
|
454
|
+
? [connectorFrames.get(branch.connectorId)!.position!, ...branch.points.slice(1)]
|
|
455
|
+
: branch.points,
|
|
456
|
+
startTangent: connectorFrames?.get(branch.connectorId)?.tangent ?? branch.startTangent,
|
|
457
|
+
}))
|
|
458
|
+
const previewDefinition = {
|
|
459
|
+
...framedDefinition,
|
|
460
|
+
editorProxies: root.userData.editorMetadata !== false,
|
|
461
|
+
materialConnectors: connectors.map(connector => ({
|
|
462
|
+
id: connector.id,
|
|
463
|
+
position: connector.localPosition,
|
|
464
|
+
})),
|
|
465
|
+
materialConnectionPhase: root.userData.materialConnectionPhase as number | undefined,
|
|
466
|
+
deferStoneArchitectureUntilMaterialContinuity: root.userData.deferStoneArchitectureUntilMaterialContinuity === true,
|
|
467
|
+
waterFlowStartConnected: framedDefinition.junctionMainConnectorId
|
|
468
|
+
? continuousFluidConnectorIds?.has(framedDefinition.junctionMainConnectorId) === true
|
|
469
|
+
: connectors.some(
|
|
470
|
+
connector => connector.kind.endsWith('input') && continuousFluidConnectorIds?.has(connector.id),
|
|
471
|
+
),
|
|
472
|
+
waterFlowEndConnected: connectors.some(
|
|
473
|
+
connector => connector.kind.endsWith('output') && continuousFluidConnectorIds?.has(connector.id),
|
|
474
|
+
),
|
|
475
|
+
...(junctionBranches ? { junctionBranches } : {}),
|
|
476
|
+
verticalWalls: true,
|
|
477
|
+
...(root.userData.overviewLod ? { previewProfileSamplePitch: 0.05 } : {}),
|
|
478
|
+
...(appearance || framedDefinition.appearance
|
|
479
|
+
? { appearance: { ...appearance, ...framedDefinition.appearance } }
|
|
480
|
+
: {}),
|
|
481
|
+
}
|
|
482
|
+
addRailPiece(
|
|
483
|
+
root,
|
|
484
|
+
undefined,
|
|
485
|
+
previewDefinition,
|
|
486
|
+
createPreviewTrackMaterials(materials, appearance, root.userData.prebuildThemeVariants !== false),
|
|
487
|
+
undefined,
|
|
488
|
+
vortex,
|
|
489
|
+
undefined,
|
|
490
|
+
{
|
|
491
|
+
capStart: !connectors.some(
|
|
492
|
+
connector => connector.kind.endsWith('input') && continuousSupportConnectorIds?.has(connector.id),
|
|
493
|
+
),
|
|
494
|
+
capEnd: !connectors.some(
|
|
495
|
+
connector => connector.kind.endsWith('output') && continuousSupportConnectorIds?.has(connector.id),
|
|
496
|
+
),
|
|
497
|
+
},
|
|
498
|
+
)
|
|
499
|
+
root.traverse(object => {
|
|
500
|
+
object.userData.pieceInstanceId = pieceId
|
|
501
|
+
})
|
|
502
|
+
materials.captureBindings(root)
|
|
503
|
+
return previewDefinition
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function createPreviewTrackMaterials(
|
|
507
|
+
materials: ThemeMaterialRegistry,
|
|
508
|
+
appearance?: TrackPieceInstance['appearance'],
|
|
509
|
+
prebuildThemeVariants = true,
|
|
510
|
+
): TrackMaterials {
|
|
511
|
+
return {
|
|
512
|
+
activeThemeId: materials.activeTheme.id,
|
|
513
|
+
prebuildThemeVariants,
|
|
514
|
+
get top() {
|
|
515
|
+
return materials.getAppearance(appearance?.floorMaterialId, 'lane.surface')
|
|
516
|
+
},
|
|
517
|
+
get edge() {
|
|
518
|
+
return materials.get('lane.edge')
|
|
519
|
+
},
|
|
520
|
+
get rail() {
|
|
521
|
+
return materials.getAppearance(appearance?.wallMaterialId, 'lane.guard')
|
|
522
|
+
},
|
|
523
|
+
get railBacking() {
|
|
524
|
+
return materials.get('lane.guard.backing')
|
|
525
|
+
},
|
|
526
|
+
get ropeWallBacking() {
|
|
527
|
+
return materials.getAppearance(SHARED_ROPE_WALL_BACKING_MATERIAL_ID, 'lane.guard.backing')
|
|
528
|
+
},
|
|
529
|
+
get ropeWallEndGrain() {
|
|
530
|
+
return materials.getAppearance(SHARED_ROPE_WALL_END_GRAIN_MATERIAL_ID, 'structure.primary')
|
|
531
|
+
},
|
|
532
|
+
get ropeWallTimber() {
|
|
533
|
+
return materials.getAppearance(SHARED_ROPE_WALL_TIMBER_MATERIAL_ID, 'structure.primary')
|
|
534
|
+
},
|
|
535
|
+
get structure() {
|
|
536
|
+
return materials.get('structure.primary')
|
|
537
|
+
},
|
|
538
|
+
get moving() {
|
|
539
|
+
return materials.get('mechanism.moving')
|
|
540
|
+
},
|
|
541
|
+
get signal() {
|
|
542
|
+
return materials.get('signal.route')
|
|
543
|
+
},
|
|
544
|
+
get hazard() {
|
|
545
|
+
return materials.get('signal.hazard')
|
|
546
|
+
},
|
|
547
|
+
get portal() {
|
|
548
|
+
return materials.get('effect.portal')
|
|
549
|
+
},
|
|
550
|
+
get energy() {
|
|
551
|
+
return materials.get('effect.energy')
|
|
552
|
+
},
|
|
553
|
+
get railroad() {
|
|
554
|
+
return materials.getAppearance(resolvedRailMaterialId(appearance) ?? WILD_WEST_RAIL_MATERIAL_ID, 'lane.edge')
|
|
555
|
+
},
|
|
556
|
+
get railDetailStyle() {
|
|
557
|
+
return materials.getAppearanceDetailStyle(resolvedRailMaterialId(appearance))
|
|
558
|
+
},
|
|
559
|
+
get railroadTimber() {
|
|
560
|
+
return materials.getAppearance(WILD_WEST_RAIL_TIE_MATERIAL_ID, 'structure.primary')
|
|
561
|
+
},
|
|
562
|
+
get obstacle() {
|
|
563
|
+
return materials.get('obstacle.primary')
|
|
564
|
+
},
|
|
565
|
+
get obstacleAccent() {
|
|
566
|
+
return materials.get('obstacle.secondary')
|
|
567
|
+
},
|
|
568
|
+
get obstacleBase() {
|
|
569
|
+
return materials.get('obstacle.base')
|
|
570
|
+
},
|
|
571
|
+
get flipperPrimary() {
|
|
572
|
+
return materials.get('mechanism.flipper.primary')
|
|
573
|
+
},
|
|
574
|
+
get flipperSecondary() {
|
|
575
|
+
return materials.get('mechanism.flipper.secondary')
|
|
576
|
+
},
|
|
577
|
+
get mineTimber() {
|
|
578
|
+
return materials.getAppearance('wild-west.mine-timber', 'structure.primary')
|
|
579
|
+
},
|
|
580
|
+
get coveredMineTimber() {
|
|
581
|
+
return materials.getAppearance(WILD_WEST_COVERED_MINE_TIMBER_ID, 'structure.primary')
|
|
582
|
+
},
|
|
583
|
+
get coveredMineIron() {
|
|
584
|
+
return materials.getAppearance(WILD_WEST_COVERED_MINE_IRON_ID, 'structure.secondary')
|
|
585
|
+
},
|
|
586
|
+
get portalIron() {
|
|
587
|
+
return materials.getAppearance(WILD_WEST_GUNMETAL_FLIPPER_MATERIAL_ID, 'structure.secondary')
|
|
588
|
+
},
|
|
589
|
+
get bone() {
|
|
590
|
+
return materials.getAppearance('wild-west.warm-ivory-bone', 'structure.primary')
|
|
591
|
+
},
|
|
592
|
+
get support() {
|
|
593
|
+
return materials.getAppearance(appearance?.supportMaterialId, 'structure.primary')
|
|
594
|
+
},
|
|
595
|
+
get floorSurfaceTreatment() {
|
|
596
|
+
return resolveFloorSurfaceTreatment(materials, appearance?.floorMaterialId)
|
|
597
|
+
},
|
|
598
|
+
get floorSurfaceTreatments() {
|
|
599
|
+
if (prebuildThemeVariants) {
|
|
600
|
+
return resolveFloorSurfaceTreatments(materials, appearance?.floorMaterialId)
|
|
601
|
+
}
|
|
602
|
+
const treatment = resolveFloorSurfaceTreatment(materials, appearance?.floorMaterialId)
|
|
603
|
+
return treatment ? [treatment] : []
|
|
604
|
+
},
|
|
605
|
+
get supportMetal() {
|
|
606
|
+
return materials.getAppearance(WILD_WEST_TRESTLE_METAL_MATERIAL_ID, 'structure.secondary')
|
|
607
|
+
},
|
|
608
|
+
get jungleTempleStone() {
|
|
609
|
+
return materials.getAppearance(JUNGLE_TEMPLE_STONE_MATERIAL_ID, 'lane.guard')
|
|
610
|
+
},
|
|
611
|
+
get jungleTempleFacade() {
|
|
612
|
+
return materials.getAppearance(JUNGLE_TEMPLE_FACADE_MATERIAL_ID, 'lane.guard')
|
|
613
|
+
},
|
|
614
|
+
get jungleTempleTimber() {
|
|
615
|
+
return materials.getAppearance(JUNGLE_TEMPLE_TIMBER_MATERIAL_ID, 'structure.primary')
|
|
616
|
+
},
|
|
617
|
+
get jungleTemplePlatform() {
|
|
618
|
+
return materials.getAppearance(JUNGLE_TEMPLE_PLATFORM_MATERIAL_ID, 'mechanism.moving')
|
|
619
|
+
},
|
|
620
|
+
get jungleTempleMoss() {
|
|
621
|
+
return materials.get('world.foliage')
|
|
622
|
+
},
|
|
623
|
+
get jungleTempleBronze() {
|
|
624
|
+
return materials.getAppearance(JUNGLE_TEMPLE_BRONZE_MATERIAL_ID, 'signal.route')
|
|
625
|
+
},
|
|
626
|
+
get wildWestBrass() {
|
|
627
|
+
return materials.getAppearance(WILD_WEST_ANTIQUE_BRASS_MATERIAL_ID, 'signal.route')
|
|
628
|
+
},
|
|
629
|
+
get egyptPortalSandstone() {
|
|
630
|
+
return materials.getAppearance(ANCIENT_EGYPT_PORTAL_SANDSTONE_ID, 'structure.primary')
|
|
631
|
+
},
|
|
632
|
+
get egyptPortalCarvedSandstone() {
|
|
633
|
+
return materials.getAppearance(ANCIENT_EGYPT_PORTAL_CARVED_SANDSTONE_ID, 'lane.guard')
|
|
634
|
+
},
|
|
635
|
+
get egyptPortalAgedGold() {
|
|
636
|
+
return materials.getAppearance(ANCIENT_EGYPT_PORTAL_AGED_GOLD_ID, 'signal.route')
|
|
637
|
+
},
|
|
638
|
+
get egyptPyramidLimestone() {
|
|
639
|
+
return materials.getAppearance(ANCIENT_EGYPT_PYRAMID_LIMESTONE_ID, 'structure.primary')
|
|
640
|
+
},
|
|
641
|
+
get egyptPyramidGold() {
|
|
642
|
+
return materials.get('obstacle.primary')
|
|
643
|
+
},
|
|
644
|
+
get sakuraCastleTimber() {
|
|
645
|
+
return materials.getAppearance(JAPANESE_SAKURA_CASTLE_TIMBER_ID, 'structure.primary')
|
|
646
|
+
},
|
|
647
|
+
get sakuraCastleMasonry() {
|
|
648
|
+
return materials.getAppearance(JAPANESE_SAKURA_CASTLE_MASONRY_ID, 'structure.secondary')
|
|
649
|
+
},
|
|
650
|
+
get sakuraCastleKawara() {
|
|
651
|
+
return materials.getAppearance(JAPANESE_SAKURA_CASTLE_KAWARA_ID, 'structure.secondary')
|
|
652
|
+
},
|
|
653
|
+
get sakuraBridgeTimber() {
|
|
654
|
+
return materials.getAppearance(JAPANESE_SAKURA_BRIDGE_TIMBER_ID, 'structure.primary')
|
|
655
|
+
},
|
|
656
|
+
get sakuraBridgeHardware() {
|
|
657
|
+
return materials.getAppearance(JAPANESE_SAKURA_BRIDGE_HARDWARE_ID, 'structure.secondary')
|
|
658
|
+
},
|
|
659
|
+
get spaceStationWallChassis() {
|
|
660
|
+
return materials.getAppearance(SPACE_STATION_ARMORED_WALL_ID, 'lane.guard')
|
|
661
|
+
},
|
|
662
|
+
get spaceStationWallPanel() {
|
|
663
|
+
return materials.getAppearance(SPACE_STATION_WALL_PANEL_ID, 'lane.guard')
|
|
664
|
+
},
|
|
665
|
+
get spaceStationWallRib() {
|
|
666
|
+
return materials.getAppearance(SPACE_STATION_WALL_RIB_ID, 'structure.primary')
|
|
667
|
+
},
|
|
668
|
+
get spaceStationWallTrim() {
|
|
669
|
+
return materials.getAppearance(SPACE_STATION_WALL_TRIM_ID, 'structure.secondary')
|
|
670
|
+
},
|
|
671
|
+
get spaceStationWallLight() {
|
|
672
|
+
return materials.getAppearance(SPACE_STATION_WALL_LIGHT_ID, 'effect.energy')
|
|
673
|
+
},
|
|
674
|
+
get spaceStationCenterGuide() {
|
|
675
|
+
return materials.getAppearance(SPACE_STATION_CENTER_GUIDE_ID, 'effect.energy')
|
|
676
|
+
},
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function addFinishPreview(root: THREE.Group, materials: ThemeMaterialRegistry, pieceId: string): void {
|
|
681
|
+
addRailPreview(root, materials, pieceId, {
|
|
682
|
+
id: `${pieceId}-channel`,
|
|
683
|
+
label: 'Continuous narrowing ranked finish channel',
|
|
684
|
+
type: 'rail',
|
|
685
|
+
raceRole: 'finish',
|
|
686
|
+
points: [
|
|
687
|
+
[0, 0, 2.5],
|
|
688
|
+
[0, -2, -2],
|
|
689
|
+
[0, -2.7, -12],
|
|
690
|
+
],
|
|
691
|
+
pointWidths: [4, TRACK_LAB_FINISH_WIDTH, TRACK_LAB_FINISH_WIDTH],
|
|
692
|
+
width: TRACK_LAB_FINISH_WIDTH,
|
|
693
|
+
pointTroughDepths: [
|
|
694
|
+
defaultTrackTroughDepth(4),
|
|
695
|
+
defaultTrackTroughDepth(TRACK_LAB_FINISH_WIDTH),
|
|
696
|
+
defaultTrackTroughDepth(TRACK_LAB_FINISH_WIDTH),
|
|
697
|
+
],
|
|
698
|
+
troughDepth: defaultTrackTroughDepth(TRACK_LAB_FINISH_WIDTH),
|
|
699
|
+
troughProfile: 'full',
|
|
700
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
701
|
+
floorFriction: 0.08,
|
|
702
|
+
wallFriction: 0,
|
|
703
|
+
closedEnd: true,
|
|
704
|
+
})
|
|
705
|
+
const finishMaterials = {
|
|
706
|
+
...createPreviewTrackMaterials(materials, undefined, root.userData.prebuildThemeVariants !== false),
|
|
707
|
+
signal: materials.get('signal.finish'),
|
|
708
|
+
}
|
|
709
|
+
addFinishGate(
|
|
710
|
+
root,
|
|
711
|
+
{
|
|
712
|
+
id: pieceId,
|
|
713
|
+
label: 'Ranked finish channel',
|
|
714
|
+
type: 'finish',
|
|
715
|
+
center: [0, -2, -2],
|
|
716
|
+
direction: [0, 0, -1],
|
|
717
|
+
width: TRACK_LAB_FINISH_WIDTH,
|
|
718
|
+
channelEnd: [0, -2.7, -12],
|
|
719
|
+
rankingSlots: 6,
|
|
720
|
+
rankingSpacing: 1.22,
|
|
721
|
+
},
|
|
722
|
+
finishMaterials,
|
|
723
|
+
)
|
|
724
|
+
root.traverse(object => {
|
|
725
|
+
object.userData.pieceInstanceId = pieceId
|
|
726
|
+
})
|
|
727
|
+
materials.captureBindings(root)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
type AddMesh = (
|
|
731
|
+
geometry: THREE.BufferGeometry,
|
|
732
|
+
slot: ThemeMaterialSlot,
|
|
733
|
+
name: string,
|
|
734
|
+
configure?: (mesh: THREE.Mesh) => void,
|
|
735
|
+
appearanceMaterialId?: string,
|
|
736
|
+
) => THREE.Mesh
|
|
737
|
+
|
|
738
|
+
function addMechanismVisual(
|
|
739
|
+
root: THREE.Group,
|
|
740
|
+
materials: ThemeMaterialRegistry,
|
|
741
|
+
pieceId: string,
|
|
742
|
+
template: TrackPieceTemplate,
|
|
743
|
+
parameters: JsonObject,
|
|
744
|
+
appearance: TrackPieceInstance['appearance'],
|
|
745
|
+
_connectedConnectorIds: ReadonlySet<string> | undefined,
|
|
746
|
+
addMesh: AddMesh,
|
|
747
|
+
): TrackPieceVisual['portalPreview'] {
|
|
748
|
+
if (template.runtimeKind === 'start') {
|
|
749
|
+
addRailPreview(root, materials, pieceId, {
|
|
750
|
+
id: pieceId,
|
|
751
|
+
label: 'Start grid',
|
|
752
|
+
type: 'rail',
|
|
753
|
+
raceRole: 'start',
|
|
754
|
+
points: [
|
|
755
|
+
[0, 1.2, 4],
|
|
756
|
+
[0, 0, -2.5],
|
|
757
|
+
],
|
|
758
|
+
startWidth: 6,
|
|
759
|
+
width: 4,
|
|
760
|
+
startTroughDepth: defaultTrackTroughDepth(6),
|
|
761
|
+
troughDepth: defaultTrackTroughDepth(4),
|
|
762
|
+
troughProfile: 'full',
|
|
763
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
764
|
+
floorFriction: 0.08,
|
|
765
|
+
})
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (template.runtimeKind === 'peg-field') {
|
|
770
|
+
const layout = createConeFieldLayout(
|
|
771
|
+
lengthUnitsFromParameters(parameters),
|
|
772
|
+
conePatternFromParameters(parameters),
|
|
773
|
+
gradeFromParameters(parameters),
|
|
774
|
+
)
|
|
775
|
+
const deckProfile = createConeFieldDeckProfile(layout, defaultTrackTroughDepth(4))
|
|
776
|
+
addRailPreview(root, materials, pieceId, {
|
|
777
|
+
id: `${pieceId}-deck`,
|
|
778
|
+
label: 'Continuous enclosed cone forest deck',
|
|
779
|
+
type: 'rail',
|
|
780
|
+
points: [...deckProfile.points],
|
|
781
|
+
width: 4,
|
|
782
|
+
pointWidths: [...deckProfile.pointWidths],
|
|
783
|
+
troughDepth: defaultTrackTroughDepth(4),
|
|
784
|
+
pointTroughDepths: [...deckProfile.pointTroughDepths],
|
|
785
|
+
troughProfile: 'full',
|
|
786
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
787
|
+
floorFriction: 0.012,
|
|
788
|
+
wallFriction: 0,
|
|
789
|
+
})
|
|
790
|
+
const up = vector(layout.up).normalize()
|
|
791
|
+
const rotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), up)
|
|
792
|
+
const createVisual = createPegObstacleVisualFactory({
|
|
793
|
+
radius: CONE_FIELD_PEG_RADIUS,
|
|
794
|
+
height: CONE_FIELD_PEG_HEIGHT,
|
|
795
|
+
colliderShape: 'cone',
|
|
796
|
+
bodyMaterial: materials.get('obstacle.primary'),
|
|
797
|
+
trimMaterial: materials.get('obstacle.secondary'),
|
|
798
|
+
})
|
|
799
|
+
layout.pegBases.forEach((base, index) => {
|
|
800
|
+
const visual = createVisual(`${pieceId}:${index}`).group
|
|
801
|
+
visual.name = `${pieceId}-cone-${index}`
|
|
802
|
+
visual.position.copy(vector(base).addScaledVector(up, CONE_FIELD_PEG_HEIGHT / 2))
|
|
803
|
+
visual.quaternion.copy(rotation)
|
|
804
|
+
visual.traverse(object => {
|
|
805
|
+
object.userData.pieceInstanceId = pieceId
|
|
806
|
+
})
|
|
807
|
+
root.add(visual)
|
|
808
|
+
})
|
|
809
|
+
materials.captureBindings(root)
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (template.runtimeKind === 'single-file-squeeze') {
|
|
814
|
+
addRailPreview(root, materials, pieceId, createSingleFileSqueezeRail(pieceId, template.label, parameters))
|
|
815
|
+
return
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (template.runtimeKind === 'rebounds') {
|
|
819
|
+
const appearance = root.userData.pieceAppearance as TrackPieceInstance['appearance'] | undefined
|
|
820
|
+
const lengthUnits = lengthUnitsFromParameters(parameters)
|
|
821
|
+
const layout = createReboundLayout(lengthUnits, parameters, reboundPatternFromParameters(parameters))
|
|
822
|
+
addRailPreview(
|
|
823
|
+
root,
|
|
824
|
+
materials,
|
|
825
|
+
pieceId,
|
|
826
|
+
createReboundDeck(`${pieceId}-deck`, template.label, lengthUnits, parameters),
|
|
827
|
+
)
|
|
828
|
+
addRebounds(
|
|
829
|
+
root,
|
|
830
|
+
{
|
|
831
|
+
id: `${pieceId}-rebounds`,
|
|
832
|
+
label: template.label,
|
|
833
|
+
type: 'rebounds',
|
|
834
|
+
direction: layout.direction,
|
|
835
|
+
mounts: layout.mounts,
|
|
836
|
+
reboundLength: REBOUND_LENGTH,
|
|
837
|
+
reboundWidth: REBOUND_WIDTH,
|
|
838
|
+
reboundHeight: REBOUND_HEIGHT,
|
|
839
|
+
friction: 0.01,
|
|
840
|
+
restitution: REBOUND_RESTITUTION,
|
|
841
|
+
strikeDistance: REBOUND_STRIKE_DISTANCE,
|
|
842
|
+
strikeSeconds: REBOUND_STRIKE_SECONDS,
|
|
843
|
+
holdSeconds: REBOUND_HOLD_SECONDS,
|
|
844
|
+
returnSeconds: REBOUND_RETURN_SECONDS,
|
|
845
|
+
cooldownSeconds: REBOUND_COOLDOWN_SECONDS,
|
|
846
|
+
...(appearance ? { appearance } : {}),
|
|
847
|
+
},
|
|
848
|
+
createPreviewTrackMaterials(materials, appearance, root.userData.prebuildThemeVariants !== false),
|
|
849
|
+
)
|
|
850
|
+
root.children
|
|
851
|
+
.filter(child => child.name.startsWith(`${pieceId}-rebounds-rebound-`))
|
|
852
|
+
.forEach(markDynamicTrackShadowRoot)
|
|
853
|
+
materials.captureBindings(root)
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (template.runtimeKind === 'rising-wall-forest') {
|
|
858
|
+
const layout = createRisingWallLayout(lengthUnitsFromParameters(parameters), parameters)
|
|
859
|
+
const density = risingWallDensityFromParameters(parameters)
|
|
860
|
+
const schedule = createRisingWallSchedule(layout, density, `preview:${pieceId}`)
|
|
861
|
+
const basis = trackSurfaceBasis(layout.direction)
|
|
862
|
+
const up = vector(basis.up)
|
|
863
|
+
const rotation = new THREE.Quaternion().setFromRotationMatrix(
|
|
864
|
+
new THREE.Matrix4().makeBasis(vector(basis.right), up, vector(basis.forward).negate()),
|
|
865
|
+
)
|
|
866
|
+
addRailPreview(root, materials, pieceId, createRisingWallDeck(`${pieceId}-deck`, template.label, layout))
|
|
867
|
+
const wallGeometry = new THREE.BoxGeometry(RISING_WALL_WIDTH, RISING_WALL_HEIGHT, RISING_WALL_DEPTH)
|
|
868
|
+
const slotGeometry = new THREE.BoxGeometry(RISING_WALL_WIDTH + 0.14, 0.035, RISING_WALL_DEPTH + 0.14)
|
|
869
|
+
layout.anchors.forEach((anchor, index) => {
|
|
870
|
+
const fraction = risingWallHeightFraction(schedule, index, 0)
|
|
871
|
+
const exposedHeight = risingWallExposedHeight(RISING_WALL_HEIGHT, fraction)
|
|
872
|
+
const wall = addMesh(wallGeometry, 'obstacle.primary', `rising-wall-${index}`, mesh => {
|
|
873
|
+
mesh.position.copy(vector(anchor.position).addScaledVector(up, exposedHeight / 2))
|
|
874
|
+
mesh.quaternion.copy(rotation)
|
|
875
|
+
mesh.scale.set(1, exposedHeight / RISING_WALL_HEIGHT, 1)
|
|
876
|
+
mesh.visible = exposedHeight > 0.001
|
|
877
|
+
})
|
|
878
|
+
markDynamicTrackShadowRoot(wall)
|
|
879
|
+
addMesh(slotGeometry, 'obstacle.base', `rising-wall-slot-${index}`, mesh => {
|
|
880
|
+
mesh.position.copy(vector(anchor.position).addScaledVector(up, 0.018))
|
|
881
|
+
mesh.quaternion.copy(rotation)
|
|
882
|
+
})
|
|
883
|
+
})
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (template.runtimeKind === 'splitter') {
|
|
888
|
+
const drop = laneGradeDrop({ ...parameters, grade: parameters.grade ?? 'slope', lengthUnits: 1 })
|
|
889
|
+
const basis = trackSurfaceBasis([0, -drop, -TRACK_MODULE_LENGTH])
|
|
890
|
+
const forward = vector(basis.forward)
|
|
891
|
+
const surfaceRotation = new THREE.Quaternion().setFromRotationMatrix(
|
|
892
|
+
new THREE.Matrix4().makeBasis(forward, vector(basis.up), vector(basis.right)),
|
|
893
|
+
)
|
|
894
|
+
const center = vector(splitterPivotLocalPosition(drop))
|
|
895
|
+
addRailPreview(
|
|
896
|
+
root,
|
|
897
|
+
materials,
|
|
898
|
+
pieceId,
|
|
899
|
+
createRoutingSplitterDeck(`${pieceId}-deck`, 'Splitter routing deck', drop),
|
|
900
|
+
)
|
|
901
|
+
const flipperPrimary = materials.get('mechanism.flipper.primary')
|
|
902
|
+
const flipperSecondary = materials.get('mechanism.flipper.secondary')
|
|
903
|
+
const paddle = createEndPivotedFlipperPaddleVisual(
|
|
904
|
+
`${pieceId}-splitter-paddle`,
|
|
905
|
+
flipperPrimary,
|
|
906
|
+
{
|
|
907
|
+
length: TRACK_LAB_SPLITTER_ARM_LENGTH,
|
|
908
|
+
width: TRACK_LAB_SPLITTER_ARM_DEPTH,
|
|
909
|
+
height: TRACK_LAB_SPLITTER_ARM_HEIGHT,
|
|
910
|
+
},
|
|
911
|
+
flipperSecondary,
|
|
912
|
+
`${pieceId}-splitter-hub`,
|
|
913
|
+
0,
|
|
914
|
+
)
|
|
915
|
+
paddle.position.copy(center)
|
|
916
|
+
paddle.quaternion.copy(surfaceRotation)
|
|
917
|
+
paddle.userData.pieceInstanceId = pieceId
|
|
918
|
+
markDynamicTrackShadowRoot(paddle)
|
|
919
|
+
const paddleSurface = paddle.getObjectByName(`${pieceId}-splitter-paddle-surface`)
|
|
920
|
+
if (!(paddleSurface instanceof THREE.Mesh)) throw new Error('Canonical splitter paddle surface is missing')
|
|
921
|
+
materials.bind(paddleSurface, 'mechanism.flipper.primary')
|
|
922
|
+
const paddleHub = paddle.getObjectByName(`${pieceId}-splitter-hub`)
|
|
923
|
+
if (!(paddleHub instanceof THREE.Mesh)) throw new Error('Canonical splitter hub is missing')
|
|
924
|
+
materials.bind(paddleHub, 'mechanism.flipper.secondary')
|
|
925
|
+
root.add(paddle)
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (template.runtimeKind === 'merge') {
|
|
930
|
+
const drop = laneGradeDrop({ ...parameters, grade: parameters.grade ?? 'slope', lengthUnits: 1 })
|
|
931
|
+
addRailPreview(root, materials, pieceId, createRoutingMergeDeck(pieceId, 'Route merge catch deck', drop))
|
|
932
|
+
return
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (template.runtimeKind === 'intersection') {
|
|
936
|
+
const connectors = resolveTemplateConnectors(template, parameters)
|
|
937
|
+
const output = connectors.find(connector => connector.kind.endsWith('output'))
|
|
938
|
+
const allInputs = connectors.filter(connector => connector.kind.endsWith('input'))
|
|
939
|
+
if (!output || allInputs.length === 0) return
|
|
940
|
+
createIntersectionRailDefinitions(pieceId, template.label, allInputs, output).forEach(definition => {
|
|
941
|
+
addRailPreview(root, materials, pieceId, definition)
|
|
942
|
+
})
|
|
943
|
+
return
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (template.runtimeKind === 'flipper-bank') {
|
|
947
|
+
const layout = createFlipperBankLayout(lengthUnitsFromParameters(parameters), gradeFromParameters(parameters))
|
|
948
|
+
addRailPreview(
|
|
949
|
+
root,
|
|
950
|
+
materials,
|
|
951
|
+
pieceId,
|
|
952
|
+
createFlipperBankDeck(
|
|
953
|
+
`${pieceId}-deck`,
|
|
954
|
+
'Continuous enclosed flipper deck',
|
|
955
|
+
lengthUnitsFromParameters(parameters),
|
|
956
|
+
gradeFromParameters(parameters),
|
|
957
|
+
),
|
|
958
|
+
)
|
|
959
|
+
const mounts = layout.mounts
|
|
960
|
+
const { length: paddleLength, height: paddleHeight } = CANONICAL_FLIPPER_DIMENSIONS
|
|
961
|
+
const flipperPrimary = materials.get('mechanism.flipper.primary')
|
|
962
|
+
const flipperSecondary = materials.get('mechanism.flipper.secondary')
|
|
963
|
+
mounts.forEach((mount, index) => {
|
|
964
|
+
const tangent = vector(layout.direction).normalize()
|
|
965
|
+
const right = new THREE.Vector3(tangent.z, 0, -tangent.x).normalize()
|
|
966
|
+
const up = new THREE.Vector3().crossVectors(tangent, right).normalize()
|
|
967
|
+
const paddleDepth = new THREE.Vector3().crossVectors(tangent, up).normalize()
|
|
968
|
+
const paddleRotation = new THREE.Quaternion().setFromRotationMatrix(
|
|
969
|
+
new THREE.Matrix4().makeBasis(tangent, up, paddleDepth),
|
|
970
|
+
)
|
|
971
|
+
const hubRotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), up)
|
|
972
|
+
const hinge = vector(mount.anchor).addScaledVector(up, paddleHeight / 2)
|
|
973
|
+
const center = hinge.clone().addScaledVector(tangent, paddleLength / 2)
|
|
974
|
+
const paddle = createFlipperPaddleVisual(
|
|
975
|
+
`${pieceId}-flipper-${index + 1}`,
|
|
976
|
+
flipperPrimary,
|
|
977
|
+
CANONICAL_FLIPPER_DIMENSIONS,
|
|
978
|
+
)
|
|
979
|
+
paddle.position.copy(center)
|
|
980
|
+
paddle.quaternion.copy(paddleRotation)
|
|
981
|
+
paddle.userData.pieceInstanceId = pieceId
|
|
982
|
+
markDynamicTrackShadowRoot(paddle)
|
|
983
|
+
const paddleSurface = paddle.getObjectByName(`${pieceId}-flipper-${index + 1}-surface`)
|
|
984
|
+
if (!(paddleSurface instanceof THREE.Mesh)) throw new Error('Canonical flipper paddle surface is missing')
|
|
985
|
+
materials.bind(paddleSurface, 'mechanism.flipper.primary')
|
|
986
|
+
const hub = createFlipperHubVisual(
|
|
987
|
+
`${pieceId}-flipper-hub-${index + 1}`,
|
|
988
|
+
flipperSecondary,
|
|
989
|
+
CANONICAL_FLIPPER_DIMENSIONS,
|
|
990
|
+
)
|
|
991
|
+
hub.position.copy(hinge)
|
|
992
|
+
hub.quaternion.copy(hubRotation)
|
|
993
|
+
hub.userData.pieceInstanceId = pieceId
|
|
994
|
+
materials.bind(hub, 'mechanism.flipper.secondary')
|
|
995
|
+
root.add(paddle, hub)
|
|
996
|
+
})
|
|
997
|
+
return
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (template.runtimeKind === 'conveyor') {
|
|
1001
|
+
addConveyorPreview(root, materials, pieceId, addMesh)
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (template.runtimeKind === 'portal-entry-preview' || template.runtimeKind === 'portal-exit-preview') {
|
|
1006
|
+
const isEntry = template.runtimeKind === 'portal-entry-preview'
|
|
1007
|
+
addRailPreview(root, materials, pieceId, {
|
|
1008
|
+
id: `${pieceId}-deck`,
|
|
1009
|
+
label: isEntry ? 'Portal entry channel' : 'Portal exit channel',
|
|
1010
|
+
type: 'rail',
|
|
1011
|
+
points: isEntry
|
|
1012
|
+
? [
|
|
1013
|
+
[0, 0, TRACK_MODULE_LENGTH / 2],
|
|
1014
|
+
[0, 0, 0],
|
|
1015
|
+
]
|
|
1016
|
+
: [
|
|
1017
|
+
[0, 0, 0],
|
|
1018
|
+
[0, 0, -TRACK_MODULE_LENGTH / 2],
|
|
1019
|
+
],
|
|
1020
|
+
width: 4,
|
|
1021
|
+
troughDepth: defaultTrackTroughDepth(4),
|
|
1022
|
+
troughProfile: 'full',
|
|
1023
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
1024
|
+
floorFriction: 0.04,
|
|
1025
|
+
})
|
|
1026
|
+
const color = portalPairColor(typeof parameters.pairId === 'string' ? parameters.pairId : '1')
|
|
1027
|
+
const activeThemeId = materials.activeTheme.id
|
|
1028
|
+
const endpoint = createPortalEndpointVisual(
|
|
1029
|
+
`${pieceId}-portal`,
|
|
1030
|
+
portalVisualStyleFromId(appearance?.portalVisualId),
|
|
1031
|
+
color,
|
|
1032
|
+
{
|
|
1033
|
+
activeThemeId,
|
|
1034
|
+
structure: materials.get('structure.primary'),
|
|
1035
|
+
timber: materials.getAppearance('wild-west.mine-timber', 'structure.primary'),
|
|
1036
|
+
metal: materials.getAppearance(WILD_WEST_GUNMETAL_FLIPPER_MATERIAL_ID, 'structure.secondary'),
|
|
1037
|
+
portal: materials.get('effect.portal'),
|
|
1038
|
+
energy: materials.get('effect.energy'),
|
|
1039
|
+
jungle:
|
|
1040
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1041
|
+
? {
|
|
1042
|
+
stone: materials.getAppearance(JUNGLE_TEMPLE_STONE_MATERIAL_ID, 'lane.guard'),
|
|
1043
|
+
facade: materials.getAppearance(JUNGLE_TEMPLE_FACADE_MATERIAL_ID, 'lane.guard'),
|
|
1044
|
+
bronze: materials.getAppearance(JUNGLE_TEMPLE_BRONZE_MATERIAL_ID, 'signal.route'),
|
|
1045
|
+
foliage: materials.get('world.foliage'),
|
|
1046
|
+
}
|
|
1047
|
+
: undefined,
|
|
1048
|
+
egypt:
|
|
1049
|
+
activeThemeId === EGYPT_ELEVATOR_THEME_ID
|
|
1050
|
+
? {
|
|
1051
|
+
sandstone: materials.getAppearance(ANCIENT_EGYPT_PORTAL_SANDSTONE_ID, 'structure.primary'),
|
|
1052
|
+
carvedStone: materials.getAppearance(ANCIENT_EGYPT_PORTAL_CARVED_SANDSTONE_ID, 'lane.guard'),
|
|
1053
|
+
agedGold: materials.getAppearance(ANCIENT_EGYPT_PORTAL_AGED_GOLD_ID, 'signal.route'),
|
|
1054
|
+
}
|
|
1055
|
+
: undefined,
|
|
1056
|
+
sakura:
|
|
1057
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1058
|
+
? {
|
|
1059
|
+
vermilion: materials.getAppearance(JAPANESE_SAKURA_BRIDGE_TIMBER_ID, 'structure.primary'),
|
|
1060
|
+
plaster: materials.get('lane.guard.backing'),
|
|
1061
|
+
foundationStone: materials.getAppearance(
|
|
1062
|
+
JAPANESE_SAKURA_PORTAL_FOUNDATION_STONE_ID,
|
|
1063
|
+
'structure.secondary',
|
|
1064
|
+
),
|
|
1065
|
+
kawara: materials.getAppearance(JAPANESE_SAKURA_CASTLE_KAWARA_ID, 'structure.secondary'),
|
|
1066
|
+
hardware: materials.getAppearance(JAPANESE_SAKURA_BRIDGE_HARDWARE_ID, 'structure.secondary'),
|
|
1067
|
+
}
|
|
1068
|
+
: undefined,
|
|
1069
|
+
spaceStation:
|
|
1070
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1071
|
+
? {
|
|
1072
|
+
chassis: materials.getAppearance(SPACE_STATION_ARMORED_WALL_ID, 'lane.guard'),
|
|
1073
|
+
panel: materials.getAppearance(SPACE_STATION_WALL_PANEL_ID, 'lane.guard'),
|
|
1074
|
+
rib: materials.getAppearance(SPACE_STATION_WALL_RIB_ID, 'structure.primary'),
|
|
1075
|
+
trim: materials.getAppearance(SPACE_STATION_WALL_TRIM_ID, 'structure.secondary'),
|
|
1076
|
+
light: materials.getAppearance(SPACE_STATION_WALL_LIGHT_ID, 'effect.energy'),
|
|
1077
|
+
}
|
|
1078
|
+
: undefined,
|
|
1079
|
+
},
|
|
1080
|
+
)
|
|
1081
|
+
endpoint.group.position.y = 1.8
|
|
1082
|
+
if (!isEntry) endpoint.group.rotation.y = Math.PI
|
|
1083
|
+
const requestedPairId = typeof parameters.pairId === 'string' ? parameters.pairId : '1'
|
|
1084
|
+
const pairNumber = portalPairNumber(requestedPairId)
|
|
1085
|
+
const portalPreview: NonNullable<TrackPieceVisual['portalPreview']> = {
|
|
1086
|
+
color,
|
|
1087
|
+
endpoint,
|
|
1088
|
+
// Runtime portal ids use the canonical numeric pair id (A -> 1,
|
|
1089
|
+
// B -> 2, ...). Keep the editor preview on the same identity so the
|
|
1090
|
+
// race handoff leases these exact endpoint visuals instead of building
|
|
1091
|
+
// an overlapping second portal pair.
|
|
1092
|
+
pairId: pairNumber === Number.MAX_SAFE_INTEGER ? requestedPairId : String(pairNumber),
|
|
1093
|
+
role: isEntry ? 'entry' : 'exit',
|
|
1094
|
+
}
|
|
1095
|
+
endpoint.group.traverse(object => {
|
|
1096
|
+
object.userData.pieceInstanceId = pieceId
|
|
1097
|
+
})
|
|
1098
|
+
root.add(endpoint.group)
|
|
1099
|
+
materials.captureBindings(endpoint.group)
|
|
1100
|
+
return portalPreview
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const bounds = template.bounds(parameters)
|
|
1104
|
+
const width = bounds.maximum[0] - bounds.minimum[0]
|
|
1105
|
+
const height = Math.max(0.8, bounds.maximum[1] - bounds.minimum[1])
|
|
1106
|
+
const length = bounds.maximum[2] - bounds.minimum[2]
|
|
1107
|
+
addMesh(new THREE.BoxGeometry(width, 0.35, length), 'lane.surface', 'mechanism-deck')
|
|
1108
|
+
addMesh(
|
|
1109
|
+
new VisualCylinderGeometry(Math.min(width, length) * 0.28, Math.min(width, length) * 0.28, height * 0.55, 24),
|
|
1110
|
+
'mechanism.body',
|
|
1111
|
+
'mechanism-body',
|
|
1112
|
+
mesh => {
|
|
1113
|
+
mesh.position.y = height * 0.3
|
|
1114
|
+
},
|
|
1115
|
+
)
|
|
1116
|
+
addMesh(new THREE.BoxGeometry(width * 0.72, 0.24, 0.45), 'mechanism.moving', 'moving-element', mesh => {
|
|
1117
|
+
mesh.position.y = Math.max(0.8, height * 0.58)
|
|
1118
|
+
})
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function addConveyorPreview(
|
|
1122
|
+
root: THREE.Group,
|
|
1123
|
+
materials: ThemeMaterialRegistry,
|
|
1124
|
+
pieceId: string,
|
|
1125
|
+
addMesh: AddMesh,
|
|
1126
|
+
): void {
|
|
1127
|
+
addRailPreview(root, materials, pieceId, {
|
|
1128
|
+
id: `${pieceId}-approach`,
|
|
1129
|
+
label: 'Conveyor approach channel',
|
|
1130
|
+
type: 'rail',
|
|
1131
|
+
points: [TRACK_LAB_CONVEYOR_INPUT, TRACK_LAB_CONVEYOR_LOADING],
|
|
1132
|
+
startWidth: 4,
|
|
1133
|
+
width: TRACK_LAB_CONVEYOR_WIDTH,
|
|
1134
|
+
startTroughDepth: defaultTrackTroughDepth(4),
|
|
1135
|
+
troughDepth: defaultTrackTroughDepth(TRACK_LAB_CONVEYOR_WIDTH),
|
|
1136
|
+
troughProfile: 'full',
|
|
1137
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
1138
|
+
floorFriction: 0.08,
|
|
1139
|
+
})
|
|
1140
|
+
addRailPreview(root, materials, pieceId, {
|
|
1141
|
+
id: `${pieceId}-release`,
|
|
1142
|
+
label: 'Conveyor release channel',
|
|
1143
|
+
type: 'rail',
|
|
1144
|
+
points: [TRACK_LAB_CONVEYOR_RELEASE, [0, 6, -2.5]],
|
|
1145
|
+
startWidth: TRACK_LAB_CONVEYOR_WIDTH,
|
|
1146
|
+
width: 4,
|
|
1147
|
+
startTroughDepth: defaultTrackTroughDepth(TRACK_LAB_CONVEYOR_WIDTH),
|
|
1148
|
+
troughDepth: defaultTrackTroughDepth(4),
|
|
1149
|
+
troughProfile: 'full',
|
|
1150
|
+
wallHeight: TRACK_LAB_WALL_HEIGHT,
|
|
1151
|
+
floorFriction: 0.08,
|
|
1152
|
+
})
|
|
1153
|
+
|
|
1154
|
+
const liftHeight = TRACK_LAB_CONVEYOR_TOP[1] - TRACK_LAB_CONVEYOR_BOTTOM[1]
|
|
1155
|
+
const liftMiddleY = (TRACK_LAB_CONVEYOR_TOP[1] + TRACK_LAB_CONVEYOR_BOTTOM[1]) / 2
|
|
1156
|
+
const liftZ = TRACK_LAB_CONVEYOR_BOTTOM[2]
|
|
1157
|
+
const activeThemeId = materials.activeTheme.id
|
|
1158
|
+
const mineLiftMaterials = {
|
|
1159
|
+
timber:
|
|
1160
|
+
activeThemeId === WILD_WEST_THEME_ID
|
|
1161
|
+
? materials.getAppearance('wild-west.mine-timber', 'structure.primary')
|
|
1162
|
+
: materials.get('structure.primary'),
|
|
1163
|
+
iron:
|
|
1164
|
+
activeThemeId === WILD_WEST_THEME_ID
|
|
1165
|
+
? materials.getAppearance('wild-west.railroad-iron', 'lane.edge')
|
|
1166
|
+
: materials.get('lane.edge'),
|
|
1167
|
+
signal: materials.get('signal.route'),
|
|
1168
|
+
}
|
|
1169
|
+
const mineLiftFrame =
|
|
1170
|
+
activeThemeId === WILD_WEST_THEME_ID
|
|
1171
|
+
? addWildWestMineLiftFrame(root, {
|
|
1172
|
+
id: pieceId,
|
|
1173
|
+
bottom: vector(TRACK_LAB_CONVEYOR_BOTTOM),
|
|
1174
|
+
rotation: new THREE.Quaternion(),
|
|
1175
|
+
width: TRACK_LAB_CONVEYOR_WIDTH,
|
|
1176
|
+
height: liftHeight,
|
|
1177
|
+
depth: TRACK_LAB_CONVEYOR_SHELF_DEPTH,
|
|
1178
|
+
releaseHeight: TRACK_LAB_CONVEYOR_RELEASE[1] - TRACK_LAB_CONVEYOR_BOTTOM[1],
|
|
1179
|
+
materials: mineLiftMaterials,
|
|
1180
|
+
})
|
|
1181
|
+
: undefined
|
|
1182
|
+
if (mineLiftFrame) materials.captureBindings(mineLiftFrame)
|
|
1183
|
+
const jungleTempleMaterials = {
|
|
1184
|
+
stone:
|
|
1185
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1186
|
+
? materials.getAppearance(JUNGLE_TEMPLE_STONE_MATERIAL_ID, 'lane.guard')
|
|
1187
|
+
: materials.get('lane.guard'),
|
|
1188
|
+
facade:
|
|
1189
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1190
|
+
? materials.getAppearance(JUNGLE_TEMPLE_FACADE_MATERIAL_ID, 'lane.guard')
|
|
1191
|
+
: materials.get('lane.guard'),
|
|
1192
|
+
timber:
|
|
1193
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1194
|
+
? materials.getAppearance(JUNGLE_TEMPLE_TIMBER_MATERIAL_ID, 'structure.primary')
|
|
1195
|
+
: materials.get('structure.primary'),
|
|
1196
|
+
platform:
|
|
1197
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1198
|
+
? materials.getAppearance(JUNGLE_TEMPLE_PLATFORM_MATERIAL_ID, 'mechanism.moving')
|
|
1199
|
+
: materials.get('mechanism.moving'),
|
|
1200
|
+
moss: materials.get('world.foliage'),
|
|
1201
|
+
accent:
|
|
1202
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1203
|
+
? materials.getAppearance(JUNGLE_TEMPLE_BRONZE_MATERIAL_ID, 'signal.route')
|
|
1204
|
+
: materials.get('signal.route'),
|
|
1205
|
+
}
|
|
1206
|
+
const jungleTempleSkin =
|
|
1207
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1208
|
+
? addJungleTempleLiftSkin(root, {
|
|
1209
|
+
id: pieceId,
|
|
1210
|
+
bottom: vector(TRACK_LAB_CONVEYOR_BOTTOM),
|
|
1211
|
+
rotation: new THREE.Quaternion(),
|
|
1212
|
+
width: TRACK_LAB_CONVEYOR_WIDTH,
|
|
1213
|
+
height: liftHeight,
|
|
1214
|
+
depth: TRACK_LAB_CONVEYOR_SHELF_DEPTH,
|
|
1215
|
+
releaseHeight: TRACK_LAB_CONVEYOR_RELEASE[1] - TRACK_LAB_CONVEYOR_BOTTOM[1],
|
|
1216
|
+
materials: jungleTempleMaterials,
|
|
1217
|
+
})
|
|
1218
|
+
: undefined
|
|
1219
|
+
if (jungleTempleSkin) materials.captureBindings(jungleTempleSkin)
|
|
1220
|
+
const portalDerivedMaterials = {
|
|
1221
|
+
egypt: {
|
|
1222
|
+
sandstone:
|
|
1223
|
+
activeThemeId === EGYPT_ELEVATOR_THEME_ID
|
|
1224
|
+
? materials.getAppearance(ANCIENT_EGYPT_PORTAL_SANDSTONE_ID, 'structure.primary')
|
|
1225
|
+
: materials.get('structure.primary'),
|
|
1226
|
+
carvedStone:
|
|
1227
|
+
activeThemeId === EGYPT_ELEVATOR_THEME_ID
|
|
1228
|
+
? materials.getAppearance(ANCIENT_EGYPT_PORTAL_CARVED_SANDSTONE_ID, 'lane.guard')
|
|
1229
|
+
: materials.get('lane.guard'),
|
|
1230
|
+
pyramidStone:
|
|
1231
|
+
activeThemeId === EGYPT_ELEVATOR_THEME_ID
|
|
1232
|
+
? materials.getAppearance(ANCIENT_EGYPT_PYRAMID_LIMESTONE_ID, 'structure.primary')
|
|
1233
|
+
: materials.get('structure.primary'),
|
|
1234
|
+
pyramidGold: materials.get('obstacle.primary'),
|
|
1235
|
+
bronze: materials.get('lane.edge'),
|
|
1236
|
+
gold:
|
|
1237
|
+
activeThemeId === EGYPT_ELEVATOR_THEME_ID
|
|
1238
|
+
? materials.getAppearance(ANCIENT_EGYPT_PORTAL_AGED_GOLD_ID, 'signal.route')
|
|
1239
|
+
: materials.get('signal.route'),
|
|
1240
|
+
paving: materials.get('lane.surface'),
|
|
1241
|
+
signal: materials.get('signal.route'),
|
|
1242
|
+
},
|
|
1243
|
+
japan: {
|
|
1244
|
+
vermilion:
|
|
1245
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1246
|
+
? materials.getAppearance(JAPANESE_SAKURA_BRIDGE_TIMBER_ID, 'structure.primary')
|
|
1247
|
+
: materials.get('structure.primary'),
|
|
1248
|
+
timber:
|
|
1249
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1250
|
+
? materials.getAppearance(JAPANESE_SAKURA_CASTLE_TIMBER_ID, 'structure.primary')
|
|
1251
|
+
: materials.get('structure.primary'),
|
|
1252
|
+
plaster: materials.get('lane.guard.backing'),
|
|
1253
|
+
stone:
|
|
1254
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1255
|
+
? materials.getAppearance(JAPANESE_SAKURA_PORTAL_FOUNDATION_STONE_ID, 'structure.secondary')
|
|
1256
|
+
: materials.get('structure.secondary'),
|
|
1257
|
+
kawara:
|
|
1258
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1259
|
+
? materials.getAppearance(JAPANESE_SAKURA_CASTLE_KAWARA_ID, 'structure.secondary')
|
|
1260
|
+
: materials.get('structure.secondary'),
|
|
1261
|
+
hardware:
|
|
1262
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1263
|
+
? materials.getAppearance(JAPANESE_SAKURA_BRIDGE_HARDWARE_ID, 'structure.secondary')
|
|
1264
|
+
: materials.get('structure.secondary'),
|
|
1265
|
+
signal: materials.get('signal.route'),
|
|
1266
|
+
},
|
|
1267
|
+
space: {
|
|
1268
|
+
chassis:
|
|
1269
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1270
|
+
? materials.getAppearance(SPACE_STATION_ARMORED_WALL_ID, 'lane.guard')
|
|
1271
|
+
: materials.get('lane.guard'),
|
|
1272
|
+
panel:
|
|
1273
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1274
|
+
? materials.getAppearance(SPACE_STATION_WALL_PANEL_ID, 'lane.guard')
|
|
1275
|
+
: materials.get('lane.guard'),
|
|
1276
|
+
rib:
|
|
1277
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1278
|
+
? materials.getAppearance(SPACE_STATION_WALL_RIB_ID, 'structure.primary')
|
|
1279
|
+
: materials.get('structure.primary'),
|
|
1280
|
+
trim:
|
|
1281
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1282
|
+
? materials.getAppearance(SPACE_STATION_WALL_TRIM_ID, 'structure.secondary')
|
|
1283
|
+
: materials.get('structure.secondary'),
|
|
1284
|
+
light:
|
|
1285
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1286
|
+
? materials.getAppearance(SPACE_STATION_WALL_LIGHT_ID, 'effect.energy')
|
|
1287
|
+
: materials.get('effect.energy'),
|
|
1288
|
+
},
|
|
1289
|
+
}
|
|
1290
|
+
const portalDerivedSkins = addPortalDerivedElevatorSkins(root, {
|
|
1291
|
+
activeThemeId,
|
|
1292
|
+
id: pieceId,
|
|
1293
|
+
bottom: vector(TRACK_LAB_CONVEYOR_BOTTOM),
|
|
1294
|
+
rotation: new THREE.Quaternion(),
|
|
1295
|
+
width: TRACK_LAB_CONVEYOR_WIDTH,
|
|
1296
|
+
height: liftHeight,
|
|
1297
|
+
depth: TRACK_LAB_CONVEYOR_SHELF_DEPTH,
|
|
1298
|
+
releaseHeight: TRACK_LAB_CONVEYOR_RELEASE[1] - TRACK_LAB_CONVEYOR_BOTTOM[1],
|
|
1299
|
+
materials: portalDerivedMaterials,
|
|
1300
|
+
})
|
|
1301
|
+
portalDerivedSkins.forEach(skin => materials.captureBindings(skin))
|
|
1302
|
+
const themedElevatorIds = [
|
|
1303
|
+
JUNGLE_TEMPLE_THEME_ID,
|
|
1304
|
+
EGYPT_ELEVATOR_THEME_ID,
|
|
1305
|
+
JAPAN_ELEVATOR_THEME_ID,
|
|
1306
|
+
SPACE_ELEVATOR_THEME_ID,
|
|
1307
|
+
]
|
|
1308
|
+
for (const side of [-1, 1] as const) {
|
|
1309
|
+
addMesh(
|
|
1310
|
+
new THREE.BoxGeometry(0.24, liftHeight + 1, TRACK_LAB_CONVEYOR_SHELF_DEPTH + 0.16),
|
|
1311
|
+
'lane.guard',
|
|
1312
|
+
`conveyor-wall-${side}`,
|
|
1313
|
+
mesh => {
|
|
1314
|
+
mesh.position.set(side * (TRACK_LAB_CONVEYOR_WIDTH / 2 - 0.12), liftMiddleY, liftZ)
|
|
1315
|
+
mesh.userData.themeHidden = themedElevatorIds
|
|
1316
|
+
},
|
|
1317
|
+
JUNGLE_TEMPLE_STONE_MATERIAL_ID,
|
|
1318
|
+
)
|
|
1319
|
+
addMesh(
|
|
1320
|
+
new THREE.BoxGeometry(0.38, liftHeight, 0.34),
|
|
1321
|
+
'mechanism.body',
|
|
1322
|
+
`conveyor-belt-${side}`,
|
|
1323
|
+
mesh => {
|
|
1324
|
+
mesh.position.set(side * (TRACK_LAB_CONVEYOR_WIDTH / 2 - 0.58), liftMiddleY, liftZ + 1.7)
|
|
1325
|
+
mesh.userData.themeHidden = themedElevatorIds
|
|
1326
|
+
},
|
|
1327
|
+
JUNGLE_TEMPLE_TIMBER_MATERIAL_ID,
|
|
1328
|
+
)
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
for (const y of [TRACK_LAB_CONVEYOR_BOTTOM[1] - 0.5, TRACK_LAB_CONVEYOR_TOP[1] + 0.5]) {
|
|
1332
|
+
const drum = addMesh(
|
|
1333
|
+
new VisualCylinderGeometry(0.56, 0.56, TRACK_LAB_CONVEYOR_WIDTH - 0.7, 20),
|
|
1334
|
+
'signal.route',
|
|
1335
|
+
`conveyor-drum-${y}`,
|
|
1336
|
+
mesh => {
|
|
1337
|
+
mesh.position.set(0, y, liftZ)
|
|
1338
|
+
mesh.rotation.z = Math.PI / 2
|
|
1339
|
+
mesh.userData.decorative = true
|
|
1340
|
+
mesh.userData.themeHidden = [WILD_WEST_THEME_ID, ...themedElevatorIds]
|
|
1341
|
+
},
|
|
1342
|
+
JUNGLE_TEMPLE_TIMBER_MATERIAL_ID,
|
|
1343
|
+
)
|
|
1344
|
+
materials.captureBindings(drum)
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
for (let index = 0; index < TRACK_LAB_CONVEYOR_SHELF_COUNT; index += 1) {
|
|
1348
|
+
const progress = (index + 0.5) / TRACK_LAB_CONVEYOR_SHELF_COUNT
|
|
1349
|
+
const shelf = addMesh(
|
|
1350
|
+
new THREE.BoxGeometry(TRACK_LAB_CONVEYOR_WIDTH - 0.56, 0.2, TRACK_LAB_CONVEYOR_SHELF_DEPTH),
|
|
1351
|
+
'mechanism.moving',
|
|
1352
|
+
`conveyor-shelf-${index + 1}`,
|
|
1353
|
+
mesh => {
|
|
1354
|
+
const shelfHeight = THREE.MathUtils.lerp(TRACK_LAB_CONVEYOR_BOTTOM[1], TRACK_LAB_CONVEYOR_TOP[1], progress)
|
|
1355
|
+
const tiltTransition = THREE.MathUtils.smoothstep(shelfHeight - TRACK_LAB_CONVEYOR_BOTTOM[1], 0.45, 3.2)
|
|
1356
|
+
mesh.position.set(0, shelfHeight, liftZ)
|
|
1357
|
+
mesh.rotation.x = THREE.MathUtils.lerp(
|
|
1358
|
+
TRACK_LAB_CONVEYOR_SHELF_TILT_RADIANS,
|
|
1359
|
+
-TRACK_LAB_CONVEYOR_SHELF_TILT_RADIANS,
|
|
1360
|
+
tiltTransition,
|
|
1361
|
+
)
|
|
1362
|
+
},
|
|
1363
|
+
JUNGLE_TEMPLE_PLATFORM_MATERIAL_ID,
|
|
1364
|
+
)
|
|
1365
|
+
markDynamicTrackShadowRoot(shelf)
|
|
1366
|
+
if (activeThemeId === WILD_WEST_THEME_ID)
|
|
1367
|
+
decorateWildWestMineLiftShelf(
|
|
1368
|
+
shelf,
|
|
1369
|
+
`${pieceId}-conveyor-shelf-${index + 1}`,
|
|
1370
|
+
TRACK_LAB_CONVEYOR_WIDTH - 0.56,
|
|
1371
|
+
TRACK_LAB_CONVEYOR_SHELF_DEPTH,
|
|
1372
|
+
mineLiftMaterials,
|
|
1373
|
+
)
|
|
1374
|
+
if (activeThemeId === JUNGLE_TEMPLE_THEME_ID)
|
|
1375
|
+
decorateJungleTempleShelf(
|
|
1376
|
+
shelf,
|
|
1377
|
+
`${pieceId}-conveyor-shelf-${index + 1}`,
|
|
1378
|
+
TRACK_LAB_CONVEYOR_WIDTH - 0.56,
|
|
1379
|
+
TRACK_LAB_CONVEYOR_SHELF_DEPTH,
|
|
1380
|
+
jungleTempleMaterials,
|
|
1381
|
+
)
|
|
1382
|
+
decoratePortalDerivedElevatorShelf(
|
|
1383
|
+
shelf,
|
|
1384
|
+
`${pieceId}-conveyor-shelf-${index + 1}`,
|
|
1385
|
+
TRACK_LAB_CONVEYOR_WIDTH - 0.56,
|
|
1386
|
+
TRACK_LAB_CONVEYOR_SHELF_DEPTH,
|
|
1387
|
+
portalDerivedMaterials,
|
|
1388
|
+
activeThemeId,
|
|
1389
|
+
)
|
|
1390
|
+
materials.captureBindings(shelf)
|
|
1391
|
+
}
|
|
1392
|
+
// Theme visibility metadata is assigned by the mesh configurators after the
|
|
1393
|
+
// initial slot binding, so recapture the completed lift construction once.
|
|
1394
|
+
materials.captureBindings(root)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function lengthUnitsFromParameters(parameters: JsonObject): number {
|
|
1398
|
+
const value = parameters.lengthUnits
|
|
1399
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 1
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function gradeFromParameters(parameters: JsonObject): 'flat' | 'slope' | 'steep' {
|
|
1403
|
+
return parameters.grade === 'flat' || parameters.grade === 'steep' ? parameters.grade : 'slope'
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function conePatternFromParameters(parameters: JsonObject): 'classic' | 'dense' {
|
|
1407
|
+
return parameters.pattern === 'classic' ? 'classic' : 'dense'
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function reboundPatternFromParameters(parameters: JsonObject): ReboundPattern {
|
|
1411
|
+
return parameters.pattern === 'zigzag' ? 'zigzag' : 'alternating'
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function risingWallDensityFromParameters(parameters: JsonObject): RisingWallDensity {
|
|
1415
|
+
return parameters.density === 'sparse' ? 'sparse' : 'dense'
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function vector(value: TrackVector): THREE.Vector3 {
|
|
1419
|
+
return new THREE.Vector3(...value)
|
|
1420
|
+
}
|