@tomtom-org/maps-sdk-plugin-landmarks-3d 0.0.12 → 0.1.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/dist/index.d.ts CHANGED
@@ -1,17 +1,73 @@
1
- import { TomTomMap } from "@tomtom-org/maps-sdk/map";
1
+ import { KnobConfigOf, KnobEntry, KnobRange, KnobValueOf, TomTomMap } from "@tomtom-org/maps-sdk/map";
2
2
  import { CustomLayerInterface, CustomRenderMethodInput, Map } from "maplibre-gl";
3
3
  import { AmbientLight, Camera, DirectionalLight, Group, Mesh, Scene, ShaderMaterial, Texture, WebGLRenderer } from "three";
4
- //#region src/displayMode.d.ts
4
+ //#region src/knobCatalogue.d.ts
5
+ declare const landmarks3DKnobDefinitions: {
6
+ readonly visible: {
7
+ readonly kind: "toggle";
8
+ readonly description: "The landmark models.";
9
+ readonly default: true;
10
+ };
11
+ readonly color: {
12
+ readonly kind: "color";
13
+ readonly description: "Colour the landmarks are shaded in. Unset: the base map's 3D buildings' colour, following the map colours that restyle them.";
14
+ };
15
+ readonly opacity: {
16
+ readonly kind: "number";
17
+ readonly description: "Opacity of the landmarks, 0 to 1. Unset: the base map's 3D buildings' opacity.";
18
+ readonly range: KnobRange;
19
+ };
20
+ readonly verticalGradient: {
21
+ readonly kind: "toggle";
22
+ readonly description: "Whether the landmarks' walls darken towards the ground. Unset: as the base map's 3D buildings do.";
23
+ };
24
+ readonly minZoom: {
25
+ readonly kind: "number";
26
+ readonly description: "Lowest zoom the landmarks are drawn at. Unset: the base map's 3D buildings' one.";
27
+ readonly range: KnobRange;
28
+ };
29
+ readonly maxZoom: {
30
+ readonly kind: "number";
31
+ readonly description: "Highest whole zoom level the landmarks are drawn at, up to the next one. Unset: the base map's 3D buildings' one.";
32
+ readonly range: KnobRange;
33
+ };
34
+ };
5
35
  /**
6
- * How 3D landmarks are shaded relative to the base map. Every mode renders the
7
- * landmarks as maplibre-style fill-extrusion buildings:
8
- * - `inherited`: mirrors the colour, opacity and vertical gradient of the basemap 3D building layer (default).
9
- * - `dark`: uses the 3D building look of the SDK's `streetDark` style.
10
- * - `light`: uses the 3D building look of the SDK's `streetLight` style.
36
+ * The id of a landmarks knob, as accepted by the SDK's `setKnob` and as a key of {@link Landmarks3DConfig}.
11
37
  *
12
38
  * @group Landmarks 3D
13
39
  */
14
- type Landmarks3DDisplayMode = 'inherited' | 'dark' | 'light';
40
+ type Landmarks3DKnobId = keyof typeof landmarks3DKnobDefinitions;
41
+ /**
42
+ * The value type a landmarks knob takes.
43
+ *
44
+ * @group Landmarks 3D
45
+ */
46
+ type Landmarks3DKnobValueOf<ID extends Landmarks3DKnobId> = KnobValueOf<(typeof landmarks3DKnobDefinitions)[ID]['kind']>;
47
+ /**
48
+ * One entry of {@link landmarks3DKnobCatalogue}, with what a UI or a coding agent needs to offer the knob
49
+ * and validate a value for it. The SDK's `getKnob` reads the value in force.
50
+ *
51
+ * @group Landmarks 3D
52
+ */
53
+ type Landmarks3DKnob = KnobEntry<Landmarks3DKnobId>;
54
+ /**
55
+ * Every landmarks knob with its kind, description and, for `visible`, its default. It's static data,
56
+ * readable without a map.
57
+ *
58
+ * @remarks
59
+ * A knob is a plain value in {@link Landmarks3DConfig}, under its id. Every knob but `visible`
60
+ * follows the base map's 3D buildings while unset, which the SDK's `getKnob` reports.
61
+ *
62
+ * @group Landmarks 3D
63
+ */
64
+ export declare const landmarks3DKnobCatalogue: readonly Landmarks3DKnob[];
65
+ /**
66
+ * Every landmarks knob id, in catalogue order.
67
+ *
68
+ * @group Landmarks 3D
69
+ */
70
+ export declare const landmarks3DKnobIds: readonly Landmarks3DKnobId[];
15
71
  //#endregion
16
72
  //#region src/FillExtrusionMaterial.d.ts
17
73
  /**
@@ -162,6 +218,7 @@ export declare class ModelsSource {
162
218
  private readonly gltfLoader;
163
219
  private readonly specification;
164
220
  private readonly withCredentials;
221
+ private coveringTileKeys;
165
222
  constructor(id: string, options: ModelsSourceSpecification);
166
223
  onAdd(map: Map, renderer: WebGLRenderer): void;
167
224
  updateTiles(): void;
@@ -180,6 +237,10 @@ export declare class ModelsSource {
180
237
  * viewport, renders them as maplibre-style fill-extrusion buildings, and keeps its
181
238
  * lighting in sync with the map style light.
182
239
  *
240
+ * It draws whenever the map draws, and asks for a frame of its own only when a tile arrives,
241
+ * so a still map stays idle. After changing one of its fields yourself, call
242
+ * `triggerRepaint()` on the MapLibre map.
243
+ *
183
244
  * @group Landmarks 3D
184
245
  */
