@tonybfox/threejs-tools 1.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/README.md +321 -0
- package/dist/asset-loader/index.cjs +376 -0
- package/dist/asset-loader/index.cjs.map +1 -0
- package/dist/asset-loader/index.d.mts +101 -0
- package/dist/asset-loader/index.d.ts +101 -0
- package/dist/asset-loader/index.mjs +7 -0
- package/dist/asset-loader/index.mjs.map +1 -0
- package/dist/camera/index.cjs +313 -0
- package/dist/camera/index.cjs.map +1 -0
- package/dist/camera/index.d.mts +82 -0
- package/dist/camera/index.d.ts +82 -0
- package/dist/camera/index.mjs +7 -0
- package/dist/camera/index.mjs.map +1 -0
- package/dist/chunk-5DP6WDB3.mjs +1161 -0
- package/dist/chunk-5DP6WDB3.mjs.map +1 -0
- package/dist/chunk-BJKSICFA.mjs +1579 -0
- package/dist/chunk-BJKSICFA.mjs.map +1 -0
- package/dist/chunk-BYRZCHE7.mjs +277 -0
- package/dist/chunk-BYRZCHE7.mjs.map +1 -0
- package/dist/chunk-EIROAPF7.mjs +387 -0
- package/dist/chunk-EIROAPF7.mjs.map +1 -0
- package/dist/chunk-EQDOX34V.mjs +164 -0
- package/dist/chunk-EQDOX34V.mjs.map +1 -0
- package/dist/chunk-IIAZ2WJJ.mjs +405 -0
- package/dist/chunk-IIAZ2WJJ.mjs.map +1 -0
- package/dist/chunk-L4VIIJZD.mjs +340 -0
- package/dist/chunk-L4VIIJZD.mjs.map +1 -0
- package/dist/chunk-P35QJCOG.mjs +339 -0
- package/dist/chunk-P35QJCOG.mjs.map +1 -0
- package/dist/chunk-R64RVBRM.mjs +394 -0
- package/dist/chunk-R64RVBRM.mjs.map +1 -0
- package/dist/compass/index.cjs +375 -0
- package/dist/compass/index.cjs.map +1 -0
- package/dist/compass/index.d.mts +58 -0
- package/dist/compass/index.d.ts +58 -0
- package/dist/compass/index.mjs +7 -0
- package/dist/compass/index.mjs.map +1 -0
- package/dist/grid/index.cjs +200 -0
- package/dist/grid/index.cjs.map +1 -0
- package/dist/grid/index.d.mts +43 -0
- package/dist/grid/index.d.ts +43 -0
- package/dist/grid/index.mjs +7 -0
- package/dist/grid/index.mjs.map +1 -0
- package/dist/index.cjs +5049 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +47 -0
- package/dist/index.mjs.map +1 -0
- package/dist/measurements/index.cjs +1198 -0
- package/dist/measurements/index.cjs.map +1 -0
- package/dist/measurements/index.d.mts +449 -0
- package/dist/measurements/index.d.ts +449 -0
- package/dist/measurements/index.mjs +9 -0
- package/dist/measurements/index.mjs.map +1 -0
- package/dist/sunlight/index.cjs +441 -0
- package/dist/sunlight/index.cjs.map +1 -0
- package/dist/sunlight/index.d.mts +92 -0
- package/dist/sunlight/index.d.ts +92 -0
- package/dist/sunlight/index.mjs +7 -0
- package/dist/sunlight/index.mjs.map +1 -0
- package/dist/terrain/index.cjs +423 -0
- package/dist/terrain/index.cjs.map +1 -0
- package/dist/terrain/index.d.mts +219 -0
- package/dist/terrain/index.d.ts +219 -0
- package/dist/terrain/index.mjs +7 -0
- package/dist/terrain/index.mjs.map +1 -0
- package/dist/transform-controls/index.cjs +1587 -0
- package/dist/transform-controls/index.cjs.map +1 -0
- package/dist/transform-controls/index.d.mts +162 -0
- package/dist/transform-controls/index.d.ts +162 -0
- package/dist/transform-controls/index.mjs +13 -0
- package/dist/transform-controls/index.mjs.map +1 -0
- package/dist/view-helper/index.cjs +430 -0
- package/dist/view-helper/index.cjs.map +1 -0
- package/dist/view-helper/index.d.mts +75 -0
- package/dist/view-helper/index.d.ts +75 -0
- package/dist/view-helper/index.mjs +7 -0
- package/dist/view-helper/index.mjs.map +1 -0
- package/package.json +124 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// packages/terrain/src/TerrainTool.ts
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
var TerrainTool = class extends THREE.EventDispatcher {
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new TerrainTool instance
|
|
6
|
+
* @param scene - The Three.js scene to add terrain to
|
|
7
|
+
* @param options - Configuration options
|
|
8
|
+
*/
|
|
9
|
+
constructor(scene, options = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.mesh = null;
|
|
12
|
+
this.currentData = null;
|
|
13
|
+
this.isLoading = false;
|
|
14
|
+
this.generatedTextureUrls = /* @__PURE__ */ new Set();
|
|
15
|
+
this.scene = scene;
|
|
16
|
+
this.widthSegments = options.widthSegments ?? 50;
|
|
17
|
+
this.depthSegments = options.depthSegments ?? 50;
|
|
18
|
+
this.elevationScale = options.elevationScale ?? 1;
|
|
19
|
+
this.baseColor = options.baseColor ?? 9139029;
|
|
20
|
+
this.wireframe = options.wireframe ?? false;
|
|
21
|
+
this.textureUrl = options.textureUrl;
|
|
22
|
+
this.mapboxOptions = options.mapbox;
|
|
23
|
+
this.receiveShadow = options.receiveShadow ?? true;
|
|
24
|
+
this.castShadow = options.castShadow ?? true;
|
|
25
|
+
this.useDemoData = options.useDemoData ?? false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Update Mapbox imagery configuration at runtime
|
|
29
|
+
*/
|
|
30
|
+
setMapboxOptions(options) {
|
|
31
|
+
this.mapboxOptions = options;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Loads terrain data and creates a mesh
|
|
35
|
+
* @param center - Center coordinates (latitude, longitude)
|
|
36
|
+
* @param dimensions - Terrain dimensions in meters
|
|
37
|
+
*/
|
|
38
|
+
async loadTerrain(center, dimensions) {
|
|
39
|
+
if (this.isLoading) {
|
|
40
|
+
console.warn("Terrain is already loading");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.isLoading = true;
|
|
44
|
+
this.dispatchEvent({
|
|
45
|
+
type: "updateStarted",
|
|
46
|
+
center,
|
|
47
|
+
dimensions
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
const terrainData = await this.fetchElevationData(center, dimensions);
|
|
51
|
+
this.currentData = terrainData;
|
|
52
|
+
this.dispatchEvent({
|
|
53
|
+
type: "dataLoaded",
|
|
54
|
+
data: terrainData
|
|
55
|
+
});
|
|
56
|
+
const textureSource = await this.resolveTexture(center, dimensions);
|
|
57
|
+
await this.createMesh(terrainData, textureSource);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Error loading terrain:", error);
|
|
60
|
+
this.dispatchEvent({
|
|
61
|
+
type: "error",
|
|
62
|
+
message: "Failed to load terrain",
|
|
63
|
+
error
|
|
64
|
+
});
|
|
65
|
+
} finally {
|
|
66
|
+
this.isLoading = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Updates the terrain with new coordinates and/or dimensions
|
|
71
|
+
* @param center - New center coordinates (optional, keeps current if not provided)
|
|
72
|
+
* @param dimensions - New dimensions (optional, keeps current if not provided)
|
|
73
|
+
*/
|
|
74
|
+
async updateTerrain(center, dimensions) {
|
|
75
|
+
if (!this.currentData && !center) {
|
|
76
|
+
console.warn("No current terrain data and no center provided");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const newCenter = center || this.currentData.center;
|
|
80
|
+
const newDimensions = dimensions || this.currentData.dimensions;
|
|
81
|
+
await this.loadTerrain(newCenter, newDimensions);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetches elevation data from Open-Elevation API or generates demo data
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
async fetchElevationData(center, dimensions) {
|
|
88
|
+
if (this.useDemoData) {
|
|
89
|
+
return this.generateDemoData(center, dimensions);
|
|
90
|
+
}
|
|
91
|
+
const points = [];
|
|
92
|
+
const metersPerDegreeLat = 111320;
|
|
93
|
+
const metersPerDegreeLon = 111320 * Math.max(Math.abs(Math.cos(center.latitude * Math.PI / 180)), 1e-6);
|
|
94
|
+
const latRange = dimensions.depth / metersPerDegreeLat;
|
|
95
|
+
const lonRange = dimensions.width / metersPerDegreeLon;
|
|
96
|
+
const latStep = latRange / this.depthSegments;
|
|
97
|
+
const lonStep = lonRange / this.widthSegments;
|
|
98
|
+
const startLat = center.latitude + latRange / 2;
|
|
99
|
+
const startLon = center.longitude - lonRange / 2;
|
|
100
|
+
for (let z = 0; z <= this.depthSegments; z++) {
|
|
101
|
+
for (let x = 0; x <= this.widthSegments; x++) {
|
|
102
|
+
points.push({
|
|
103
|
+
latitude: startLat - z * latStep,
|
|
104
|
+
longitude: startLon + x * lonStep
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const response = await fetch(
|
|
109
|
+
"https://api.open-elevation.com/api/v1/lookup",
|
|
110
|
+
{
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json"
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
locations: points
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`Elevation API error: ${response.statusText}`);
|
|
122
|
+
}
|
|
123
|
+
const data = await response.json();
|
|
124
|
+
const results = data.results;
|
|
125
|
+
const elevations = [];
|
|
126
|
+
let minElevation = Infinity;
|
|
127
|
+
let maxElevation = -Infinity;
|
|
128
|
+
for (let z = 0; z <= this.depthSegments; z++) {
|
|
129
|
+
const row = [];
|
|
130
|
+
for (let x = 0; x <= this.widthSegments; x++) {
|
|
131
|
+
const index = z * (this.widthSegments + 1) + x;
|
|
132
|
+
const elevation = results[index].elevation;
|
|
133
|
+
row.push(elevation);
|
|
134
|
+
minElevation = Math.min(minElevation, elevation);
|
|
135
|
+
maxElevation = Math.max(maxElevation, elevation);
|
|
136
|
+
}
|
|
137
|
+
elevations.push(row);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
center,
|
|
141
|
+
dimensions,
|
|
142
|
+
elevations,
|
|
143
|
+
minElevation,
|
|
144
|
+
maxElevation
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Generates demo terrain data using perlin-like noise
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
generateDemoData(center, dimensions) {
|
|
152
|
+
const elevations = [];
|
|
153
|
+
let minElevation = Infinity;
|
|
154
|
+
let maxElevation = -Infinity;
|
|
155
|
+
const seed = center.latitude + center.longitude;
|
|
156
|
+
for (let z = 0; z <= this.depthSegments; z++) {
|
|
157
|
+
const row = [];
|
|
158
|
+
for (let x = 0; x <= this.widthSegments; x++) {
|
|
159
|
+
const nx = x / this.widthSegments * 4;
|
|
160
|
+
const nz = z / this.depthSegments * 4;
|
|
161
|
+
let elevation = 0;
|
|
162
|
+
elevation += Math.sin(nx + seed) * 500;
|
|
163
|
+
elevation += Math.cos(nz + seed) * 500;
|
|
164
|
+
elevation += Math.sin(nx * 2 + seed) * 200;
|
|
165
|
+
elevation += Math.cos(nz * 2 + seed) * 200;
|
|
166
|
+
elevation += Math.sin(nx * 4 + seed) * 50;
|
|
167
|
+
elevation += Math.cos(nz * 4 + seed) * 50;
|
|
168
|
+
elevation += (Math.random() - 0.5) * 30;
|
|
169
|
+
row.push(elevation);
|
|
170
|
+
minElevation = Math.min(minElevation, elevation);
|
|
171
|
+
maxElevation = Math.max(maxElevation, elevation);
|
|
172
|
+
}
|
|
173
|
+
elevations.push(row);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
center,
|
|
177
|
+
dimensions,
|
|
178
|
+
elevations,
|
|
179
|
+
minElevation,
|
|
180
|
+
maxElevation
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Creates a terrain mesh from elevation data
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
async createMesh(data, textureSource) {
|
|
188
|
+
if (this.mesh) {
|
|
189
|
+
this.scene.remove(this.mesh);
|
|
190
|
+
this.mesh.geometry.dispose();
|
|
191
|
+
if (Array.isArray(this.mesh.material)) {
|
|
192
|
+
this.mesh.material.forEach((mat) => mat.dispose());
|
|
193
|
+
} else {
|
|
194
|
+
this.mesh.material.dispose();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const geometry = new THREE.PlaneGeometry(
|
|
198
|
+
data.dimensions.width,
|
|
199
|
+
data.dimensions.depth,
|
|
200
|
+
this.widthSegments,
|
|
201
|
+
this.depthSegments
|
|
202
|
+
);
|
|
203
|
+
const positions = geometry.attributes.position;
|
|
204
|
+
let vertexIndex = 0;
|
|
205
|
+
for (let z = 0; z <= this.depthSegments; z++) {
|
|
206
|
+
for (let x = 0; x <= this.widthSegments; x++) {
|
|
207
|
+
const elevation = data.elevations[z][x];
|
|
208
|
+
const height = (elevation - data.minElevation) * this.elevationScale;
|
|
209
|
+
positions.setZ(vertexIndex, height);
|
|
210
|
+
vertexIndex++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
geometry.computeVertexNormals();
|
|
214
|
+
let material;
|
|
215
|
+
if (textureSource?.url) {
|
|
216
|
+
try {
|
|
217
|
+
const texture = await new THREE.TextureLoader().loadAsync(
|
|
218
|
+
textureSource.url
|
|
219
|
+
);
|
|
220
|
+
material = new THREE.MeshStandardMaterial({
|
|
221
|
+
map: texture,
|
|
222
|
+
wireframe: this.wireframe
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.warn("Failed to load terrain texture, using base color.", error);
|
|
226
|
+
material = new THREE.MeshStandardMaterial({
|
|
227
|
+
color: this.baseColor,
|
|
228
|
+
wireframe: this.wireframe
|
|
229
|
+
});
|
|
230
|
+
} finally {
|
|
231
|
+
if (textureSource.revokeOnUse) {
|
|
232
|
+
this.releaseGeneratedTexture(textureSource.url);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
material = new THREE.MeshStandardMaterial({
|
|
237
|
+
color: this.baseColor,
|
|
238
|
+
wireframe: this.wireframe
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
this.mesh = new THREE.Mesh(geometry, material);
|
|
242
|
+
this.mesh.rotation.x = -Math.PI / 2;
|
|
243
|
+
this.mesh.receiveShadow = this.receiveShadow;
|
|
244
|
+
this.mesh.castShadow = this.castShadow;
|
|
245
|
+
this.scene.add(this.mesh);
|
|
246
|
+
this.dispatchEvent({
|
|
247
|
+
type: "meshLoaded",
|
|
248
|
+
mesh: this.mesh
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async resolveTexture(center, dimensions) {
|
|
252
|
+
if (this.textureUrl) {
|
|
253
|
+
return {
|
|
254
|
+
url: this.textureUrl,
|
|
255
|
+
revokeOnUse: false
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (!this.mapboxOptions) {
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const url = await this.fetchMapboxTexture(
|
|
263
|
+
center,
|
|
264
|
+
dimensions,
|
|
265
|
+
this.mapboxOptions
|
|
266
|
+
);
|
|
267
|
+
this.generatedTextureUrls.add(url);
|
|
268
|
+
return {
|
|
269
|
+
url,
|
|
270
|
+
revokeOnUse: true
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.warn(
|
|
274
|
+
"Failed to fetch Mapbox imagery, falling back to base material.",
|
|
275
|
+
error
|
|
276
|
+
);
|
|
277
|
+
return void 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
computeBoundingBox(center, dimensions, paddingRatio) {
|
|
281
|
+
const metersPerDegreeLat = 111320;
|
|
282
|
+
const latRadians = center.latitude * Math.PI / 180;
|
|
283
|
+
const cosLat = Math.cos(latRadians);
|
|
284
|
+
const metersPerDegreeLon = 111320 * Math.max(Math.abs(cosLat), 1e-6);
|
|
285
|
+
const appliedPadding = Math.max(0, paddingRatio);
|
|
286
|
+
const widthWithPadding = dimensions.width * (1 + appliedPadding);
|
|
287
|
+
const depthWithPadding = dimensions.depth * (1 + appliedPadding);
|
|
288
|
+
const halfDepthDegrees = depthWithPadding / 2 / metersPerDegreeLat;
|
|
289
|
+
const halfWidthDegrees = widthWithPadding / 2 / metersPerDegreeLon;
|
|
290
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
291
|
+
const minLat = clamp(center.latitude - halfDepthDegrees, -90, 90);
|
|
292
|
+
const maxLat = clamp(center.latitude + halfDepthDegrees, -90, 90);
|
|
293
|
+
const minLon = clamp(center.longitude - halfWidthDegrees, -180, 180);
|
|
294
|
+
const maxLon = clamp(center.longitude + halfWidthDegrees, -180, 180);
|
|
295
|
+
return { minLat, maxLat, minLon, maxLon };
|
|
296
|
+
}
|
|
297
|
+
async fetchMapboxTexture(center, dimensions, options) {
|
|
298
|
+
const styleId = options.styleId ?? "mapbox/satellite-v9";
|
|
299
|
+
const normalizedStyleId = styleId.replace("mapbox://styles/", "").replace(/^\/+/, "");
|
|
300
|
+
const width = Math.min(
|
|
301
|
+
1280,
|
|
302
|
+
Math.max(1, Math.floor(options.imageWidth ?? 1024))
|
|
303
|
+
);
|
|
304
|
+
const height = Math.min(
|
|
305
|
+
1280,
|
|
306
|
+
Math.max(1, Math.floor(options.imageHeight ?? 1024))
|
|
307
|
+
);
|
|
308
|
+
const highResSuffix = options.highResolution ? "@2x" : "";
|
|
309
|
+
const format = options.imageFormat ?? "png";
|
|
310
|
+
const paddingRatio = options.paddingRatio ?? 0.1;
|
|
311
|
+
const bounds = this.computeBoundingBox(center, dimensions, paddingRatio);
|
|
312
|
+
const bbox = `${bounds.minLon.toFixed(6)},${bounds.minLat.toFixed(
|
|
313
|
+
6
|
|
314
|
+
)},${bounds.maxLon.toFixed(6)},${bounds.maxLat.toFixed(6)}`;
|
|
315
|
+
const params = new URLSearchParams({
|
|
316
|
+
access_token: options.accessToken,
|
|
317
|
+
format
|
|
318
|
+
});
|
|
319
|
+
const requestUrl = `https://api.mapbox.com/styles/v1/${normalizedStyleId}/static/[${bbox}]/${width}x${height}${highResSuffix}?${params.toString()}`;
|
|
320
|
+
const response = await fetch(requestUrl);
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Mapbox imagery request failed: ${response.status} ${response.statusText}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const blob = await response.blob();
|
|
327
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
|
|
328
|
+
throw new Error(
|
|
329
|
+
"URL.createObjectURL is not available in this environment"
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return URL.createObjectURL(blob);
|
|
333
|
+
}
|
|
334
|
+
releaseGeneratedTexture(url) {
|
|
335
|
+
if (!this.generatedTextureUrls.has(url)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
|
|
339
|
+
URL.revokeObjectURL(url);
|
|
340
|
+
}
|
|
341
|
+
this.generatedTextureUrls.delete(url);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Gets the current terrain mesh
|
|
345
|
+
*/
|
|
346
|
+
getMesh() {
|
|
347
|
+
return this.mesh;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Gets the current terrain data
|
|
351
|
+
*/
|
|
352
|
+
getData() {
|
|
353
|
+
return this.currentData;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Checks if terrain is currently loading
|
|
357
|
+
*/
|
|
358
|
+
isTerrainLoading() {
|
|
359
|
+
return this.isLoading;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Disposes of all resources
|
|
363
|
+
*/
|
|
364
|
+
dispose() {
|
|
365
|
+
if (this.mesh) {
|
|
366
|
+
this.scene.remove(this.mesh);
|
|
367
|
+
this.mesh.geometry.dispose();
|
|
368
|
+
if (Array.isArray(this.mesh.material)) {
|
|
369
|
+
this.mesh.material.forEach((mat) => mat.dispose());
|
|
370
|
+
} else {
|
|
371
|
+
this.mesh.material.dispose();
|
|
372
|
+
}
|
|
373
|
+
this.mesh = null;
|
|
374
|
+
}
|
|
375
|
+
this.currentData = null;
|
|
376
|
+
if (this.generatedTextureUrls.size > 0) {
|
|
377
|
+
for (const url of Array.from(this.generatedTextureUrls)) {
|
|
378
|
+
this.releaseGeneratedTexture(url);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export {
|
|
385
|
+
TerrainTool
|
|
386
|
+
};
|
|
387
|
+
//# sourceMappingURL=chunk-EIROAPF7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../packages/terrain/src/TerrainTool.ts"],"sourcesContent":["import * as THREE from 'three'\nimport {\n GeoCoordinates,\n TerrainDimensions,\n TerrainToolOptions,\n TerrainData,\n TerrainToolEvents,\n ElevationPoint,\n MapboxTextureOptions,\n} from './TerrainTypes'\n\ntype TerrainTextureSource = {\n url: string\n revokeOnUse: boolean\n}\n\n/**\n * Terrain mesh creator for Three.js scenes\n * Fetches elevation data from Open-Elevation API and creates 3D terrain meshes\n *\n * @example\n * ```javascript\n * const terrainTool = new TerrainTool(scene, {\n * widthSegments: 100,\n * depthSegments: 100,\n * elevationScale: 2.0,\n * useDemoData: false // Set to true for demo/testing without API\n * })\n *\n * // Listen for events\n * terrainTool.addEventListener('dataLoaded', (e) => {\n * console.log('Terrain data loaded:', e.data)\n * })\n *\n * terrainTool.addEventListener('meshLoaded', (e) => {\n * console.log('Terrain mesh created:', e.mesh)\n * })\n *\n * // Load terrain for a location\n * terrainTool.loadTerrain(\n * { latitude: 36.1699, longitude: -115.1398 }, // Las Vegas\n * { width: 5000, depth: 5000 } // 5km x 5km\n * )\n * ```\n */\nexport class TerrainTool extends THREE.EventDispatcher<TerrainToolEvents> {\n private scene: THREE.Scene\n private mesh: THREE.Mesh | null = null\n private currentData: TerrainData | null = null\n private isLoading: boolean = false\n private mapboxOptions?: MapboxTextureOptions\n private generatedTextureUrls: Set<string> = new Set()\n\n // Configuration\n public widthSegments: number\n public depthSegments: number\n public elevationScale: number\n public baseColor: number\n public wireframe: boolean\n public textureUrl: string | undefined\n public receiveShadow: boolean\n public castShadow: boolean\n public useDemoData: boolean\n\n /**\n * Creates a new TerrainTool instance\n * @param scene - The Three.js scene to add terrain to\n * @param options - Configuration options\n */\n constructor(scene: THREE.Scene, options: TerrainToolOptions = {}) {\n super()\n this.scene = scene\n\n // Apply options with defaults\n this.widthSegments = options.widthSegments ?? 50\n this.depthSegments = options.depthSegments ?? 50\n this.elevationScale = options.elevationScale ?? 1.0\n this.baseColor = options.baseColor ?? 0x8b7355\n this.wireframe = options.wireframe ?? false\n this.textureUrl = options.textureUrl\n this.mapboxOptions = options.mapbox\n this.receiveShadow = options.receiveShadow ?? true\n this.castShadow = options.castShadow ?? true\n this.useDemoData = options.useDemoData ?? false\n }\n\n /**\n * Update Mapbox imagery configuration at runtime\n */\n setMapboxOptions(options?: MapboxTextureOptions): void {\n this.mapboxOptions = options\n }\n\n /**\n * Loads terrain data and creates a mesh\n * @param center - Center coordinates (latitude, longitude)\n * @param dimensions - Terrain dimensions in meters\n */\n async loadTerrain(\n center: GeoCoordinates,\n dimensions: TerrainDimensions\n ): Promise<void> {\n if (this.isLoading) {\n console.warn('Terrain is already loading')\n return\n }\n\n this.isLoading = true\n this.dispatchEvent({\n type: 'updateStarted',\n center,\n dimensions,\n })\n\n try {\n // Fetch elevation data\n const terrainData = await this.fetchElevationData(center, dimensions)\n\n // Store the data\n this.currentData = terrainData\n\n // Dispatch data loaded event\n this.dispatchEvent({\n type: 'dataLoaded',\n data: terrainData,\n })\n\n const textureSource = await this.resolveTexture(center, dimensions)\n\n // Create and add mesh to scene\n await this.createMesh(terrainData, textureSource)\n } catch (error) {\n console.error('Error loading terrain:', error)\n this.dispatchEvent({\n type: 'error',\n message: 'Failed to load terrain',\n error: error as Error,\n })\n } finally {\n this.isLoading = false\n }\n }\n\n /**\n * Updates the terrain with new coordinates and/or dimensions\n * @param center - New center coordinates (optional, keeps current if not provided)\n * @param dimensions - New dimensions (optional, keeps current if not provided)\n */\n async updateTerrain(\n center?: GeoCoordinates,\n dimensions?: TerrainDimensions\n ): Promise<void> {\n if (!this.currentData && !center) {\n console.warn('No current terrain data and no center provided')\n return\n }\n\n const newCenter = center || this.currentData!.center\n const newDimensions = dimensions || this.currentData!.dimensions\n\n await this.loadTerrain(newCenter, newDimensions)\n }\n\n /**\n * Fetches elevation data from Open-Elevation API or generates demo data\n * @private\n */\n private async fetchElevationData(\n center: GeoCoordinates,\n dimensions: TerrainDimensions\n ): Promise<TerrainData> {\n if (this.useDemoData) {\n return this.generateDemoData(center, dimensions)\n }\n\n // Calculate grid of lat/lon points\n const points: { latitude: number; longitude: number }[] = []\n\n // Approximate degrees per meter at this latitude\n const metersPerDegreeLat = 111320\n const metersPerDegreeLon =\n 111320 *\n Math.max(Math.abs(Math.cos((center.latitude * Math.PI) / 180)), 1e-6)\n\n const latRange = dimensions.depth / metersPerDegreeLat\n const lonRange = dimensions.width / metersPerDegreeLon\n\n const latStep = latRange / this.depthSegments\n const lonStep = lonRange / this.widthSegments\n\n const startLat = center.latitude + latRange / 2\n const startLon = center.longitude - lonRange / 2\n\n // Create grid of points\n for (let z = 0; z <= this.depthSegments; z++) {\n for (let x = 0; x <= this.widthSegments; x++) {\n points.push({\n latitude: startLat - z * latStep,\n longitude: startLon + x * lonStep,\n })\n }\n }\n\n // Fetch elevation data from Open-Elevation API\n // Note: This API has rate limits and may not work for large requests\n // In production, you might want to use a different API or cache results\n const response = await fetch(\n 'https://api.open-elevation.com/api/v1/lookup',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n locations: points,\n }),\n }\n )\n\n if (!response.ok) {\n throw new Error(`Elevation API error: ${response.statusText}`)\n }\n\n const data = await response.json()\n const results: ElevationPoint[] = data.results\n\n // Convert to 2D grid\n const elevations: number[][] = []\n let minElevation = Infinity\n let maxElevation = -Infinity\n\n for (let z = 0; z <= this.depthSegments; z++) {\n const row: number[] = []\n for (let x = 0; x <= this.widthSegments; x++) {\n const index = z * (this.widthSegments + 1) + x\n const elevation = results[index].elevation\n row.push(elevation)\n minElevation = Math.min(minElevation, elevation)\n maxElevation = Math.max(maxElevation, elevation)\n }\n elevations.push(row)\n }\n\n return {\n center,\n dimensions,\n elevations,\n minElevation,\n maxElevation,\n }\n }\n\n /**\n * Generates demo terrain data using perlin-like noise\n * @private\n */\n private generateDemoData(\n center: GeoCoordinates,\n dimensions: TerrainDimensions\n ): TerrainData {\n const elevations: number[][] = []\n let minElevation = Infinity\n let maxElevation = -Infinity\n\n // Simple sine-based terrain generation for demo\n const seed = center.latitude + center.longitude\n for (let z = 0; z <= this.depthSegments; z++) {\n const row: number[] = []\n for (let x = 0; x <= this.widthSegments; x++) {\n // Create varied terrain using multiple sine waves\n const nx = (x / this.widthSegments) * 4\n const nz = (z / this.depthSegments) * 4\n\n let elevation = 0\n // Large features\n elevation += Math.sin(nx + seed) * 500\n elevation += Math.cos(nz + seed) * 500\n // Medium features\n elevation += Math.sin(nx * 2 + seed) * 200\n elevation += Math.cos(nz * 2 + seed) * 200\n // Small features\n elevation += Math.sin(nx * 4 + seed) * 50\n elevation += Math.cos(nz * 4 + seed) * 50\n // Random noise\n elevation += (Math.random() - 0.5) * 30\n\n row.push(elevation)\n minElevation = Math.min(minElevation, elevation)\n maxElevation = Math.max(maxElevation, elevation)\n }\n elevations.push(row)\n }\n\n return {\n center,\n dimensions,\n elevations,\n minElevation,\n maxElevation,\n }\n }\n\n /**\n * Creates a terrain mesh from elevation data\n * @private\n */\n private async createMesh(\n data: TerrainData,\n textureSource?: TerrainTextureSource\n ): Promise<void> {\n // Remove existing mesh if any\n if (this.mesh) {\n this.scene.remove(this.mesh)\n this.mesh.geometry.dispose()\n if (Array.isArray(this.mesh.material)) {\n this.mesh.material.forEach((mat) => mat.dispose())\n } else {\n this.mesh.material.dispose()\n }\n }\n\n // Create plane geometry\n const geometry = new THREE.PlaneGeometry(\n data.dimensions.width,\n data.dimensions.depth,\n this.widthSegments,\n this.depthSegments\n )\n\n // Apply elevation data to geometry vertices\n const positions = geometry.attributes.position\n let vertexIndex = 0\n\n for (let z = 0; z <= this.depthSegments; z++) {\n for (let x = 0; x <= this.widthSegments; x++) {\n const elevation = data.elevations[z][x]\n // Normalize elevation relative to min elevation\n const height = (elevation - data.minElevation) * this.elevationScale\n positions.setZ(vertexIndex, height)\n vertexIndex++\n }\n }\n\n // Recompute normals for proper lighting\n geometry.computeVertexNormals()\n\n // Create material\n let material: THREE.Material\n\n if (textureSource?.url) {\n try {\n const texture = await new THREE.TextureLoader().loadAsync(\n textureSource.url\n )\n material = new THREE.MeshStandardMaterial({\n map: texture,\n wireframe: this.wireframe,\n })\n } catch (error) {\n console.warn('Failed to load terrain texture, using base color.', error)\n material = new THREE.MeshStandardMaterial({\n color: this.baseColor,\n wireframe: this.wireframe,\n })\n } finally {\n if (textureSource.revokeOnUse) {\n this.releaseGeneratedTexture(textureSource.url)\n }\n }\n } else {\n material = new THREE.MeshStandardMaterial({\n color: this.baseColor,\n wireframe: this.wireframe,\n })\n }\n\n // Create mesh\n this.mesh = new THREE.Mesh(geometry, material)\n this.mesh.rotation.x = -Math.PI / 2 // Rotate to horizontal\n this.mesh.receiveShadow = this.receiveShadow\n this.mesh.castShadow = this.castShadow\n\n // Add to scene\n this.scene.add(this.mesh)\n\n // Dispatch mesh loaded event\n this.dispatchEvent({\n type: 'meshLoaded',\n mesh: this.mesh,\n })\n }\n\n private async resolveTexture(\n center: GeoCoordinates,\n dimensions: TerrainDimensions\n ): Promise<TerrainTextureSource | undefined> {\n if (this.textureUrl) {\n return {\n url: this.textureUrl,\n revokeOnUse: false,\n }\n }\n\n if (!this.mapboxOptions) {\n return undefined\n }\n\n try {\n const url = await this.fetchMapboxTexture(\n center,\n dimensions,\n this.mapboxOptions\n )\n this.generatedTextureUrls.add(url)\n return {\n url,\n revokeOnUse: true,\n }\n } catch (error) {\n console.warn(\n 'Failed to fetch Mapbox imagery, falling back to base material.',\n error\n )\n return undefined\n }\n }\n\n private computeBoundingBox(\n center: GeoCoordinates,\n dimensions: TerrainDimensions,\n paddingRatio: number\n ): { minLat: number; maxLat: number; minLon: number; maxLon: number } {\n const metersPerDegreeLat = 111320\n const latRadians = (center.latitude * Math.PI) / 180\n const cosLat = Math.cos(latRadians)\n const metersPerDegreeLon = 111320 * Math.max(Math.abs(cosLat), 1e-6)\n\n const appliedPadding = Math.max(0, paddingRatio)\n const widthWithPadding = dimensions.width * (1 + appliedPadding)\n const depthWithPadding = dimensions.depth * (1 + appliedPadding)\n\n const halfDepthDegrees = depthWithPadding / 2 / metersPerDegreeLat\n const halfWidthDegrees = widthWithPadding / 2 / metersPerDegreeLon\n\n const clamp = (value: number, min: number, max: number) =>\n Math.min(Math.max(value, min), max)\n\n const minLat = clamp(center.latitude - halfDepthDegrees, -90, 90)\n const maxLat = clamp(center.latitude + halfDepthDegrees, -90, 90)\n const minLon = clamp(center.longitude - halfWidthDegrees, -180, 180)\n const maxLon = clamp(center.longitude + halfWidthDegrees, -180, 180)\n\n return { minLat, maxLat, minLon, maxLon }\n }\n\n private async fetchMapboxTexture(\n center: GeoCoordinates,\n dimensions: TerrainDimensions,\n options: MapboxTextureOptions\n ): Promise<string> {\n const styleId = options.styleId ?? 'mapbox/satellite-v9'\n const normalizedStyleId = styleId\n .replace('mapbox://styles/', '')\n .replace(/^\\/+/, '')\n\n const width = Math.min(\n 1280,\n Math.max(1, Math.floor(options.imageWidth ?? 1024))\n )\n const height = Math.min(\n 1280,\n Math.max(1, Math.floor(options.imageHeight ?? 1024))\n )\n const highResSuffix = options.highResolution ? '@2x' : ''\n const format = options.imageFormat ?? 'png'\n const paddingRatio = options.paddingRatio ?? 0.1\n\n const bounds = this.computeBoundingBox(center, dimensions, paddingRatio)\n const bbox = `${bounds.minLon.toFixed(6)},${bounds.minLat.toFixed(\n 6\n )},${bounds.maxLon.toFixed(6)},${bounds.maxLat.toFixed(6)}`\n\n const params = new URLSearchParams({\n access_token: options.accessToken,\n format,\n })\n\n const requestUrl = `https://api.mapbox.com/styles/v1/${normalizedStyleId}/static/[${bbox}]/${width}x${height}${highResSuffix}?${params.toString()}`\n\n const response = await fetch(requestUrl)\n if (!response.ok) {\n throw new Error(\n `Mapbox imagery request failed: ${response.status} ${response.statusText}`\n )\n }\n\n const blob = await response.blob()\n\n if (\n typeof URL === 'undefined' ||\n typeof URL.createObjectURL !== 'function'\n ) {\n throw new Error(\n 'URL.createObjectURL is not available in this environment'\n )\n }\n\n return URL.createObjectURL(blob)\n }\n\n private releaseGeneratedTexture(url: string): void {\n if (!this.generatedTextureUrls.has(url)) {\n return\n }\n\n if (\n typeof URL !== 'undefined' &&\n typeof URL.revokeObjectURL === 'function'\n ) {\n URL.revokeObjectURL(url)\n }\n\n this.generatedTextureUrls.delete(url)\n }\n\n /**\n * Gets the current terrain mesh\n */\n getMesh(): THREE.Mesh | null {\n return this.mesh\n }\n\n /**\n * Gets the current terrain data\n */\n getData(): TerrainData | null {\n return this.currentData\n }\n\n /**\n * Checks if terrain is currently loading\n */\n isTerrainLoading(): boolean {\n return this.isLoading\n }\n\n /**\n * Disposes of all resources\n */\n dispose(): void {\n if (this.mesh) {\n this.scene.remove(this.mesh)\n this.mesh.geometry.dispose()\n if (Array.isArray(this.mesh.material)) {\n this.mesh.material.forEach((mat) => mat.dispose())\n } else {\n this.mesh.material.dispose()\n }\n this.mesh = null\n }\n this.currentData = null\n\n if (this.generatedTextureUrls.size > 0) {\n for (const url of Array.from(this.generatedTextureUrls)) {\n this.releaseGeneratedTexture(url)\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// packages/grid/src/InfiniteGrid.ts
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
var InfiniteGrid = class extends THREE.Object3D {
|
|
4
|
+
constructor(divisions = 1, subdivisions = 10) {
|
|
5
|
+
super();
|
|
6
|
+
this.eventDispatcher = new THREE.EventDispatcher();
|
|
7
|
+
this.divisions = divisions;
|
|
8
|
+
this.subdivisions = subdivisions;
|
|
9
|
+
const gridSize = 100;
|
|
10
|
+
const gridGeometry = new THREE.PlaneGeometry(
|
|
11
|
+
gridSize * 2,
|
|
12
|
+
gridSize * 2,
|
|
13
|
+
1,
|
|
14
|
+
1
|
|
15
|
+
);
|
|
16
|
+
this.gridMaterial = new THREE.ShaderMaterial({
|
|
17
|
+
uniforms: {
|
|
18
|
+
uSize1: { value: this.divisions / this.subdivisions },
|
|
19
|
+
uSize2: { value: this.divisions },
|
|
20
|
+
uColor1: { value: new THREE.Color(4473924) },
|
|
21
|
+
uColor2: { value: new THREE.Color(6710886) },
|
|
22
|
+
uFogColor: { value: new THREE.Color(2763306) },
|
|
23
|
+
uFogNear: { value: 20 },
|
|
24
|
+
uFogFar: { value: 60 }
|
|
25
|
+
},
|
|
26
|
+
vertexShader: `
|
|
27
|
+
varying vec3 worldPosition;
|
|
28
|
+
|
|
29
|
+
void main() {
|
|
30
|
+
worldPosition = position.xzy;
|
|
31
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
fragmentShader: `
|
|
35
|
+
uniform float uSize1;
|
|
36
|
+
uniform float uSize2;
|
|
37
|
+
uniform vec3 uColor1;
|
|
38
|
+
uniform vec3 uColor2;
|
|
39
|
+
uniform vec3 uFogColor;
|
|
40
|
+
uniform float uFogNear;
|
|
41
|
+
uniform float uFogFar;
|
|
42
|
+
|
|
43
|
+
varying vec3 worldPosition;
|
|
44
|
+
|
|
45
|
+
float getGrid(float size) {
|
|
46
|
+
vec2 r = worldPosition.xz / size;
|
|
47
|
+
vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
|
|
48
|
+
float line = min(grid.x, grid.y);
|
|
49
|
+
return 1.0 - min(line, 1.0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void main() {
|
|
53
|
+
float g1 = getGrid(uSize1);
|
|
54
|
+
float g2 = getGrid(uSize2);
|
|
55
|
+
|
|
56
|
+
// Calculate fog factor using both near and far
|
|
57
|
+
float dist = length(worldPosition);
|
|
58
|
+
float fogFactor = smoothstep(uFogNear, uFogFar, dist);
|
|
59
|
+
|
|
60
|
+
// Mix grid colors
|
|
61
|
+
vec3 color = mix(uColor1, uColor2, g2);
|
|
62
|
+
|
|
63
|
+
// Apply fog to color and use inverse fog factor for alpha (fade out with distance)
|
|
64
|
+
vec3 finalColor = mix(color, uFogColor, fogFactor);
|
|
65
|
+
float alpha = (g1 + g2) * (1.0 - fogFactor);
|
|
66
|
+
|
|
67
|
+
gl_FragColor = vec4(finalColor, alpha);
|
|
68
|
+
}
|
|
69
|
+
`,
|
|
70
|
+
transparent: true,
|
|
71
|
+
side: THREE.DoubleSide,
|
|
72
|
+
depthWrite: false
|
|
73
|
+
});
|
|
74
|
+
const gridMesh = new THREE.Mesh(gridGeometry, this.gridMaterial);
|
|
75
|
+
gridMesh.renderOrder = -1;
|
|
76
|
+
gridMesh.rotation.x = -Math.PI / 2;
|
|
77
|
+
this.add(gridMesh);
|
|
78
|
+
}
|
|
79
|
+
setSubdivisions(subdivisions) {
|
|
80
|
+
this.subdivisions = subdivisions;
|
|
81
|
+
this.gridMaterial.uniforms.uSize1.value = this.divisions / this.subdivisions;
|
|
82
|
+
this.eventDispatcher.dispatchEvent({
|
|
83
|
+
type: "subdivisionsChanged",
|
|
84
|
+
subdivisions
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
setDivisions(divisions) {
|
|
88
|
+
this.divisions = divisions;
|
|
89
|
+
this.gridMaterial.uniforms.uSize1.value = this.divisions / this.subdivisions;
|
|
90
|
+
this.gridMaterial.uniforms.uSize2.value = this.divisions;
|
|
91
|
+
this.eventDispatcher.dispatchEvent({ type: "divisionsChanged", divisions });
|
|
92
|
+
}
|
|
93
|
+
setColor1(color) {
|
|
94
|
+
if (typeof color === "number") {
|
|
95
|
+
this.gridMaterial.uniforms.uColor1.value.setHex(color);
|
|
96
|
+
} else {
|
|
97
|
+
this.gridMaterial.uniforms.uColor1.value.copy(color);
|
|
98
|
+
}
|
|
99
|
+
this.eventDispatcher.dispatchEvent({
|
|
100
|
+
type: "colorChanged",
|
|
101
|
+
color,
|
|
102
|
+
colorType: "color1"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
setColor2(color) {
|
|
106
|
+
if (typeof color === "number") {
|
|
107
|
+
this.gridMaterial.uniforms.uColor2.value.setHex(color);
|
|
108
|
+
} else {
|
|
109
|
+
this.gridMaterial.uniforms.uColor2.value.copy(color);
|
|
110
|
+
}
|
|
111
|
+
this.eventDispatcher.dispatchEvent({
|
|
112
|
+
type: "colorChanged",
|
|
113
|
+
color,
|
|
114
|
+
colorType: "color2"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
setFogColor(color) {
|
|
118
|
+
if (typeof color === "number") {
|
|
119
|
+
this.gridMaterial.uniforms.uFogColor.value.setHex(color);
|
|
120
|
+
} else {
|
|
121
|
+
this.gridMaterial.uniforms.uFogColor.value.copy(color);
|
|
122
|
+
}
|
|
123
|
+
this.eventDispatcher.dispatchEvent({
|
|
124
|
+
type: "colorChanged",
|
|
125
|
+
color,
|
|
126
|
+
colorType: "fog"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
setFogNear(near) {
|
|
130
|
+
const fogFar = this.gridMaterial.uniforms.uFogFar.value;
|
|
131
|
+
const clampedNear = Math.min(near, fogFar);
|
|
132
|
+
this.gridMaterial.uniforms.uFogNear.value = clampedNear;
|
|
133
|
+
this.eventDispatcher.dispatchEvent({
|
|
134
|
+
type: "fogChanged",
|
|
135
|
+
property: "near",
|
|
136
|
+
value: clampedNear
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
setFogFar(far) {
|
|
140
|
+
const fogNear = this.gridMaterial.uniforms.uFogNear.value;
|
|
141
|
+
const clampedFar = Math.max(far, fogNear);
|
|
142
|
+
this.gridMaterial.uniforms.uFogFar.value = clampedFar;
|
|
143
|
+
this.eventDispatcher.dispatchEvent({
|
|
144
|
+
type: "fogChanged",
|
|
145
|
+
property: "far",
|
|
146
|
+
value: clampedFar
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Custom event listener methods (prefixed to avoid conflicts with Object3D)
|
|
150
|
+
addGridEventListener(type, listener) {
|
|
151
|
+
this.eventDispatcher.addEventListener(type, listener);
|
|
152
|
+
}
|
|
153
|
+
removeGridEventListener(type, listener) {
|
|
154
|
+
this.eventDispatcher.removeEventListener(type, listener);
|
|
155
|
+
}
|
|
156
|
+
hasGridEventListener(type, listener) {
|
|
157
|
+
return this.eventDispatcher.hasEventListener(type, listener);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
InfiniteGrid
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=chunk-EQDOX34V.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../packages/grid/src/InfiniteGrid.ts"],"sourcesContent":["import * as THREE from 'three'\n\n// Define custom event types\ninterface InfiniteGridEventMap {\n subdivisionsChanged: { subdivisions: number }\n divisionsChanged: { divisions: number }\n colorChanged: {\n color: THREE.Color | number\n colorType: 'color1' | 'color2' | 'fog'\n }\n fogChanged: { property: 'near' | 'far'; value: number }\n}\n\nexport class InfiniteGrid extends THREE.Object3D {\n subdivisions: number\n divisions: number\n gridMaterial: THREE.ShaderMaterial\n private eventDispatcher: THREE.EventDispatcher<InfiniteGridEventMap>\n\n constructor(divisions: number = 1, subdivisions: number = 10) {\n super()\n\n // Create an internal EventDispatcher for custom events\n this.eventDispatcher = new THREE.EventDispatcher<InfiniteGridEventMap>()\n this.divisions = divisions\n this.subdivisions = subdivisions\n\n // Create a custom grid shader material\n const gridSize = 100\n\n // Create a plane for the grid\n const gridGeometry = new THREE.PlaneGeometry(\n gridSize * 2,\n gridSize * 2,\n 1,\n 1\n )\n\n // Custom grid shader material\n this.gridMaterial = new THREE.ShaderMaterial({\n uniforms: {\n uSize1: { value: this.divisions / this.subdivisions },\n uSize2: { value: this.divisions },\n uColor1: { value: new THREE.Color(0x444444) },\n uColor2: { value: new THREE.Color(0x666666) },\n uFogColor: { value: new THREE.Color(0x2a2a2a) },\n uFogNear: { value: 20.0 },\n uFogFar: { value: 60.0 },\n },\n vertexShader: `\n varying vec3 worldPosition;\n \n void main() {\n worldPosition = position.xzy;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform float uSize1;\n uniform float uSize2;\n uniform vec3 uColor1;\n uniform vec3 uColor2;\n uniform vec3 uFogColor;\n uniform float uFogNear;\n uniform float uFogFar;\n \n varying vec3 worldPosition;\n \n float getGrid(float size) {\n vec2 r = worldPosition.xz / size;\n vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);\n float line = min(grid.x, grid.y);\n return 1.0 - min(line, 1.0);\n }\n \n void main() {\n float g1 = getGrid(uSize1);\n float g2 = getGrid(uSize2);\n \n // Calculate fog factor using both near and far\n float dist = length(worldPosition);\n float fogFactor = smoothstep(uFogNear, uFogFar, dist);\n \n // Mix grid colors\n vec3 color = mix(uColor1, uColor2, g2);\n \n // Apply fog to color and use inverse fog factor for alpha (fade out with distance)\n vec3 finalColor = mix(color, uFogColor, fogFactor);\n float alpha = (g1 + g2) * (1.0 - fogFactor);\n \n gl_FragColor = vec4(finalColor, alpha);\n }\n `,\n transparent: true,\n side: THREE.DoubleSide,\n depthWrite: false,\n })\n\n // Create the grid mesh\n const gridMesh = new THREE.Mesh(gridGeometry, this.gridMaterial)\n gridMesh.renderOrder = -1\n gridMesh.rotation.x = -Math.PI / 2\n this.add(gridMesh)\n // return gridMesh;\n }\n\n setSubdivisions(subdivisions: number): void {\n this.subdivisions = subdivisions\n this.gridMaterial.uniforms.uSize1.value = this.divisions / this.subdivisions\n this.eventDispatcher.dispatchEvent({\n type: 'subdivisionsChanged',\n subdivisions,\n })\n }\n\n setDivisions(divisions: number): void {\n this.divisions = divisions\n this.gridMaterial.uniforms.uSize1.value = this.divisions / this.subdivisions\n this.gridMaterial.uniforms.uSize2.value = this.divisions\n this.eventDispatcher.dispatchEvent({ type: 'divisionsChanged', divisions })\n }\n\n setColor1(color: THREE.Color | number): void {\n if (typeof color === 'number') {\n this.gridMaterial.uniforms.uColor1.value.setHex(color)\n } else {\n this.gridMaterial.uniforms.uColor1.value.copy(color)\n }\n this.eventDispatcher.dispatchEvent({\n type: 'colorChanged',\n color,\n colorType: 'color1',\n })\n }\n\n setColor2(color: THREE.Color | number): void {\n if (typeof color === 'number') {\n this.gridMaterial.uniforms.uColor2.value.setHex(color)\n } else {\n this.gridMaterial.uniforms.uColor2.value.copy(color)\n }\n this.eventDispatcher.dispatchEvent({\n type: 'colorChanged',\n color,\n colorType: 'color2',\n })\n }\n\n setFogColor(color: THREE.Color | number): void {\n if (typeof color === 'number') {\n this.gridMaterial.uniforms.uFogColor.value.setHex(color)\n } else {\n this.gridMaterial.uniforms.uFogColor.value.copy(color)\n }\n this.eventDispatcher.dispatchEvent({\n type: 'colorChanged',\n color,\n colorType: 'fog',\n })\n }\n\n setFogNear(near: number): void {\n const fogFar = this.gridMaterial.uniforms.uFogFar.value\n const clampedNear = Math.min(near, fogFar)\n this.gridMaterial.uniforms.uFogNear.value = clampedNear\n this.eventDispatcher.dispatchEvent({\n type: 'fogChanged',\n property: 'near',\n value: clampedNear,\n })\n }\n\n setFogFar(far: number): void {\n const fogNear = this.gridMaterial.uniforms.uFogNear.value\n const clampedFar = Math.max(far, fogNear)\n this.gridMaterial.uniforms.uFogFar.value = clampedFar\n this.eventDispatcher.dispatchEvent({\n type: 'fogChanged',\n property: 'far',\n value: clampedFar,\n })\n }\n\n // Custom event listener methods (prefixed to avoid conflicts with Object3D)\n addGridEventListener<K extends keyof InfiniteGridEventMap>(\n type: K,\n listener: (event: InfiniteGridEventMap[K] & { type: K }) => void\n ): void {\n this.eventDispatcher.addEventListener(type, listener)\n }\n\n removeGridEventListener<K extends keyof InfiniteGridEventMap>(\n type: K,\n listener: (event: InfiniteGridEventMap[K] & { type: K }) => void\n ): void {\n this.eventDispatcher.removeEventListener(type, listener)\n }\n\n hasGridEventListener<K extends keyof InfiniteGridEventMap>(\n type: K,\n listener: (event: InfiniteGridEventMap[K] & { type: K }) => void\n ): boolean {\n return this.eventDispatcher.hasEventListener(type, listener)\n }\n}\n"],"mappings":"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","names":[]}
|