185
246
  export declare class ModelsLayer implements CustomLayerInterface {
@@ -201,11 +262,15 @@ export declare class ModelsLayer implements CustomLayerInterface {
201
262
  private readonly fillExtrusionMaterial;
202
263
  private readonly depthPrepassMaterial;
203
264
  private readonly maskedFillExtrusionMaterials;
265
+ private readonly tilesMainMatrix;
266
+ private tilesStale;
267
+ private readonly markTilesStale;
204
268
  constructor(options: ModelsLayerSpecification, source: ModelsSourceSpecification);
205
269
  updateProperties(options: ModelsLayerSpecification): void;
206
270
  setDirectionalLight(radius: number, phi: number, theta: number): void;
207
271
  onAdd(map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
208
272
  render(_gl: WebGLRenderingContext | WebGL2RenderingContext, options: CustomRenderMethodInput): void;
273
+ private updateTilesWhenStale;
209
274
  private groundLandmarksOnTerrain;
210
275
  private groundMeshOnTerrain;
211
276
  private terrainSamplePoints;
@@ -225,35 +290,23 @@ export declare class ModelsLayer implements CustomLayerInterface {
225
290
  //#endregion
226
291
  //#region src/Landmarks3D.d.ts
227
292
  /**
228
- * Configuration for the {@link Landmarks3D} plugin.
293
+ * The landmark knobs an app sets, as {@link Landmarks3D.getConfig} returns them. Each knob id is its
294
+ * key in here. Every knob but `visible` follows the base map's 3D buildings while unset. It's plain JSON.
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * const config: Landmarks3DConfig = { color: '#c8b28a', opacity: 0.9 };
299
+ * ```
300
+ *
301
+ * @group Landmarks 3D
302
+ */
303
+ type Landmarks3DConfig = KnobConfigOf<{ [ID in Landmarks3DKnobId]: Landmarks3DKnobValueOf<ID>; }>;
304
+ /**
305
+ * What the {@link Landmarks3D} plugin starts with: its knobs, and how it requests tiles.
229
306
  *
230
307
  * @group Landmarks 3D
231
308
  */
232
- type Landmarks3DOptions = {
233
- /**
234
- * Initial display mode for the landmarks. To change it at runtime, call
235
- * {@link Landmarks3D.setDisplayMode}.
236
- * @defaultValue `'inherited'`
237
- */
238
- displayMode?: Landmarks3DDisplayMode;
239
- /**
240
- * Whether the landmarks are initially visible. To change it at runtime, call
241
- * {@link Landmarks3D.setVisible}.
242
- * @defaultValue `true`
243
- */
244
- visible?: boolean;
245
- /**
246
- * Minimum zoom at which landmarks are rendered.
247
- * @defaultValue The basemap 3D building layer's minimum zoom, so landmarks appear together
248
- * with it (falls back to `10` when that layer is absent).
249
- */
250
- minZoom?: number;
251
- /**
252
- * Maximum zoom at which landmarks are rendered.
253
- * @defaultValue The basemap 3D building layer's maximum zoom, so landmarks disappear together
254
- * with it (falls back to `22` when that layer is absent).
255
- */
256
- maxZoom?: number;
309
+ type Landmarks3DOptions = Landmarks3DConfig & {
257
310
  /**
258
311
  * Whether each tile request sends `credentials: 'include'`, so a proxy session
259
312
  * cookie travels with it. By default, this tracks the SDK's proxy config,
@@ -273,9 +326,11 @@ type Landmarks3DOptions = {
273
326
  * across map style changes.
274
327
  *
275
328
  * @remarks
276
- * It requires an API key with Orbis 3D Landmarks entitlements. The exclusion
277
- * filter and `inherited` mode only have an effect on a map style that shows
278
- * extruded 3D buildings.
329
+ * It requires an API key with Orbis 3D Landmarks entitlements. Its look is a set of knobs, listed in
330
+ * {@link landmarks3DKnobCatalogue}. Each one you leave unset follows the base map's 3D buildings,
331
+ * through style changes and anything that restyles them, falling back to the `streetLight` style's look.
332
+ * Whether those buildings show is the `buildings3D` group of the `BaseMapModule`, hidden in most
333
+ * standard styles; the plugin leaves it as it is.
279
334
  *
280
335
  * @example
281
336
  * ```typescript
@@ -284,7 +339,7 @@ type Landmarks3DOptions = {
284
339
  * // assume `map` is your initialized TomTomMap instance
285
340
  * const landmarks = new Landmarks3D(map);
286
341
  *
287
- * await landmarks.setDisplayMode('inherited');
342
+ * landmarks.updateConfig({ color: '#c8b28a' });
288
343
  * ```
289
344
  *
290
345
  * @group Landmarks 3D
@@ -294,50 +349,81 @@ export declare class Landmarks3D {
294
349
  private readonly layerID;
295
350
  private readonly basemapBuildingLayerID;
296
351
  private readonly modelsLayer;
297
- private readonly layerMinZoom?;
298
- private readonly layerMaxZoom?;
299
- private displayMode;
352
+ private config;
353
+ private renderedLook;
300
354
  /**
301
355
  * Creates the plugin bound to a TomTom map and starts rendering landmarks
302
356
  * as soon as the map is ready.
303
357
  *
304
358
  * @param map - The TomTom map instance to render landmarks on.
359
+ * @param options - The knobs to start with, and how to request tiles.
305
360
  */
306
- constructor(map: TomTomMap, options?: Landmarks3DOptions);
361
+ constructor(map: TomTomMap, { withCredentials, ...config }?: Landmarks3DOptions);
307
362
  private installWhenReady;
308
363
  /**
309
364
  * The underlying MapLibre custom layer, for advanced use beyond this facade.
310
365
  */
311
366
  get layer(): ModelsLayer;
312
367
  /**
313
- * The currently applied display mode.
368
+ * Returns the knobs set, as a plain serializable copy. The SDK's `getKnob` reads a knob's value
369
+ * in force, the base map's one when unset.
314
370
  */
315
- getDisplayMode(): Landmarks3DDisplayMode;
371
+ getConfig(): Landmarks3DConfig;
316
372
  /**
317
- * Changes how landmarks are rendered relative to the base map.
373
+ * Replaces the whole configuration. An empty object has every knob follow the base map's 3D buildings.
374
+ *
375
+ * @remarks
376
+ * Use it, and {@link updateConfig}, for a look your code writes: as on a map module, the type is
377
+ * the check, and the values are not checked again. For values held as data, the SDK's `setKnobs`
378
+ * checks each against {@link landmarks3DKnobCatalogue} and sets them by id.
318
379
  *
319
- * @param mode - The display mode to apply.
320
- * @returns A promise that resolves once the mode has been applied.
380
+ * @example
381
+ * ```typescript
382
+ * landmarks.applyConfig({ color: '#c8b28a', opacity: 0.9 });
383
+ * ```
384
+ */
385
+ applyConfig(config: Landmarks3DConfig): void;
386
+ /**
387
+ * Changes the given knobs and keeps the others.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * landmarks.updateConfig({ opacity: 1 }); // the colour still follows the base map
392
+ * ```
393
+ */
394
+ updateConfig(partial: Landmarks3DConfig): void;
395
+ /**
396
+ * Unsets every knob, so the landmarks are shown and look like the base map's 3D buildings.
321
397
  */
322
- setDisplayMode(mode: Landmarks3DDisplayMode): Promise<void>;
398
+ resetConfig(): void;
323
399
  /**
324
- * Whether the landmarks are currently visible.
400
+ * Whether the landmarks are set to be shown: the `visible` knob, `true` unless set.
325
401
  */
326
402
  isVisible(): boolean;
327
403
  /**
328
- * Shows or hides the landmarks layer.
404
+ * Shows or hides the landmarks. Stored as `visible` in the configuration.
329
405
  *
330
406
  * @param visible - `true` to show landmarks, `false` to hide them.
331
- * @returns A promise that resolves once the visibility has been applied.
332
407
  */
333
- setVisible(visible: boolean): Promise<void>;
408
+ setVisible(visible: boolean): void;
409
+ /**
410
+ * @ignore
411
+ */
412
+ _knobValue(id: string): unknown;
413
+ /**
414
+ * @ignore
415
+ */
416
+ _setKnobValues(values: Readonly<Record<string, unknown>>): void;
417
+ /**
418
+ * @ignore
419
+ */
420
+ _resetKnobValue(id: string): void;
334
421
  private install;
335
422
  private landmarkInsertBeforeID;
336
- private alignZoomRangeWithBasemapBuildings;
337
423
  private excludeLandmarkBuildingsFromBasemap;
338
- private readBasemapBuildingMaterial;
339
- private resolveCurrentMaterialState;
340
- private applyDisplayMode;
424
+ private readBasemapBuildingsLook;
425
+ private look;
426
+ private render;
341
427
  }
342
428
  //#endregion
343
429
  //#region src/landmarksTileURL.d.ts
@@ -352,4 +438,4 @@ export declare class Landmarks3D {
352
438
  */
353
439
  export declare const buildLandmarksTileURL: () => string;
354
440
  //#endregion
355
- export type { Landmarks3DDisplayMode, Landmarks3DOptions, ModelsLayerSpecification, ModelsSourceSpecification };
441
+ export type { Landmarks3DConfig, Landmarks3DKnob, Landmarks3DKnobId, Landmarks3DKnobValueOf, Landmarks3DOptions, ModelsLayerSpecification, ModelsSourceSpecification };
package/dist/index.es.js CHANGED
@@ -1 +1 @@
1
- import{TomTomConfig as e,isProxyCredentialsMode as i}from"@tomtom-org/maps-sdk/core";import{LayerFilterComposer as t,mapStyleLayerIDs as a}from"@tomtom-org/maps-sdk/map";import{AmbientLight as s,BufferAttribute as r,Camera as n,Color as o,CustomBlending as l,DirectionalLight as h,DoubleSide as d,FrontSide as m,Group as c,LoaderUtils as u,LoadingManager as p,Matrix4 as f,OneFactor as y,OneMinusSrcAlphaFactor as g,SRGBColorSpace as v,Scene as x,ShaderMaterial as M,Vector3 as L,WebGLRenderer as b}from"three";import{MeshoptDecoder as w}from"three/examples/jsm/libs/meshopt_decoder.module.js";import{GLTFLoader as k}from"three/examples/jsm/loaders/GLTFLoader.js";import{KTX2Loader as C}from"three/examples/jsm/loaders/KTX2Loader.js";import{toCreasedNormals as P}from"three/examples/jsm/utils/BufferGeometryUtils.js";import{MercatorCoordinate as D}from"maplibre-gl";var S={diffuseColor:"hsl(38, 6%, 90%)",opacity:.7,verticalGradient:!0},z={diffuseColor:"hsl(210, 9%, 17%)",opacity:.7,verticalGradient:!0},T=()=>{const{commonBaseURL:i,apiKey:t}=e.instance.get();return`${i}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${t?`key=${t}&`:""}apiVersion=2`},B="fillExtrusionGradient",O=`\n attribute float ${B};\n varying vec3 vWorldNormal;\n varying float vGradient;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n vGradient = ${B};\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n`,E="\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n",I="\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n",A=`\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n uniform float verticalGradient;\n varying vec3 vWorldNormal;\n varying float vGradient;\n ${E}\n\n void main() {\n ${I}\n vec3 normal = normalize(vWorldNormal);\n // Double-sided (alpha-masked) surfaces show their interior through carved openings;\n // flip backface normals so those interiors shade like the facade instead of going dark.\n if (!gl_FrontFacing) {\n normal = -normal;\n }\n // Extrusions have only vertical walls and flat roofs, so a sloped landmark roof would shade\n // darker than every basemap roof around it: up-facing normals light like a flat roof, the\n // rest like a wall, blended for curved surfaces.\n vec3 flattened = vec3(normal.xy, 0.0);\n vec3 wallNormal = flattened / max(length(flattened), 1e-5);\n float upness = smoothstep(0.4, 0.8, normal.z);\n normal = normalize(mix(wallNormal, vec3(0.0, 0.0, 1.0), upness));\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n vec3 color = diffuseColor + vec3(0.03);\n float directional = clamp(dot(normal, lightPosition), 0.0, 1.0);\n directional = mix(1.0 - lightIntensity, max(1.0 - colorValue + lightIntensity, 1.0), directional);\n if (abs(normal.y) > 0.001) {\n directional *=\n (1.0 - verticalGradient) +\n verticalGradient * clamp(vGradient, mix(0.7, 0.98, 1.0 - lightIntensity), 1.0);\n }\n vec3 shaded = clamp(\n color * directional * lightColor,\n mix(vec3(0.0), vec3(0.3), vec3(1.0) - lightColor),\n vec3(1.0)\n );\n gl_FragColor = vec4(shaded, 1.0) * layerOpacity;\n }\n`,F=new L(.2875,-.498,.996),G=new o,_=new o,R=class extends M{constructor(e){super({uniforms:{diffuseColor:{value:new L(1,1,1)},lightPosition:{value:F.clone()},lightIntensity:{value:.5},lightColor:{value:new L(1,1,1)},layerOpacity:{value:1},verticalGradient:{value:1},...e&&{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}}},...e&&{defines:{USE_ALPHA_MASK:""}},vertexShader:O,fragmentShader:A,side:e?d:m,transparent:!0,blending:l,blendSrc:y,blendDst:g})}copyShadingFrom(e){this.uniforms.diffuseColor.value.copy(e.uniforms.diffuseColor.value),this.uniforms.lightPosition.value.copy(e.uniforms.lightPosition.value),this.uniforms.lightIntensity.value=e.uniforms.lightIntensity.value,this.uniforms.lightColor.value.copy(e.uniforms.lightColor.value),this.uniforms.layerOpacity.value=e.uniforms.layerOpacity.value,this.uniforms.verticalGradient.value=e.uniforms.verticalGradient.value}setDiffuseColor(e){G.setStyle(e),G.getRGB(_,v),this.uniforms.diffuseColor.value.set(_.r,_.g,_.b)}setLayerOpacity(e){this.uniforms.layerOpacity.value=e}setVerticalGradient(e){this.uniforms.verticalGradient.value=e?1:0}setLight(e,i,t,a){const s=Math.cos(i),r=Math.sin(i);this.uniforms.lightPosition.value.set(e.x*s-e.y*r,e.x*r+e.y*s,e.z),this.uniforms.lightIntensity.value=t,this.uniforms.lightColor.value.set(a.r,a.g,a.b)}},U=class extends M{constructor(e){super({...e&&{uniforms:{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}},defines:{USE_ALPHA_MASK:""}},vertexShader:O,fragmentShader:`\n ${E}\n void main() {\n ${I}\n gl_FragColor = vec4(0.0);\n }\n `,side:e?d:m,colorWrite:!1})}},Z=e=>!0===e.isMesh,j=1/(2*Math.PI*6378137),V=Math.PI/6,$=class{type="models";id;minzoom;maxzoom;map;tiles;scene;loadedTiles=/* @__PURE__ */new Set;sceneOrigin=null;onTileLoaded;ktx2Loader;transcoderReady;gltfLoader;specification;withCredentials;constructor(e,i){this.id=e,this.minzoom=i.minzoom??0,this.maxzoom=i.maxzoom??22,this.tiles=i.tiles,this.specification=i,this.withCredentials=i.withCredentials??!1,this.scene=new c,this.ktx2Loader=new C(new p).setCrossOrigin("anonymous").setTranscoderPath(i.transcoderPath??""),this.gltfLoader=(new k).setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(w)}onAdd(e,i){this.map=e,this.ktx2Loader.detectSupport(i)}updateTiles(){if(0===this.tiles.length)return;const e=/* @__PURE__ */new Set;for(const i of this.coveringTiles())e.add(i.key),this.loadTile(i);this.showCoveringTilesOnly(e)}showCoveringTilesOnly(e){for(const i of this.scene.children)i.visible=e.has(i.userData.key)}coveringTiles(){const e={tileSize:this.map._camera.transform.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,terrain:this.map.terrain};return this.map.coveringTiles(e)}loadTile(e){if(this.loadedTiles.has(e.key))return;this.loadedTiles.add(e.key);const i=e.canonical.url(this.tiles,this.map.getPixelRatio(),this.specification.scheme);this.fetchTile(i).then(i=>{i&&(W(i),this.rebaseToSceneOrigin(i),i.userData={isTile:!0,key:e.key},this.scene.add(i),this.onTileLoaded?.(i,e.key))}).catch(i=>{console.warn(`Problem with loading ${e.toString()}: ${i?.message??"unknown"}`)})}rebaseToSceneOrigin(e){const i=[];if(e.traverse(e=>{Z(e)&&i.push(e)}),0===i.length)return;const t=i[0].position,a={x:Math.round(t.x),y:Math.round(t.y)};if(this.sceneOrigin){if(Math.hypot(t.x-this.sceneOrigin.x,t.y-this.sceneOrigin.y)>1e5){const e=a.x-this.sceneOrigin.x,i=a.y-this.sceneOrigin.y;this.scene.traverse(t=>{Z(t)&&(t.position.x-=e,t.position.y-=i)}),this.sceneOrigin=a}}else this.sceneOrigin=a;for(const s of i)s.position.x-=this.sceneOrigin.x,s.position.y-=this.sceneOrigin.y}async fetchTile(e){const i=await fetch(e,this.withCredentials?{credentials:"include"}:void 0);if(204===i.status||404===i.status)return null;if(403===i.status&&"InvalidReferer"!==await(async e=>(await e.json().catch(()=>{}))?.detailedError?.code)(i))throw new Error("tile request failed with 403: Orbis 3D Landmarks is in Private Preview (https://docs.tomtom.com/legal/private-preview), and the API key needs access to it: request it from TomTom Sales at https://www.tomtom.com/contact-sales/?source_app=docs-portal&source_product=map-display-api-3d");if(!i.ok)throw new Error(`tile request failed with ${i.status}`);const t=await i.arrayBuffer();this.transcoderReady??=(async(e,i)=>{if(void 0!==i)return;const{resolveBundledTranscoderUrl:t}=await(import("./bundledTranscoder-Dn27cuYm.js"));e.setURLModifier(t)})(this.ktx2Loader.manager,this.specification.transcoderPath),await this.transcoderReady;const{scene:a}=await this.gltfLoader.parseAsync(t,u.extractUrlBase(e));return a}},W=e=>{e.traverse(e=>{if(!Z(e))return;const i=e.material;i.alphaTest=.1,i.side=d,e.userData.originalMaterial=i,e.geometry=P(e.geometry,V),K(e)})},K=e=>{const i=e.geometry,t=i.getAttribute("position");i.computeBoundingBox();const a=i.boundingBox?.max.z??0,s=a>0?Math.sqrt(a/150)/a:0,n=new Float32Array(t.count);for(let r=0;r<t.count;r++)n[r]=t.getZ(r)*s;i.setAttribute(B,new r(n,1))},H=e=>e*Math.PI/180,N=class{id;type="custom";renderingMode="3d";renderer;scene;camera;tiles;directionalLight;ambientLight;map;source;minzoom=0;maxzoom=22;visible=!0;diffuseColor="#ffffff";fillExtrusionMaterial=new R;depthPrepassMaterial=new U;maskedFillExtrusionMaterials=/* @__PURE__ */new Map;constructor(e,i){this.id=e.id,this.source=new $(this.id,i),this.tiles=this.source.scene,this.updateProperties(e)}updateProperties(e){this.visible="none"!==e.layout?.visibility,this.diffuseColor=e.paint?.["models-base-color"]??"#ffffff",this.minzoom=e.minzoom??0,this.maxzoom=e.maxzoom??22}setDirectionalLight(e,i,t){this.directionalLight.position.setFromSphericalCoords(e,H(i),H(t))}onAdd(e,i){this.map=e,this.scene=new x,this.tiles=this.source.scene,this.camera=new n,this.directionalLight=new h(16777215,.5),this.setDirectionalLight(1.15,90,0),this.scene.add(this.directionalLight),this.ambientLight=new s(16777215,.1),this.scene.add(this.ambientLight),this.scene.add(this.tiles),this.renderer=new b({canvas:e.getCanvas(),context:i,antialias:!0}),this.renderer.autoClear=!1,this.source.onAdd(e,this.renderer)}render(e,i){const t=this.map._camera.transform.tileZoom,a=t>=this.minzoom&&t<=this.maxzoom;this.tiles.visible=this.visible&&a,this.visible&&a&&(this.syncLighting(),this.source.updateTiles(),this.groundLandmarksOnTerrain(),this.alignCameraToMap(i),this.renderer.resetState(),this.needsTranslucentPrepass()&&this.renderDepthPrepass(),this.applyMaterials(),this.renderer.render(this.scene,this.camera),this.map.triggerRepaint())}groundLandmarksOnTerrain(){for(const e of this.tiles.children)e.visible&&e.traverse(e=>{Z(e)&&this.groundMeshOnTerrain(e)})}groundMeshOnTerrain(e){const i=this.terrainSamplePoints(e);if(!i)return;let t=null;for(const a of i){const i=this.map.queryTerrainElevation(a);if(null===i)return void(e.position.z=0);(null===t||i<t)&&(t=i)}null!==t&&(e.position.z=t-.5)}terrainSamplePoints(e){let i=e.userData.terrainSamplePoints;if(!i){e.geometry.boundingBox||e.geometry.computeBoundingBox();const t=e.geometry.boundingBox;if(!t||t.isEmpty())return null;const a=e.position.x+this.sceneOrigin.x,s=e.position.y+this.sceneOrigin.y,r=(t.min.x+t.max.x)/2,n=(t.min.y+t.max.y)/2;i=[[r,n],[t.min.x,t.min.y],[t.min.x,t.max.y],[t.max.x,t.min.y],[t.max.x,t.max.y],[r,t.min.y],[r,t.max.y],[t.min.x,n],[t.max.x,n]].map(([e,i])=>new D(.5+(a+e)*j,.5-(s+i)*j).toLngLat().toArray()),e.userData.terrainSamplePoints=i}return i}alignCameraToMap(e){const i=j/Math.cos(H(this.map.getCenter().lat)),t=.5+j*this.sceneOrigin.x,a=.5-j*this.sceneOrigin.y,s=new f;s.set(j,0,0,t,0,-j,0,a,0,0,i,0,0,0,0,1);const r=(new f).fromArray(e.defaultProjectionData.mainMatrix);this.camera.projectionMatrix.copy(r).multiply(s)}get sceneOrigin(){return this.source.sceneOrigin??{x:0,y:0}}needsTranslucentPrepass(){return this.fillExtrusionMaterial.uniforms.layerOpacity.value<1}renderDepthPrepass(){this.tiles.traverse(e=>{Z(e)&&(e.material=this.maskedFillExtrusionVariant(e)?.depth??this.depthPrepassMaterial)}),this.renderer.render(this.scene,this.camera)}setOpacity(e){this.forEachFillExtrusionMaterial(i=>i.setLayerOpacity(e))}setVerticalGradient(e){this.forEachFillExtrusionMaterial(i=>i.setVerticalGradient(e))}getMaterial(e){return this.maskedFillExtrusionVariant(e)?.color??this.fillExtrusionMaterial}maskedFillExtrusionVariant(e){const i=e.userData.originalMaterial,t=i?.map;if(!t)return null;let a=this.maskedFillExtrusionMaterials.get(t);if(!a){const e={map:t,cutoff:i.alphaTest||.1};a={color:new R(e),depth:new U(e)},a.color.copyShadingFrom(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.set(t,a)}return a}forEachFillExtrusionMaterial(e){e(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.forEach(i=>e(i.color))}syncLighting(){const e=this.map,i=e.style?.light,t=i?.properties,a=i?.getCartesianPosition?.();if(t&&a)try{const i=t.get("intensity");this.directionalLight.intensity=i*Math.PI,this.ambientLight.intensity=(1-i)*Math.PI;const s={x:-a[0],y:a[1],z:a[2]};this.directionalLight.position.set(s.x,s.y,s.z);const r="viewport"===t.get("anchor")?-(e._camera.transform.bearingInRadians??0):0,n=t.get("color");this.forEachFillExtrusionMaterial(e=>e.setLight(s,r,i,n))}catch{}}applyMaterials(){this.forEachFillExtrusionMaterial(e=>e.setDiffuseColor(this.diffuseColor)),this.tiles.traverse(e=>{Z(e)&&(e.material=this.getMaterial(e))})}},q=["!",["coalesce",["get","has_landmark"],!1]],X=class{map;layerID="orbis-3d-landmarks";basemapBuildingLayerID="3D - Building";modelsLayer;layerMinZoom;layerMaxZoom;displayMode;constructor(e,t={}){this.map=e,this.layerMinZoom=t.minZoom,this.layerMaxZoom=t.maxZoom,this.displayMode=t.displayMode??"inherited";const a={id:this.layerID,type:"models",source:this.layerID,minzoom:t.minZoom??10,maxzoom:t.maxZoom??22},s={type:"models",tiles:[T()],minzoom:15,maxzoom:15,withCredentials:t.withCredentials??i()};this.modelsLayer=new N(a,s),this.modelsLayer.visible=t.visible??!0,this.map.addStyleChangeHandler({onStyleChanged:()=>this.install()}),this.installWhenReady()}installWhenReady(){this.map.mapReady?this.install():this.map.mapLibreMap.once("styledata",()=>this.installWhenReady())}get layer(){return this.modelsLayer}getDisplayMode(){return this.displayMode}async setDisplayMode(e){this.displayMode=e,this.map.mapReady&&this.applyDisplayMode()}isVisible(){return this.modelsLayer.visible}async setVisible(e){this.modelsLayer.visible=e,this.map.mapReady&&this.map.mapLibreMap.triggerRepaint()}install(){const e=this.map.mapLibreMap;e.getLayer(this.layerID)||e.addLayer(this.modelsLayer,this.landmarkInsertBeforeID()),e.getLayer(this.basemapBuildingLayerID)&&e.setLayoutProperty(this.basemapBuildingLayerID,"visibility","visible"),this.alignZoomRangeWithBasemapBuildings(),this.excludeLandmarkBuildingsFromBasemap(),this.modelsLayer.source.onTileLoaded=()=>{this.resolveCurrentMaterialState().opacity<1&&this.applyDisplayMode()},this.applyDisplayMode()}landmarkInsertBeforeID(){const e=a.poi;return this.map.mapLibreMap.getLayer(e)?e:void 0}alignZoomRangeWithBasemapBuildings(){const e=this.map.mapLibreMap.getLayer(this.basemapBuildingLayerID);e&&(this.modelsLayer.minzoom=this.layerMinZoom??e.minzoom??10,this.modelsLayer.maxzoom=this.layerMaxZoom??e.maxzoom??22)}excludeLandmarkBuildingsFromBasemap(){t.for(this.map).setClause(this.basemapBuildingLayerID,"landmarks3d.buildings",q)}readBasemapBuildingMaterial(){const e=this.map.mapLibreMap;if(!e.getLayer(this.basemapBuildingLayerID))return null;const i=(e=>{if("string"==typeof e)return e;if(Array.isArray(e)&&"match"===e[0]){const i=e.at(-1);if("string"==typeof i)return i}return null})(e.getPaintProperty(this.basemapBuildingLayerID,"fill-extrusion-color"));if(!i)return null;const t=e.getPaintProperty(this.basemapBuildingLayerID,"fill-extrusion-opacity"),a=e.getPaintProperty(this.basemapBuildingLayerID,"fill-extrusion-vertical-gradient");return{diffuseColor:i,..."number"==typeof t&&{opacity:t},..."boolean"==typeof a&&{verticalGradient:a}}}resolveCurrentMaterialState(){const e="inherited"===this.displayMode?this.readBasemapBuildingMaterial():null;return((e,i)=>{switch(e){case"dark":return z;case"light":return S;default:return{...S,...i}}})(this.displayMode,e)}applyDisplayMode(){const e=this.resolveCurrentMaterialState();this.modelsLayer.diffuseColor=e.diffuseColor,this.modelsLayer.setOpacity(e.opacity),this.modelsLayer.setVerticalGradient(e.verticalGradient),this.map.mapLibreMap.triggerRepaint()}};export{X as Landmarks3D,N as ModelsLayer,$ as ModelsSource,T as buildLandmarksTileURL};
1
+ import{TomTomConfig,isProxyCredentialsMode}from"@tomtom-org/maps-sdk/core";import{LayerFilterComposer,configWithKnobs,configWithoutKnob,mapStyleLayerIDs}from"@tomtom-org/maps-sdk/map";import{AmbientLight,BufferAttribute,Camera,Color,CustomBlending,DirectionalLight,DoubleSide,FrontSide,Group,LoaderUtils,LoadingManager,Matrix4,OneFactor,OneMinusSrcAlphaFactor,SRGBColorSpace,Scene,ShaderMaterial,Vector3,WebGLRenderer}from"three";import{MeshoptDecoder}from"three/examples/jsm/libs/meshopt_decoder.module.js";import{GLTFLoader}from"three/examples/jsm/loaders/GLTFLoader.js";import{KTX2Loader}from"three/examples/jsm/loaders/KTX2Loader.js";import{toCreasedNormals}from"three/examples/jsm/utils/BufferGeometryUtils.js";import{MercatorCoordinate}from"maplibre-gl";var ZOOM_RANGE={min:0,max:22,step:1,bounds:"hard"},landmarks3DKnobDefinitions={visible:{kind:"toggle",description:"The landmark models.",default:!0},color:{kind:"color",description:"Colour the landmarks are shaded in. Unset: the base map's 3D buildings' colour, following the map colours that restyle them."},opacity:{kind:"number",description:"Opacity of the landmarks, 0 to 1. Unset: the base map's 3D buildings' opacity.",range:{min:0,max:1,step:.05,bounds:"hard"}},verticalGradient:{kind:"toggle",description:"Whether the landmarks' walls darken towards the ground. Unset: as the base map's 3D buildings do."},minZoom:{kind:"number",description:"Lowest zoom the landmarks are drawn at. Unset: the base map's 3D buildings' one.",range:ZOOM_RANGE},maxZoom:{kind:"number",description:"Highest whole zoom level the landmarks are drawn at, up to the next one. Unset: the base map's 3D buildings' one.",range:ZOOM_RANGE}},landmarks3DKnobCatalogue=/* @__PURE__ */(()=>Object.keys(landmarks3DKnobDefinitions).map(e=>({id:e,...landmarks3DKnobDefinitions[e]})))(),landmarks3DKnobIds=/* @__PURE__ */landmarks3DKnobCatalogue.map(({id:e})=>e),buildLandmarksTileURL=()=>{const{commonBaseURL:e,apiKey:i}=TomTomConfig.instance.get();return`${e}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${i?`key=${i}&`:""}apiVersion=2`},STREET_LIGHT_LOOK={visible:!0,color:"hsl(38, 6%, 90%)",opacity:.7,verticalGradient:!0,minZoom:10,maxZoom:22},VERTEX_SHADER="\n attribute float fillExtrusionGradient;\n varying vec3 vWorldNormal;\n varying float vGradient;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n vGradient = fillExtrusionGradient;\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n",ALPHA_MASK_FRAGMENT_CHUNK="\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n",ALPHA_MASK_MAIN_CHUNK="\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n",FRAGMENT_SHADER=`\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n uniform float verticalGradient;\n varying vec3 vWorldNormal;\n varying float vGradient;\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n vec3 normal = normalize(vWorldNormal);\n // Double-sided (alpha-masked) surfaces show their interior through carved openings;\n // flip backface normals so those interiors shade like the facade instead of going dark.\n if (!gl_FrontFacing) {\n normal = -normal;\n }\n // Extrusions have only vertical walls and flat roofs, so a sloped landmark roof would shade\n // darker than every basemap roof around it: up-facing normals light like a flat roof, the\n // rest like a wall, blended for curved surfaces.\n vec3 flattened = vec3(normal.xy, 0.0);\n vec3 wallNormal = flattened / max(length(flattened), 1e-5);\n float upness = smoothstep(0.4, 0.8, normal.z);\n normal = normalize(mix(wallNormal, vec3(0.0, 0.0, 1.0), upness));\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n vec3 color = diffuseColor + vec3(0.03);\n float directional = clamp(dot(normal, lightPosition), 0.0, 1.0);\n directional = mix(1.0 - lightIntensity, max(1.0 - colorValue + lightIntensity, 1.0), directional);\n if (abs(normal.y) > 0.001) {\n directional *=\n (1.0 - verticalGradient) +\n verticalGradient * clamp(vGradient, mix(0.7, 0.98, 1.0 - lightIntensity), 1.0);\n }\n vec3 shaded = clamp(\n color * directional * lightColor,\n mix(vec3(0.0), vec3(0.3), vec3(1.0) - lightColor),\n vec3(1.0)\n );\n gl_FragColor = vec4(shaded, 1.0) * layerOpacity;\n }\n`,DEFAULT_LIGHT_POSITION=/* @__PURE__ */new Vector3(.2875,-.498,.996),parsedColor=/* @__PURE__ */new Color,srgbColor=/* @__PURE__ */new Color,FillExtrusionMaterial=class extends ShaderMaterial{constructor(e){super({uniforms:{diffuseColor:{value:new Vector3(1,1,1)},lightPosition:{value:DEFAULT_LIGHT_POSITION.clone()},lightIntensity:{value:.5},lightColor:{value:new Vector3(1,1,1)},layerOpacity:{value:1},verticalGradient:{value:1},...e&&{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}}},...e&&{defines:{USE_ALPHA_MASK:""}},vertexShader:VERTEX_SHADER,fragmentShader:FRAGMENT_SHADER,side:e?DoubleSide:FrontSide,transparent:!0,blending:CustomBlending,blendSrc:OneFactor,blendDst:OneMinusSrcAlphaFactor})}copyShadingFrom(e){this.uniforms.diffuseColor.value.copy(e.uniforms.diffuseColor.value),this.uniforms.lightPosition.value.copy(e.uniforms.lightPosition.value),this.uniforms.lightIntensity.value=e.uniforms.lightIntensity.value,this.uniforms.lightColor.value.copy(e.uniforms.lightColor.value),this.uniforms.layerOpacity.value=e.uniforms.layerOpacity.value,this.uniforms.verticalGradient.value=e.uniforms.verticalGradient.value}setDiffuseColor(e){parsedColor.setStyle(e),parsedColor.getRGB(srgbColor,SRGBColorSpace),this.uniforms.diffuseColor.value.set(srgbColor.r,srgbColor.g,srgbColor.b)}setLayerOpacity(e){this.uniforms.layerOpacity.value=e}setVerticalGradient(e){this.uniforms.verticalGradient.value=e?1:0}setLight(e,i,t,a){const r=Math.cos(i),s=Math.sin(i);this.uniforms.lightPosition.value.set(e.x*r-e.y*s,e.x*s+e.y*r,e.z),this.uniforms.lightIntensity.value=t,this.uniforms.lightColor.value.set(a.r,a.g,a.b)}},FillExtrusionDepthMaterial=class extends ShaderMaterial{constructor(e){super({...e&&{uniforms:{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}},defines:{USE_ALPHA_MASK:""}},vertexShader:VERTEX_SHADER,fragmentShader:`\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n gl_FragColor = vec4(0.0);\n }\n `,side:e?DoubleSide:FrontSide,colorWrite:!1})}},isMesh=e=>!0===e.isMesh,MERCATOR_UNITS_PER_METRE=1/(2*Math.PI*6378137),CREASE_ANGLE_RADIANS=Math.PI/6,ModelsSource=class{type="models";id;minzoom;maxzoom;map;tiles;scene;loadedTiles=/* @__PURE__ */new Set;sceneOrigin=null;onTileLoaded;ktx2Loader;transcoderReady;gltfLoader;specification;withCredentials;coveringTileKeys=/* @__PURE__ */new Set;constructor(e,i){this.id=e,this.minzoom=i.minzoom??0,this.maxzoom=i.maxzoom??22,this.tiles=i.tiles,this.specification=i,this.withCredentials=i.withCredentials??!1,this.scene=new Group,this.ktx2Loader=new KTX2Loader(new LoadingManager).setCrossOrigin("anonymous").setTranscoderPath(i.transcoderPath??""),this.gltfLoader=(new GLTFLoader).setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(MeshoptDecoder)}onAdd(e,i){this.map=e,this.ktx2Loader.detectSupport(i)}updateTiles(){if(0===this.tiles.length)return;const e=this.coveringTiles();this.coveringTileKeys=new Set(e.map(e=>e.key));for(const i of e)this.loadTile(i);this.showCoveringTilesOnly()}showCoveringTilesOnly(){for(const e of this.scene.children)e.visible=this.coveringTileKeys.has(e.userData.key)}coveringTiles(){const e={tileSize:this.map._camera.transform.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,terrain:this.map.terrain};return this.map.coveringTiles(e)}loadTile(e){if(this.loadedTiles.has(e.key))return;this.loadedTiles.add(e.key);const i=e.canonical.url(this.tiles,this.map.getPixelRatio(),this.specification.scheme);this.fetchTile(i).then(i=>{i&&(prepareTileMeshes(i),this.rebaseToSceneOrigin(i),i.userData={isTile:!0,key:e.key},i.visible=this.coveringTileKeys.has(e.key),this.scene.add(i),this.map.triggerRepaint(),this.onTileLoaded?.(i,e.key))}).catch(i=>{console.warn(`Problem with loading ${e.toString()}: ${i?.message??"unknown"}`)})}rebaseToSceneOrigin(e){const i=[];if(e.traverse(e=>{isMesh(e)&&i.push(e)}),0===i.length)return;const t=i[0].position,a={x:Math.round(t.x),y:Math.round(t.y)};if(this.sceneOrigin){if(Math.hypot(t.x-this.sceneOrigin.x,t.y-this.sceneOrigin.y)>1e5){const e=a.x-this.sceneOrigin.x,i=a.y-this.sceneOrigin.y;this.scene.traverse(t=>{isMesh(t)&&(t.position.x-=e,t.position.y-=i)}),this.sceneOrigin=a}}else this.sceneOrigin=a;for(const r of i)r.position.x-=this.sceneOrigin.x,r.position.y-=this.sceneOrigin.y}async fetchTile(e){const i=await fetch(e,this.withCredentials?{credentials:"include"}:void 0);if(204===i.status||404===i.status)return null;if(403===i.status&&"InvalidReferer"!==await(async e=>(await e.json().catch(()=>{}))?.detailedError?.code)(i))throw new Error("tile request failed with 403: Orbis 3D Landmarks is in Private Preview (https://docs.tomtom.com/legal/private-preview), and the API key needs access to it: request it from TomTom Sales at https://www.tomtom.com/contact-sales/?source_app=docs-portal&source_product=map-display-api-3d");if(!i.ok)throw new Error(`tile request failed with ${i.status}`);const t=await i.arrayBuffer();this.transcoderReady??=(async e=>{if(void 0!==this.specification.transcoderPath)return;const{resolveBundledTranscoderUrl:i}=await(import("./bundledTranscoder-IHtuhlDT.js"));e.setURLModifier(i)})(this.ktx2Loader.manager),await this.transcoderReady;const{scene:a}=await this.gltfLoader.parseAsync(t,LoaderUtils.extractUrlBase(e));return a}},prepareTileMeshes=e=>{e.traverse(e=>{if(!isMesh(e))return;const i=e.material;i.alphaTest=.1,i.side=DoubleSide,e.userData.originalMaterial=i,e.geometry=toCreasedNormals(e.geometry,CREASE_ANGLE_RADIANS),bakeGradientAttribute(e)})},bakeGradientAttribute=e=>{const i=e.geometry,t=i.getAttribute("position");i.computeBoundingBox();const a=i.boundingBox?.max.z??0,r=a>0?Math.sqrt(a/150)/a:0,s=new Float32Array(t.count);for(let o=0;o<t.count;o++)s[o]=t.getZ(o)*r;i.setAttribute("fillExtrusionGradient",new BufferAttribute(s,1))},toRadians=e=>e*Math.PI/180,ModelsLayer=class{id;type="custom";renderingMode="3d";renderer;scene;camera;tiles;directionalLight;ambientLight;map;source;minzoom=0;maxzoom=22;visible=!0;diffuseColor="#ffffff";fillExtrusionMaterial=new FillExtrusionMaterial;depthPrepassMaterial=new FillExtrusionDepthMaterial;maskedFillExtrusionMaterials=/* @__PURE__ */new Map;tilesMainMatrix=/* @__PURE__ */new Float64Array(16);tilesStale=!0;markTilesStale=()=>{this.tilesStale=!0};constructor(e,i){this.id=e.id,this.source=new ModelsSource(this.id,i),this.tiles=this.source.scene,this.updateProperties(e)}updateProperties(e){this.visible="none"!==e.layout?.visibility,this.diffuseColor=e.paint?.["models-base-color"]??"#ffffff",this.minzoom=e.minzoom??0,this.maxzoom=e.maxzoom??22}setDirectionalLight(e,i,t){this.directionalLight.position.setFromSphericalCoords(e,toRadians(i),toRadians(t))}onAdd(e,i){this.map=e,this.scene=new Scene,this.tiles=this.source.scene,this.camera=new Camera,this.directionalLight=new DirectionalLight(16777215,.5),this.setDirectionalLight(1.15,90,0),this.scene.add(this.directionalLight),this.ambientLight=new AmbientLight(16777215,.1),this.scene.add(this.ambientLight),this.scene.add(this.tiles),this.renderer=new WebGLRenderer({canvas:e.getCanvas(),context:i,antialias:!0}),this.renderer.autoClear=!1,this.source.onAdd(e,this.renderer),this.tilesStale=!0,e.off("data",this.markTilesStale),e.on("data",this.markTilesStale)}render(e,i){const t=this.map._camera.transform.tileZoom,a=t>=this.minzoom&&t<=this.maxzoom;this.tiles.visible=this.visible&&a,this.visible&&a&&(this.syncLighting(),this.updateTilesWhenStale(i.defaultProjectionData.mainMatrix),this.groundLandmarksOnTerrain(),this.alignCameraToMap(i),this.renderer.resetState(),this.needsTranslucentPrepass()&&this.renderDepthPrepass(),this.applyMaterials(),this.renderer.render(this.scene,this.camera))}updateTilesWhenStale(e){(this.tilesMainMatrix.some((i,t)=>i!==e[t])||this.tilesStale)&&(this.tilesMainMatrix.set(e),this.tilesStale=!1,this.source.updateTiles())}groundLandmarksOnTerrain(){for(const e of this.tiles.children)e.visible&&e.traverse(e=>{isMesh(e)&&this.groundMeshOnTerrain(e)})}groundMeshOnTerrain(e){const i=this.terrainSamplePoints(e);if(!i)return;let t=null;for(const a of i){const i=this.map.queryTerrainElevation(a);if(null===i)return void(e.position.z=0);(null===t||i<t)&&(t=i)}null!==t&&(e.position.z=t-.5)}terrainSamplePoints(e){let i=e.userData.terrainSamplePoints;if(!i){e.geometry.boundingBox||e.geometry.computeBoundingBox();const t=e.geometry.boundingBox;if(!t||t.isEmpty())return null;const a=e.position.x+this.sceneOrigin.x,r=e.position.y+this.sceneOrigin.y,s=(t.min.x+t.max.x)/2,o=(t.min.y+t.max.y)/2;i=[[s,o],[t.min.x,t.min.y],[t.min.x,t.max.y],[t.max.x,t.min.y],[t.max.x,t.max.y],[s,t.min.y],[s,t.max.y],[t.min.x,o],[t.max.x,o]].map(([e,i])=>new MercatorCoordinate(.5+(a+e)*MERCATOR_UNITS_PER_METRE,.5-(r+i)*MERCATOR_UNITS_PER_METRE).toLngLat().toArray()),e.userData.terrainSamplePoints=i}return i}alignCameraToMap(e){const i=MERCATOR_UNITS_PER_METRE/Math.cos(toRadians(this.map.getCenter().lat)),t=.5+MERCATOR_UNITS_PER_METRE*this.sceneOrigin.x,a=.5-MERCATOR_UNITS_PER_METRE*this.sceneOrigin.y,r=new Matrix4;r.set(MERCATOR_UNITS_PER_METRE,0,0,t,0,-MERCATOR_UNITS_PER_METRE,0,a,0,0,i,0,0,0,0,1);const s=(new Matrix4).fromArray(e.defaultProjectionData.mainMatrix);this.camera.projectionMatrix.copy(s).multiply(r)}get sceneOrigin(){return this.source.sceneOrigin??{x:0,y:0}}needsTranslucentPrepass(){return this.fillExtrusionMaterial.uniforms.layerOpacity.value<1}renderDepthPrepass(){this.tiles.traverse(e=>{isMesh(e)&&(e.material=this.maskedFillExtrusionVariant(e)?.depth??this.depthPrepassMaterial)}),this.renderer.render(this.scene,this.camera)}setOpacity(e){this.forEachFillExtrusionMaterial(i=>i.setLayerOpacity(e))}setVerticalGradient(e){this.forEachFillExtrusionMaterial(i=>i.setVerticalGradient(e))}getMaterial(e){return this.maskedFillExtrusionVariant(e)?.color??this.fillExtrusionMaterial}maskedFillExtrusionVariant(e){const i=e.userData.originalMaterial,t=i?.map;if(!t)return null;let a=this.maskedFillExtrusionMaterials.get(t);if(!a){const e={map:t,cutoff:i.alphaTest||.1};a={color:new FillExtrusionMaterial(e),depth:new FillExtrusionDepthMaterial(e)},a.color.copyShadingFrom(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.set(t,a)}return a}forEachFillExtrusionMaterial(e){e(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.forEach(i=>e(i.color))}syncLighting(){const e=this.map,i=e.style?.light,t=i?.properties,a=i?.getCartesianPosition?.();if(t&&a)try{const i=t.get("intensity");this.directionalLight.intensity=i*Math.PI,this.ambientLight.intensity=(1-i)*Math.PI;const r={x:-a[0],y:a[1],z:a[2]};this.directionalLight.position.set(r.x,r.y,r.z);const s="viewport"===t.get("anchor")?-(e._camera.transform.bearingInRadians??0):0,o=t.get("color");this.forEachFillExtrusionMaterial(e=>e.setLight(r,s,i,o))}catch{}}applyMaterials(){this.forEachFillExtrusionMaterial(e=>e.setDiffuseColor(this.diffuseColor)),this.tiles.traverse(e=>{isMesh(e)&&(e.material=this.getMaterial(e))})}},NO_LANDMARK_FILTER_CLAUSE=["!",["coalesce",["get","has_landmark"],!1]],Landmarks3D=class{map;layerID="orbis-3d-landmarks";basemapBuildingLayerID="3D - Building";modelsLayer;config;renderedLook;constructor(e,{withCredentials:i,...t}={}){this.map=e,this.config=t;const a={id:this.layerID,type:"models",source:this.layerID},r={type:"models",tiles:[buildLandmarksTileURL()],minzoom:15,maxzoom:15,withCredentials:i??isProxyCredentialsMode()};this.modelsLayer=new ModelsLayer(a,r),this.render(),this.map.addStyleChangeHandler({onStyleChanged:()=>this.install()}),this.map.mapLibreMap.on("styledata",()=>{var e,i;this.map.mapReady&&(e=this.look(),void 0===(i=this.renderedLook)||!Object.keys(e).every(t=>e[t]===i[t]))&&this.render()}),this.installWhenReady()}installWhenReady(){this.map.mapReady?this.install():this.map.mapLibreMap.once("styledata",()=>this.installWhenReady())}get layer(){return this.modelsLayer}getConfig(){return structuredClone(this.config)}applyConfig(e){this.config=e,this.render()}updateConfig(e){this.applyConfig({...this.config,...e})}resetConfig(){this.applyConfig({})}isVisible(){return!1!==this.config.visible}setVisible(e){this.updateConfig({visible:e})}_knobValue(e){return this.look()[e]}_setKnobValues(e){this.applyConfig(configWithKnobs(this.config,e))}_resetKnobValue(e){this.applyConfig(configWithoutKnob(this.config,e))}install(){const e=this.map.mapLibreMap;e.getLayer(this.layerID)||e.addLayer(this.modelsLayer,this.landmarkInsertBeforeID()),this.excludeLandmarkBuildingsFromBasemap(),this.modelsLayer.source.onTileLoaded=()=>{this.look().opacity<1&&this.render()},this.render()}landmarkInsertBeforeID(){const e=mapStyleLayerIDs.poi;return this.map.mapLibreMap.getLayer(e)?e:void 0}excludeLandmarkBuildingsFromBasemap(){LayerFilterComposer.for(this.map).setClause(this.basemapBuildingLayerID,"landmarks3d.buildings",NO_LANDMARK_FILTER_CLAUSE)}readBasemapBuildingsLook(){const e=this.map.mapLibreMap,i=e.getLayer(this.basemapBuildingLayerID);if(!i)return{};const t=i=>e.getPaintProperty(this.basemapBuildingLayerID,i),a=(e=>{if("string"==typeof e)return e;if(Array.isArray(e)&&"match"===e[0]){const i=e.at(-1);if("string"==typeof i)return i}return null})(t("fill-extrusion-color")),r=t("fill-extrusion-opacity"),s=t("fill-extrusion-vertical-gradient");return{...a&&{color:a},..."number"==typeof r&&{opacity:r},..."boolean"==typeof s&&{verticalGradient:s},...void 0!==i.minzoom&&{minZoom:i.minzoom},...void 0!==i.maxzoom&&{maxZoom:i.maxzoom}}}look(){const e=this.map.mapReady?this.readBasemapBuildingsLook():{};return((e,i)=>{const t={...STREET_LIGHT_LOOK,...i},a=i=>e[i]??t[i];return{visible:a("visible"),color:a("color"),opacity:a("opacity"),verticalGradient:a("verticalGradient"),minZoom:a("minZoom"),maxZoom:a("maxZoom")}})(this.config,e)}render(){const e=this.look();this.renderedLook=e,this.modelsLayer.visible=e.visible,this.modelsLayer.minzoom=e.minZoom,this.modelsLayer.maxzoom=e.maxZoom,this.modelsLayer.diffuseColor=e.color,this.modelsLayer.setOpacity(e.opacity),this.modelsLayer.setVerticalGradient(e.verticalGradient),this.map.mapReady&&this.map.mapLibreMap.triggerRepaint()}};export{Landmarks3D,ModelsLayer,ModelsSource,buildLandmarksTileURL,landmarks3DKnobCatalogue,landmarks3DKnobIds};
@@ -1,7 +1,7 @@
1
1
  [
2
2
  {
3
3
  "name": "maplibre-gl",
4
- "version": "6.12.0",
4
+ "version": "6.13.0",
5
5
  "license": "BSD-3-Clause",
6
6
  "bundled": false,
7
7
  "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
package/package.json CHANGED
@@ -2,25 +2,28 @@
2
2
  "name": "@tomtom-org/maps-sdk-plugin-landmarks-3d",
3
3
  "author": "TomTom International B.V.",
4
4
  "license": "See in LICENSE.txt",
5
- "version": "0.0.12",
5
+ "version": "0.1.0",
6
6
  "type": "module",
7
7
  "description": "Plugin to render TomTom Orbis 3D Landmarks (GLB tiles) on a TomTomMap with Three.js",
8
8
  "types": "dist/index.d.ts",
9
+ "sideEffects": false,
9
10
  "peerDependencies": {
10
- "@tomtom-org/maps-sdk": ">=0.64.0 <1.0.0 || >=1.0.0-rc.0 <1.0.0",
11
- "maplibre-gl": "^6.12.0",
11
+ "@tomtom-org/maps-sdk": ">=1.0.0-rc.2 <1.0.0",
12
+ "maplibre-gl": "^6.13.0",
12
13
  "three": "^0.170.0"
13
14
  },
14
15
  "devDependencies": {
16
+ "@playwright/test": "1.60.0",
15
17
  "@tsconfig/recommended": "^1.0.13",
16
18
  "@types/node": "^26.6.4",
17
19
  "@types/three": "^0.170.0",
18
- "@vitest/coverage-v8": "^4.1.11",
19
- "rolldown": "^1.2.12",
20
+ "@vitest/coverage-v8": "^5.0.3",
21
+ "rolldown": "^1.2.13",
20
22
  "typescript": "^6.0.3",
21
- "vite": "^8.3.2",
22
- "vitest": "^4.1.11",
23
- "@tomtom-org/maps-sdk": "1.0.0-rc.1"
23
+ "vite": "8.3.3",
24
+ "vitest": "^5.0.3",
25
+ "@testing/core-utils": "0.0.0",
26
+ "@tomtom-org/maps-sdk": "1.0.0-rc.3"
24
27
  },
25
28
  "files": [
26
29
  "dist",
@@ -41,6 +44,11 @@
41
44
  "build:watch": "vite build --watch --config ../plugin-vite-config.ts",
42
45
  "test": "vitest run",
43
46
  "test:coverage": "vitest run --coverage",
47
+ "test:e2e": "playwright test --config e2e-tests/playwright.config.ts",
48
+ "test:e2e:ui": "playwright test --config e2e-tests/playwright.config.ts --ui",
49
+ "test:e2e:update-snapshots": "playwright test --config e2e-tests/playwright.config.ts --update-snapshots",
50
+ "test:e2e:server": "vite --config e2e-tests/vite.config.ts",
51
+ "test:e2e:install": "playwright install chromium",
44
52
  "type-check": "tsc --noEmit"
45
53
  }
46
54
  }