@tomtom-org/maps-sdk-plugin-landmarks-3d 0.0.2 → 0.0.4

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
@@ -155,6 +155,7 @@ export declare class Landmarks3D {
155
155
  */
156
156
  setVisible(visible: boolean): Promise<void>;
157
157
  private install;
158
+ private landmarkInsertBeforeID;
158
159
  private alignZoomRangeWithBasemapBuildings;
159
160
  private excludeLandmarkBuildingsFromBasemap;
160
161
  private readBasemapBuildingMaterial;
@@ -203,6 +204,15 @@ export declare type Landmarks3DOptions = {
203
204
  * with it (falls back to `22` when that layer is absent).
204
205
  */
205
206
  maxZoom?: number;
207
+ /**
208
+ * Send `credentials: 'include'` with each tile request so a proxy session
209
+ * cookie travels with it. By default this tracks the SDK's proxy config, so
210
+ * tiles behave like every other SDK request; set it explicitly only to
211
+ * override that.
212
+ * @defaultValue Auto-detected — `true` when the SDK is configured for a
213
+ * credentialed proxy (no `apiKey` + non-default `commonBaseURL`).
214
+ */
215
+ withCredentials?: boolean;
206
216
  };
207
217
 
208
218
  /**
@@ -292,11 +302,13 @@ export declare class ModelsSource {
292
302
  private readonly ktx2Loader;
293
303
  private readonly gltfLoader;
294
304
  private readonly specification;
305
+ private readonly withCredentials;
295
306
  constructor(id: string, options: ModelsSourceSpecification);
296
307
  onAdd(map: Map_2, renderer: WebGLRenderer): void;
297
308
  updateTiles(): void;
298
309
  private coveringTiles;
299
310
  private loadTile;
311
+ private fetchTile;
300
312
  }
301
313
 
302
314
  /**
@@ -319,6 +331,13 @@ export declare interface ModelsSourceSpecification {
319
331
  * self-hosted copy when external CDNs are not an option (e.g. strict CSP).
320
332
  */
321
333
  transcoderPath?: string;
334
+ /**
335
+ * Send `credentials: 'include'` with each tile request so a session cookie
336
+ * (e.g. the demo-BFF proxy's) travels with it. Leave off for direct
337
+ * `api.tomtom.com` access, where credentialed CORS would fail.
338
+ * @defaultValue `false`
339
+ */
340
+ withCredentials?: boolean;
322
341
  }
323
342
 
324
343
  export { }
package/dist/index.es.js CHANGED
@@ -1,2 +1,2 @@
1
- import{TomTomConfig as e}from"@tomtom-org/maps-sdk/core";import{AmbientLight as i,Camera as t,Color as a,CustomBlending as s,DirectionalLight as o,DoubleSide as r,FrontSide as n,Group as l,Matrix4 as h,OneFactor as m,OneMinusSrcAlphaFactor as d,REVISION as c,SRGBColorSpace as p,Scene as u,ShaderMaterial as f,Vector3 as y,WebGLRenderer as g}from"three";import{MeshoptDecoder as v}from"three/examples/jsm/libs/meshopt_decoder.module.js";import{GLTFLoader as M}from"three/examples/jsm/loaders/GLTFLoader.js";import{KTX2Loader as L}from"three/examples/jsm/loaders/KTX2Loader.js";import{toCreasedNormals as x}from"three/examples/jsm/utils/BufferGeometryUtils.js";var b={diffuse:"hsl(38, 6%, 90%)"},C="hsl(210, 9%, 17%)",k=.82,S=()=>{const{commonBaseURL:i,apiKey:t}=e.instance.get();return`${i}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${t?`key=${t}&`:""}apiVersion=2`},z="\n varying vec3 vWorldNormal;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n",D="\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n",P="\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n",w=`\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n varying vec3 vWorldNormal;\n ${D}\n\n void main() {\n ${P}\n vec3 normal = normalize(vWorldNormal);\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n // Flat ambient term lifting the paint colour, tuned to match the basemap buildings.\n vec3 color = diffuseColor + vec3(0.145);\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 // Side faces (walls) darken to 0.9; roofs (normal +Z) stay full bright.\n float wallFactor = 1.0 - abs(normal.z);\n directional *= mix(1.0, 0.9, wallFactor);\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`,E=new y(.2875,-.498,.996),I=new a,A=new a,B=class extends f{constructor(e){super({uniforms:{diffuseColor:{value:new y(1,1,1)},lightPosition:{value:E.clone()},lightIntensity:{value:.5},lightColor:{value:new y(1,1,1)},layerOpacity:{value:1},...e&&{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}}},...e&&{defines:{USE_ALPHA_MASK:""}},vertexShader:z,fragmentShader:w,side:e?r:n,transparent:!0,blending:s,blendSrc:m,blendDst:d})}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}setDiffuseColor(e){I.setStyle(e),I.getRGB(A,p),this.uniforms.diffuseColor.value.set(A.r,A.g,A.b)}setLayerOpacity(e){this.uniforms.layerOpacity.value=e}setLight(e,i,t,a){const s=Math.cos(i),o=Math.sin(i);this.uniforms.lightPosition.value.set(e.x*s-e.y*o,e.x*o+e.y*s,e.z),this.uniforms.lightIntensity.value=t,this.uniforms.lightColor.value.set(a.r,a.g,a.b)}},F=class extends f{constructor(e){super({...e&&{uniforms:{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}},defines:{USE_ALPHA_MASK:""}},vertexShader:z,fragmentShader:`\n ${D}\n void main() {\n ${P}\n gl_FragColor = vec4(0.0);\n }\n `,side:e?r:n,colorWrite:!1})}},T=e=>!0===e.isMesh,_=Math.PI/6,R=`https://unpkg.com/three@0.${c}.x/examples/jsm/libs/basis/`,O=class{type="models";id;minzoom;maxzoom;map;tiles;scene;loadedTiles=/* @__PURE__ */new Set;onTileLoaded;ktx2Loader;gltfLoader;specification;constructor(e,i){this.id=e,this.minzoom=i.minzoom??0,this.maxzoom=i.maxzoom??22,this.tiles=i.tiles,this.specification=i,this.scene=new l,this.ktx2Loader=(new L).setCrossOrigin("anonymous").setTranscoderPath(i.transcoderPath??R),this.gltfLoader=(new M).setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(v)}onAdd(e,i){this.map=e,this.ktx2Loader.detectSupport(i)}updateTiles(){if(0!==this.tiles.length)for(const e of this.coveringTiles())this.loadTile(e)}coveringTiles(){const e=this.map.transform;return this.map.coveringTiles({tileSize:e.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom})}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.gltfLoader.loadAsync(i).then(i=>{Z(i.scene),i.scene.userData={isTile:!0,key:e.key},this.scene.add(i.scene),this.onTileLoaded?.(i.scene,e.key)}).catch(i=>{const t=i?.response?.status;204!==t&&404!==t&&console.warn(`Problem with loading ${e.toString()}: ${i?.message??"unknown"}`)})}},Z=e=>{e.traverse(e=>{if(!T(e))return;const i=e.material;i.alphaTest=.1,i.side=r,e.userData.originalMaterial=i,e.geometry=x(e.geometry,_)})},j=1/40075016.68,U=e=>e*Math.PI/180,W=class{id;type="custom";renderingMode="3d";renderer;scene;camera;tiles;directionalLight;ambientLight;map;source;minzoom=0;maxzoom=22;visible=!0;diffuseColor="#ffffff";fillExtrusionMaterial=new B;depthPrepassMaterial=new F;maskedFillExtrusionMaterials=/* @__PURE__ */new Map;constructor(e,i){this.id=e.id,this.source=new O(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,U(i),U(t))}onAdd(e,a){this.map=e,this.scene=new u,this.tiles=this.source.scene,this.camera=new t,this.directionalLight=new o(16777215,.5),this.setDirectionalLight(1.15,90,0),this.scene.add(this.directionalLight),this.ambientLight=new i(16777215,.1),this.scene.add(this.ambientLight),this.scene.add(this.tiles),this.renderer=new g({canvas:e.getCanvas(),context:a,antialias:!0}),this.renderer.autoClear=!1,this.source.onAdd(e,this.renderer)}render(e,i){const t=this.map.transform.tileZoom,a=t>=this.minzoom&&t<=this.maxzoom;this.tiles.visible=this.visible&&a,this.visible&&a&&(this.syncLighting(),this.source.updateTiles(),this.alignCameraToMap(i),this.renderer.resetState(),this.needsTranslucentPrepass()&&this.renderDepthPrepass(),this.applyMaterials(),this.renderer.render(this.scene,this.camera),this.map.triggerRepaint())}alignCameraToMap(e){const i=this.map.getCenter().lat*Math.PI/180,t=j/Math.cos(i),a=new h;a.set(j,0,0,.5,0,-2.4953202340126887e-8,0,.5,0,0,t,0,0,0,0,1);const s=(new h).fromArray(e.defaultProjectionData.mainMatrix);this.camera.projectionMatrix.copy(s).multiply(a)}needsTranslucentPrepass(){return this.fillExtrusionMaterial.uniforms.layerOpacity.value<1}renderDepthPrepass(){this.tiles.traverse(e=>{T(e)&&(e.material=this.maskedFillExtrusionVariant(e)?.depth??this.depthPrepassMaterial)}),this.renderer.render(this.scene,this.camera)}setOpacity(e){this.forEachFillExtrusionMaterial(i=>i.setLayerOpacity(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 B(e),depth:new F(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?.properties;if(i)try{const t=.5;this.directionalLight.intensity=t*Math.PI,this.ambientLight.intensity=(1-t)*Math.PI;const a=i.get("position");this.directionalLight.position.set(a.x,a.y,a.z);const s="viewport"===i.get("anchor")?e.transform.bearingInRadians??0:0,o=i.get("color");this.forEachFillExtrusionMaterial(e=>e.setLight(a,s,t,o))}catch{}}applyMaterials(){this.forEachFillExtrusionMaterial(e=>e.setDiffuseColor(this.diffuseColor)),this.tiles.traverse(e=>{T(e)&&(e.material=this.getMaterial(e))})}},$=["!",["coalesce",["get","has_landmark"],!1]],K=class{map;layerID="orbis-3d-landmarks";basemapBuildingLayerID="3D - Building";modelsLayer;layerMinZoom;layerMaxZoom;displayMode;constructor(e,i={}){this.map=e,this.layerMinZoom=i.minZoom,this.layerMaxZoom=i.maxZoom,this.displayMode=i.displayMode??"inherited";const t={id:this.layerID,type:"models",source:this.layerID,minzoom:i.minZoom??10,maxzoom:i.maxZoom??22},a={type:"models",tiles:[S()],minzoom:15,maxzoom:15};this.modelsLayer=new W(t,a),this.modelsLayer.visible=i.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),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()}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(){const e=this.map.mapLibreMap;if(!e.getLayer(this.basemapBuildingLayerID))return;const i=e.getFilter(this.basemapBuildingLayerID);if(i&&JSON.stringify(i).includes(JSON.stringify($)))return;const t=i?["all",i,$]:$;e.setFilter(this.basemapBuildingLayerID,t)}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"));return i?{diffuse:i}:null}resolveCurrentMaterialState(){const e="inherited"===this.displayMode?this.readBasemapBuildingMaterial():null;return((e,i)=>{switch(e){case"dark":return{diffuseColor:C,opacity:k};case"light":return{diffuseColor:b.diffuse,opacity:k};default:return{diffuseColor:(i??b).diffuse,opacity:k}}})(this.displayMode,e)}applyDisplayMode(){const e=this.resolveCurrentMaterialState();this.modelsLayer.diffuseColor=e.diffuseColor,this.modelsLayer.setOpacity(e.opacity),this.map.mapLibreMap.triggerRepaint()}};export{K as Landmarks3D,W as ModelsLayer,O as ModelsSource,S as buildLandmarksTileURL};
1
+ import{TomTomConfig as e,isProxyCredentialsMode as i}from"@tomtom-org/maps-sdk/core";import{mapStyleLayerIDs as t}from"@tomtom-org/maps-sdk/map";import{AmbientLight as a,Camera as s,Color as r,CustomBlending as o,DirectionalLight as n,DoubleSide as l,FrontSide as h,Group as m,LoaderUtils as d,Matrix4 as c,OneFactor as p,OneMinusSrcAlphaFactor as u,REVISION as f,SRGBColorSpace as y,Scene as g,ShaderMaterial as v,Vector3 as M,WebGLRenderer as L}from"three";import{MeshoptDecoder as x}from"three/examples/jsm/libs/meshopt_decoder.module.js";import{GLTFLoader as b}from"three/examples/jsm/loaders/GLTFLoader.js";import{KTX2Loader as C}from"three/examples/jsm/loaders/KTX2Loader.js";import{toCreasedNormals as w}from"three/examples/jsm/utils/BufferGeometryUtils.js";var k={diffuse:"hsl(38, 6%, 90%)"},D="hsl(210, 9%, 17%)",S=.82,z=()=>{const{commonBaseURL:i,apiKey:t}=e.instance.get();return`${i}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${t?`key=${t}&`:""}apiVersion=2`},P="\n varying vec3 vWorldNormal;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n",I="\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n",B="\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n",E=`\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n varying vec3 vWorldNormal;\n ${I}\n\n void main() {\n ${B}\n vec3 normal = normalize(vWorldNormal);\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n // Flat ambient term lifting the paint colour, tuned to match the basemap buildings.\n vec3 color = diffuseColor + vec3(0.145);\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 // Side faces (walls) darken to 0.9; roofs (normal +Z) stay full bright.\n float wallFactor = 1.0 - abs(normal.z);\n directional *= mix(1.0, 0.9, wallFactor);\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`,T=new M(.2875,-.498,.996),A=new r,F=new r,_=class extends v{constructor(e){super({uniforms:{diffuseColor:{value:new M(1,1,1)},lightPosition:{value:T.clone()},lightIntensity:{value:.5},lightColor:{value:new M(1,1,1)},layerOpacity:{value:1},...e&&{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}}},...e&&{defines:{USE_ALPHA_MASK:""}},vertexShader:P,fragmentShader:E,side:e?l:h,transparent:!0,blending:o,blendSrc:p,blendDst:u})}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}setDiffuseColor(e){A.setStyle(e),A.getRGB(F,y),this.uniforms.diffuseColor.value.set(F.r,F.g,F.b)}setLayerOpacity(e){this.uniforms.layerOpacity.value=e}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)}},R=class extends v{constructor(e){super({...e&&{uniforms:{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}},defines:{USE_ALPHA_MASK:""}},vertexShader:P,fragmentShader:`\n ${I}\n void main() {\n ${B}\n gl_FragColor = vec4(0.0);\n }\n `,side:e?l:h,colorWrite:!1})}},O=e=>!0===e.isMesh,Z=Math.PI/6,U=`https://unpkg.com/three@0.${f}.x/examples/jsm/libs/basis/`,j=class{type="models";id;minzoom;maxzoom;map;tiles;scene;loadedTiles=/* @__PURE__ */new Set;onTileLoaded;ktx2Loader;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 m,this.ktx2Loader=(new C).setCrossOrigin("anonymous").setTranscoderPath(i.transcoderPath??U),this.gltfLoader=(new b).setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(x)}onAdd(e,i){this.map=e,this.ktx2Loader.detectSupport(i)}updateTiles(){if(0!==this.tiles.length)for(const e of this.coveringTiles())this.loadTile(e)}coveringTiles(){const e=this.map.transform;return this.map.coveringTiles({tileSize:e.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom})}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&&($(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"}`)})}async fetchTile(e){const i=await fetch(e,this.withCredentials?{credentials:"include"}:void 0);if(204===i.status||404===i.status)return null;if(!i.ok)throw new Error(`tile request failed with ${i.status}`);const t=await i.arrayBuffer(),{scene:a}=await this.gltfLoader.parseAsync(t,d.extractUrlBase(e));return a}},$=e=>{e.traverse(e=>{if(!O(e))return;const i=e.material;i.alphaTest=.1,i.side=l,e.userData.originalMaterial=i,e.geometry=w(e.geometry,Z)})},W=1/40075016.68,K=e=>e*Math.PI/180,V=class{id;type="custom";renderingMode="3d";renderer;scene;camera;tiles;directionalLight;ambientLight;map;source;minzoom=0;maxzoom=22;visible=!0;diffuseColor="#ffffff";fillExtrusionMaterial=new _;depthPrepassMaterial=new R;maskedFillExtrusionMaterials=/* @__PURE__ */new Map;constructor(e,i){this.id=e.id,this.source=new j(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,K(i),K(t))}onAdd(e,i){this.map=e,this.scene=new g,this.tiles=this.source.scene,this.camera=new s,this.directionalLight=new n(16777215,.5),this.setDirectionalLight(1.15,90,0),this.scene.add(this.directionalLight),this.ambientLight=new a(16777215,.1),this.scene.add(this.ambientLight),this.scene.add(this.tiles),this.renderer=new L({canvas:e.getCanvas(),context:i,antialias:!0}),this.renderer.autoClear=!1,this.source.onAdd(e,this.renderer)}render(e,i){const t=this.map.transform.tileZoom,a=t>=this.minzoom&&t<=this.maxzoom;this.tiles.visible=this.visible&&a,this.visible&&a&&(this.syncLighting(),this.source.updateTiles(),this.alignCameraToMap(i),this.renderer.resetState(),this.needsTranslucentPrepass()&&this.renderDepthPrepass(),this.applyMaterials(),this.renderer.render(this.scene,this.camera),this.map.triggerRepaint())}alignCameraToMap(e){const i=this.map.getCenter().lat*Math.PI/180,t=W/Math.cos(i),a=new c;a.set(W,0,0,.5,0,-2.4953202340126887e-8,0,.5,0,0,t,0,0,0,0,1);const s=(new c).fromArray(e.defaultProjectionData.mainMatrix);this.camera.projectionMatrix.copy(s).multiply(a)}needsTranslucentPrepass(){return this.fillExtrusionMaterial.uniforms.layerOpacity.value<1}renderDepthPrepass(){this.tiles.traverse(e=>{O(e)&&(e.material=this.maskedFillExtrusionVariant(e)?.depth??this.depthPrepassMaterial)}),this.renderer.render(this.scene,this.camera)}setOpacity(e){this.forEachFillExtrusionMaterial(i=>i.setLayerOpacity(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 _(e),depth:new R(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?.properties;if(i)try{const t=.5;this.directionalLight.intensity=t*Math.PI,this.ambientLight.intensity=(1-t)*Math.PI;const a=i.get("position");this.directionalLight.position.set(a.x,a.y,a.z);const s="viewport"===i.get("anchor")?e.transform.bearingInRadians??0:0,r=i.get("color");this.forEachFillExtrusionMaterial(e=>e.setLight(a,s,t,r))}catch{}}applyMaterials(){this.forEachFillExtrusionMaterial(e=>e.setDiffuseColor(this.diffuseColor)),this.tiles.traverse(e=>{O(e)&&(e.material=this.getMaterial(e))})}},H=["!",["coalesce",["get","has_landmark"],!1]],N=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:[z()],minzoom:15,maxzoom:15,withCredentials:t.withCredentials??i()};this.modelsLayer=new V(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=t.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(){const e=this.map.mapLibreMap;if(!e.getLayer(this.basemapBuildingLayerID))return;const i=e.getFilter(this.basemapBuildingLayerID);if(i&&JSON.stringify(i).includes(JSON.stringify(H)))return;const t=i?["all",i,H]:H;e.setFilter(this.basemapBuildingLayerID,t)}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"));return i?{diffuse:i}:null}resolveCurrentMaterialState(){const e="inherited"===this.displayMode?this.readBasemapBuildingMaterial():null;return((e,i)=>{switch(e){case"dark":return{diffuseColor:D,opacity:S};case"light":return{diffuseColor:k.diffuse,opacity:S};default:return{diffuseColor:(i??k).diffuse,opacity:S}}})(this.displayMode,e)}applyDisplayMode(){const e=this.resolveCurrentMaterialState();this.modelsLayer.diffuseColor=e.diffuseColor,this.modelsLayer.setOpacity(e.opacity),this.map.mapLibreMap.triggerRepaint()}};export{N as Landmarks3D,V as ModelsLayer,j as ModelsSource,z as buildLandmarksTileURL};
2
2
  //# sourceMappingURL=index.es.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.es.js","names":[],"sources":["../src/displayMode.ts","../src/landmarksTileURL.ts","../src/FillExtrusionMaterial.ts","../src/utils.ts","../src/ModelsSource.ts","../src/ModelsLayer.ts","../src/Landmarks3D.ts"],"sourcesContent":["/**\n * How 3D landmarks are shaded relative to the base map. Every mode renders the\n * landmarks as maplibre-style fill-extrusion buildings:\n * - `inherited`: mirrors the colour and opacity of the basemap 3D building layer (default).\n * - `dark`: uses the 3D building look of the SDK's standard dark style.\n * - `light`: uses the 3D building look of the SDK's standard light style.\n *\n * @group Landmarks 3D\n */\nexport type Landmarks3DDisplayMode = 'inherited' | 'dark' | 'light';\n\n/**\n * The colour of the basemap 3D building layer, read from the map style.\n *\n * @group Landmarks 3D\n */\nexport type BasemapBuildingMaterial = {\n diffuse: string;\n};\n\n/**\n * Material state that a {@link Landmarks3DDisplayMode} resolves to on a {@link ModelsLayer}.\n *\n * @group Landmarks 3D\n */\nexport type LandmarksMaterialState = {\n diffuseColor: string;\n opacity: number;\n};\n\n// The `3D - Building` colour fallback of the SDK's standard light/dark styles.\nconst STANDARD_LIGHT_BUILDINGS: BasemapBuildingMaterial = { diffuse: 'hsl(38, 6%, 90%)' };\nconst STANDARD_DARK_BUILDINGS: BasemapBuildingMaterial = { diffuse: 'hsl(210, 9%, 17%)' };\n\n// Landmark layer opacity, tuned so the meshes blend with the basemap 3D buildings.\nconst LANDMARK_OPACITY = 0.82;\n\n/**\n * Resolves a display mode to the concrete material state to apply on the models layer.\n *\n * @param mode - The display mode to resolve.\n * @param basemapMaterial - The basemap building look used by the `inherited` mode.\n * When `null` (basemap building layer absent), `inherited` falls back to the\n * standard light style look.\n *\n * @group Landmarks 3D\n */\nexport const resolveDisplayMode = (\n mode: Landmarks3DDisplayMode,\n basemapMaterial: BasemapBuildingMaterial | null,\n): LandmarksMaterialState => {\n switch (mode) {\n case 'dark':\n return { diffuseColor: STANDARD_DARK_BUILDINGS.diffuse, opacity: LANDMARK_OPACITY };\n case 'light':\n return { diffuseColor: STANDARD_LIGHT_BUILDINGS.diffuse, opacity: LANDMARK_OPACITY };\n default: {\n // 'inherited' — mirror the basemap building colour, falling back to the standard light look.\n const material = basemapMaterial ?? STANDARD_LIGHT_BUILDINGS;\n return { diffuseColor: material.diffuse, opacity: LANDMARK_OPACITY };\n }\n }\n};\n","import { TomTomConfig } from '@tomtom-org/maps-sdk/core';\n\nconst LANDMARKS_API_VERSION = 2;\n\n/**\n * Builds the TomTom Orbis 3D Landmarks tile URL template.\n *\n * @remarks\n * The API serves meshopt-compressed GLB tiles with KTX2/Basis textures and requires\n * an API key with Orbis 3D Landmarks entitlements (still in Private Preview).\n *\n * @group Landmarks 3D\n */\nexport const buildLandmarksTileURL = (): string => {\n const { commonBaseURL, apiKey } = TomTomConfig.instance.get();\n // Proxy mode passes apiKey='' and lets the proxy inject the real key server-side —\n // skip the param so the proxy key never reaches the browser.\n const keyParam = apiKey ? `key=${apiKey}&` : '';\n return `${commonBaseURL}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${keyParam}apiVersion=${LANDMARKS_API_VERSION}`;\n};\n","import {\n Color,\n CustomBlending,\n DoubleSide,\n FrontSide,\n OneFactor,\n OneMinusSrcAlphaFactor,\n ShaderMaterial,\n SRGBColorSpace,\n type Texture,\n Vector3,\n} from 'three';\n\nconst VERTEX_SHADER = /* glsl */ `\n varying vec3 vWorldNormal;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n`;\n\n// Samples the as-authored texture alpha so untextured shading keeps the mesh's carved silhouette.\nconst ALPHA_MASK_FRAGMENT_CHUNK = /* glsl */ `\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n`;\n\nconst ALPHA_MASK_MAIN_CHUNK = /* glsl */ `\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n`;\n\n// Per-fragment replica of maplibre's fill_extrusion shading, in sRGB on the raw paint colour (no tone-mapping).\nconst FRAGMENT_SHADER = /* glsl */ `\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n varying vec3 vWorldNormal;\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n vec3 normal = normalize(vWorldNormal);\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n // Flat ambient term lifting the paint colour, tuned to match the basemap buildings.\n vec3 color = diffuseColor + vec3(0.145);\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 // Side faces (walls) darken to 0.9; roofs (normal +Z) stay full bright.\n float wallFactor = 1.0 - abs(normal.z);\n directional *= mix(1.0, 0.9, wallFactor);\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`;\n\n// Maplibre's default style light position [1.15, 210, 30] as a cartesian vector.\nconst DEFAULT_LIGHT_POSITION = new Vector3(0.2875, -0.498, 0.996);\n\nconst parsedColor = new Color();\nconst srgbColor = new Color();\n\n/**\n * Alpha mask carried over from a landmark mesh's as-authored texture.\n *\n * Landmark meshes are coarse hulls (e.g. a wall is one rectangle) whose realistic\n * silhouette is carved out by the texture's alpha channel. Untextured shading must keep\n * sampling that alpha and discard the carved-away fragments, otherwise the raw hull\n * shape gets coloured in.\n *\n * @group Landmarks 3D\n */\nexport type FillExtrusionAlphaMask = {\n /** Texture whose alpha channel carves the mesh silhouette (the mesh's base colour map). */\n map: Texture;\n /** Fragments with alpha below this are discarded. */\n cutoff: number;\n};\n\n/**\n * Material that shades untextured landmark meshes exactly like maplibre-gl shades\n * `fill-extrusion` buildings, so they blend in with the basemap building layer.\n *\n * Colour and opacity premultiply like maplibre's `v_color *= u_opacity`, paired with\n * (ONE, ONE_MINUS_SRC_ALPHA) blending. Backfaces are culled like maplibre's extrusion\n * rendering, except for alpha-masked meshes whose cutout surfaces must stay visible from\n * both sides; pair with {@link FillExtrusionDepthMaterial} as a depth prepass so that\n * translucent landmarks blend with the basemap exactly once per pixel.\n *\n * @group Landmarks 3D\n */\nexport class FillExtrusionMaterial extends ShaderMaterial {\n constructor(alphaMask?: FillExtrusionAlphaMask) {\n super({\n uniforms: {\n diffuseColor: { value: new Vector3(1, 1, 1) },\n lightPosition: { value: DEFAULT_LIGHT_POSITION.clone() },\n lightIntensity: { value: 0.5 },\n lightColor: { value: new Vector3(1, 1, 1) },\n layerOpacity: { value: 1 },\n ...(alphaMask && {\n alphaMask: { value: alphaMask.map },\n alphaMaskCutoff: { value: alphaMask.cutoff },\n }),\n },\n ...(alphaMask && { defines: { USE_ALPHA_MASK: '' } }),\n vertexShader: VERTEX_SHADER,\n fragmentShader: FRAGMENT_SHADER,\n side: alphaMask ? DoubleSide : FrontSide,\n transparent: true,\n blending: CustomBlending,\n blendSrc: OneFactor,\n blendDst: OneMinusSrcAlphaFactor,\n });\n }\n\n /**\n * Copies the shading uniforms (colour, light, layer opacity) from another instance,\n * bringing a freshly created alpha-masked variant up to date with the shared material.\n */\n copyShadingFrom(source: FillExtrusionMaterial): void {\n (this.uniforms.diffuseColor.value as Vector3).copy(source.uniforms.diffuseColor.value as Vector3);\n (this.uniforms.lightPosition.value as Vector3).copy(source.uniforms.lightPosition.value as Vector3);\n this.uniforms.lightIntensity.value = source.uniforms.lightIntensity.value;\n (this.uniforms.lightColor.value as Vector3).copy(source.uniforms.lightColor.value as Vector3);\n this.uniforms.layerOpacity.value = source.uniforms.layerOpacity.value;\n }\n\n /**\n * Sets the building colour from a CSS colour string, stored as raw sRGB\n * components — maplibre shades paint colours in sRGB space, not linear.\n */\n setDiffuseColor(cssColor: string): void {\n parsedColor.setStyle(cssColor);\n parsedColor.getRGB(srgbColor, SRGBColorSpace);\n (this.uniforms.diffuseColor.value as Vector3).set(srgbColor.r, srgbColor.g, srgbColor.b);\n }\n\n setLayerOpacity(opacity: number): void {\n this.uniforms.layerOpacity.value = opacity;\n }\n\n /**\n * Mirrors maplibre's fill-extrusion light uniforms: the style light position is\n * rotated by the map bearing for viewport-anchored lights and the light vector is\n * deliberately left unnormalised, exactly like maplibre's `u_lightpos`.\n *\n * @param position - The style light position, in maplibre's y-south axes.\n * @param bearingInRadians - Map bearing to rotate by; pass `0` for map-anchored lights.\n * @param intensity - The style light intensity.\n * @param color - The style light colour.\n */\n setLight(\n position: { x: number; y: number; z: number },\n bearingInRadians: number,\n intensity: number,\n color: { r: number; g: number; b: number },\n ): void {\n const cosine = Math.cos(bearingInRadians);\n const sine = Math.sin(bearingInRadians);\n (this.uniforms.lightPosition.value as Vector3).set(\n position.x * cosine - position.y * sine,\n position.x * sine + position.y * cosine,\n position.z,\n );\n this.uniforms.lightIntensity.value = intensity;\n (this.uniforms.lightColor.value as Vector3).set(color.r, color.g, color.b);\n }\n}\n\n/**\n * Depth-only prepass companion to {@link FillExtrusionMaterial}. Rendering the scene\n * with this material first fills the depth buffer with the nearest landmark surfaces,\n * so the colour pass shades each pixel exactly once — overlapping translucent walls\n * don't blend on top of each other, mirroring how maplibre's depth-writing\n * fill-extrusion pass keeps buildings from showing through one another.\n *\n * Shares {@link FillExtrusionMaterial}'s vertex shader and alpha mask so both passes\n * produce identical depth values and cut out the same fragments.\n *\n * @group Landmarks 3D\n */\nexport class FillExtrusionDepthMaterial extends ShaderMaterial {\n constructor(alphaMask?: FillExtrusionAlphaMask) {\n super({\n ...(alphaMask && {\n uniforms: {\n alphaMask: { value: alphaMask.map },\n alphaMaskCutoff: { value: alphaMask.cutoff },\n },\n defines: { USE_ALPHA_MASK: '' },\n }),\n vertexShader: VERTEX_SHADER,\n fragmentShader: /* glsl */ `\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n gl_FragColor = vec4(0.0);\n }\n `,\n side: alphaMask ? DoubleSide : FrontSide,\n colorWrite: false,\n });\n }\n}\n","import type { Mesh, Object3D } from 'three';\n\n/**\n * @ignore\n */\nexport const isMesh = (object: Object3D): object is Mesh => (object as Mesh).isMesh === true;\n","import type { Map as MapLibreMap } from 'maplibre-gl';\nimport { DoubleSide, Group, type MeshStandardMaterial, REVISION, type WebGLRenderer } from 'three';\nimport { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';\nimport { toCreasedNormals } from 'three/examples/jsm/utils/BufferGeometryUtils.js';\nimport type { ModelsSourceSpecification } from './types/modelsSpecifications';\nimport { isMesh } from './utils';\n\n// Edges sharper than 30° keep their own normals (crisp corners); smoother ones share one (no grain).\nconst CREASE_ANGLE_RADIANS = Math.PI / 6;\n\nconst DEFAULT_TRANSCODER_PATH = `https://unpkg.com/three@0.${REVISION}.x/examples/jsm/libs/basis/`;\n\n// Private maplibre-gl internals this source needs, reached via an `unknown` cast.\ninterface MapLibreTransformInternals {\n transform: { tileSize: number };\n}\n\n// Element of maplibre's coveringTiles() result; exposes `.key`, `.canonical.{z,url}` and `.toString()`.\ntype CoveringTile = ReturnType<MapLibreMap['coveringTiles']>[number];\n\n/**\n * Source of tiled glTF/GLB models for a {@link ModelsLayer}.\n *\n * Tracks the tiles covering the current viewport, fetches each tile's models once\n * and keeps them in a Three.js {@link Group} that the owning layer renders.\n *\n * @group Landmarks 3D\n */\nexport class ModelsSource {\n type = 'models' as const;\n id: string;\n minzoom: number;\n maxzoom: number;\n map!: MapLibreMap;\n tiles: Array<string>;\n scene: Group;\n loadedTiles = new Set<string>();\n /** Optional consumer callback fired once per successful tile load. */\n onTileLoaded?: (scene: Group, key: string) => void;\n\n private readonly ktx2Loader: KTX2Loader;\n private readonly gltfLoader: GLTFLoader;\n private readonly specification: ModelsSourceSpecification;\n\n constructor(id: string, options: ModelsSourceSpecification) {\n this.id = id;\n this.minzoom = options.minzoom ?? 0;\n this.maxzoom = options.maxzoom ?? 22;\n this.tiles = options.tiles;\n this.specification = options;\n this.scene = new Group();\n this.ktx2Loader = new KTX2Loader()\n .setCrossOrigin('anonymous')\n .setTranscoderPath(options.transcoderPath ?? DEFAULT_TRANSCODER_PATH);\n this.gltfLoader = new GLTFLoader().setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(MeshoptDecoder);\n }\n\n onAdd(map: MapLibreMap, renderer: WebGLRenderer) {\n this.map = map;\n this.ktx2Loader.detectSupport(renderer);\n }\n\n updateTiles() {\n if (this.tiles.length === 0) {\n return;\n }\n\n for (const tile of this.coveringTiles()) {\n this.loadTile(tile);\n }\n }\n\n private coveringTiles(): CoveringTile[] {\n const transform = (this.map as unknown as MapLibreTransformInternals).transform;\n return this.map.coveringTiles({\n tileSize: transform.tileSize,\n minzoom: this.minzoom,\n maxzoom: this.maxzoom,\n });\n }\n\n // The source loads a single zoom level, so tiles never overlap; each is kept on screen once loaded.\n private loadTile(tile: CoveringTile): void {\n if (this.loadedTiles.has(tile.key)) {\n return;\n }\n this.loadedTiles.add(tile.key);\n\n const url = tile.canonical.url(this.tiles, this.map.getPixelRatio(), this.specification.scheme);\n this.gltfLoader\n .loadAsync(url)\n .then((data) => {\n prepareTileMeshes(data.scene);\n data.scene.userData = { isTile: true, key: tile.key };\n this.scene.add(data.scene);\n this.onTileLoaded?.(data.scene, tile.key);\n })\n .catch((error: { response?: { status?: number }; message?: string }) => {\n const status = error?.response?.status;\n // 204 = no landmark in this tile, 404 = tile not produced — both expected.\n if (status !== 204 && status !== 404) {\n console.warn(`Problem with loading ${tile.toString()}: ${error?.message ?? 'unknown'}`);\n }\n });\n }\n}\n\n// Stores each mesh's authored material and re-creases its normals so untextured shading keeps crisp edges.\nconst prepareTileMeshes = (scene: Group): void => {\n scene.traverse((child) => {\n if (!isMesh(child)) {\n return;\n }\n\n const material = child.material as MeshStandardMaterial;\n material.alphaTest = 0.1;\n material.side = DoubleSide;\n child.userData.originalMaterial = material;\n child.geometry = toCreasedNormals(child.geometry, CREASE_ANGLE_RADIANS);\n });\n};\n","import type { CustomLayerInterface, CustomRenderMethodInput, Map as MapLibreMap } from 'maplibre-gl';\nimport {\n AmbientLight,\n Camera,\n DirectionalLight,\n Group,\n Matrix4,\n type Mesh,\n type MeshStandardMaterial,\n Scene,\n type Texture,\n WebGLRenderer,\n} from 'three';\nimport { FillExtrusionDepthMaterial, FillExtrusionMaterial } from './FillExtrusionMaterial';\nimport { ModelsSource } from './ModelsSource';\nimport type { ModelsLayerSpecification, ModelsSourceSpecification } from './types/modelsSpecifications';\nimport { isMesh } from './utils';\n\n// EPSG:3857 metres → mercator [0..1]; Z additionally scales by 1/cos(lat) per frame in render().\nconst TILE_SCALE = 1 / (2 * 20037508.34);\nconst TRANSLATE_X = 0.5;\nconst TRANSLATE_Y = 0.5;\n\nconst toRadians = (degrees: number): number => (degrees * Math.PI) / 180;\n\n// Directional light strength for the landmark shading, tuned to blend with the basemap buildings.\nconst LANDMARK_LIGHT_INTENSITY = 0.5;\n\n// Subset of the private maplibre-gl internals this layer needs, reached via an `unknown` cast.\ninterface MapLibreRenderInternals {\n transform: { tileZoom: number; bearingInRadians?: number };\n style: {\n light?: {\n properties?: {\n get(key: 'intensity'): number;\n get(key: 'position'): { x: number; y: number; z: number };\n get(key: 'anchor'): 'map' | 'viewport';\n get(key: 'color'): { r: number; g: number; b: number };\n };\n };\n };\n}\n\n/**\n * MapLibre custom layer that renders tiled glTF/GLB models with Three.js.\n *\n * The layer owns a {@link ModelsSource} that streams model tiles for the current\n * viewport, renders them as maplibre-style fill-extrusion buildings, and keeps its\n * lighting in sync with the map style light.\n *\n * @group Landmarks 3D\n */\nexport class ModelsLayer implements CustomLayerInterface {\n id: string;\n type = 'custom' as const;\n renderingMode = '3d' as const;\n\n renderer!: WebGLRenderer;\n scene!: Scene;\n camera!: Camera;\n tiles: Group;\n directionalLight!: DirectionalLight;\n ambientLight!: AmbientLight;\n map!: MapLibreMap;\n source: ModelsSource;\n minzoom = 0;\n maxzoom = 22;\n visible = true;\n\n diffuseColor = '#ffffff';\n\n private readonly fillExtrusionMaterial = new FillExtrusionMaterial();\n private readonly depthPrepassMaterial = new FillExtrusionDepthMaterial();\n // Per-texture alpha-masked variants so untextured shading keeps each mesh's carved silhouette.\n private readonly maskedFillExtrusionMaterials = new Map<\n Texture,\n { color: FillExtrusionMaterial; depth: FillExtrusionDepthMaterial }\n >();\n\n constructor(options: ModelsLayerSpecification, source: ModelsSourceSpecification) {\n this.id = options.id;\n this.source = new ModelsSource(this.id, source);\n this.tiles = this.source.scene;\n this.updateProperties(options);\n }\n\n updateProperties(options: ModelsLayerSpecification) {\n this.visible = options.layout?.visibility !== 'none';\n this.diffuseColor = options.paint?.['models-base-color'] ?? '#ffffff';\n this.minzoom = options.minzoom ?? 0;\n this.maxzoom = options.maxzoom ?? 22;\n }\n\n setDirectionalLight(radius: number, phi: number, theta: number) {\n this.directionalLight.position.setFromSphericalCoords(radius, toRadians(phi), toRadians(theta));\n }\n\n onAdd(map: MapLibreMap, gl: WebGLRenderingContext | WebGL2RenderingContext) {\n this.map = map;\n\n this.scene = new Scene();\n this.tiles = this.source.scene;\n this.camera = new Camera();\n\n this.directionalLight = new DirectionalLight(0xffffff, 0.5);\n this.setDirectionalLight(1.15, 90, 0);\n this.scene.add(this.directionalLight);\n\n this.ambientLight = new AmbientLight(0xffffff, 0.1);\n this.scene.add(this.ambientLight);\n\n this.scene.add(this.tiles);\n\n this.renderer = new WebGLRenderer({\n canvas: map.getCanvas(),\n context: gl,\n antialias: true,\n });\n this.renderer.autoClear = false;\n\n this.source.onAdd(map, this.renderer);\n }\n\n render(_gl: WebGLRenderingContext | WebGL2RenderingContext, options: CustomRenderMethodInput) {\n const internals = this.map as unknown as MapLibreRenderInternals;\n const tileZoom = internals.transform.tileZoom;\n const inZoomRange = tileZoom >= this.minzoom && tileZoom <= this.maxzoom;\n this.tiles.visible = this.visible && inZoomRange;\n\n if (!(this.visible && inZoomRange)) {\n return;\n }\n\n this.syncLighting();\n this.source.updateTiles();\n this.alignCameraToMap(options);\n\n this.renderer.resetState();\n if (this.needsTranslucentPrepass()) {\n this.renderDepthPrepass();\n }\n\n this.applyMaterials();\n this.renderer.render(this.scene, this.camera);\n this.map.triggerRepaint();\n }\n\n // Canonical maplibre v5 pattern: MVP*scale on camera.projectionMatrix, via mainMatrix because v5 flattens Z.\n private alignCameraToMap(options: CustomRenderMethodInput) {\n const latitudeInRadians = (this.map.getCenter().lat * Math.PI) / 180;\n const zScale = TILE_SCALE / Math.cos(latitudeInRadians);\n const mapLibreScale = new Matrix4();\n mapLibreScale.set(TILE_SCALE, 0, 0, TRANSLATE_X, 0, -TILE_SCALE, 0, TRANSLATE_Y, 0, 0, zScale, 0, 0, 0, 0, 1);\n const projection = new Matrix4().fromArray(options.defaultProjectionData.mainMatrix as ArrayLike<number>);\n this.camera.projectionMatrix.copy(projection).multiply(mapLibreScale);\n }\n\n // Translucent shading needs a depth pass so overlapping walls blend with the basemap once per pixel.\n private needsTranslucentPrepass(): boolean {\n const layerOpacity = this.fillExtrusionMaterial.uniforms.layerOpacity.value as number;\n return layerOpacity < 1;\n }\n\n // Per-mesh (not overrideMaterial) so each mesh's alpha mask carves the prepass depth too.\n private renderDepthPrepass() {\n this.tiles.traverse((child) => {\n if (isMesh(child)) {\n child.material = this.maskedFillExtrusionVariant(child)?.depth ?? this.depthPrepassMaterial;\n }\n });\n this.renderer.render(this.scene, this.camera);\n }\n\n setOpacity(value: number) {\n this.forEachFillExtrusionMaterial((material) => material.setLayerOpacity(value));\n }\n\n getMaterial(mesh: Mesh): FillExtrusionMaterial {\n return this.maskedFillExtrusionVariant(mesh)?.color ?? this.fillExtrusionMaterial;\n }\n\n // Fill-extrusion variant masked by the mesh's own texture alpha; null for untextured meshes.\n private maskedFillExtrusionVariant(mesh: Mesh) {\n const original = mesh.userData.originalMaterial as MeshStandardMaterial | undefined;\n const map = original?.map;\n if (!map) {\n return null;\n }\n\n let variant = this.maskedFillExtrusionMaterials.get(map);\n if (!variant) {\n const alphaMask = { map, cutoff: original.alphaTest || 0.1 };\n variant = {\n color: new FillExtrusionMaterial(alphaMask),\n depth: new FillExtrusionDepthMaterial(alphaMask),\n };\n variant.color.copyShadingFrom(this.fillExtrusionMaterial);\n this.maskedFillExtrusionMaterials.set(map, variant);\n }\n return variant;\n }\n\n private forEachFillExtrusionMaterial(apply: (material: FillExtrusionMaterial) => void) {\n apply(this.fillExtrusionMaterial);\n this.maskedFillExtrusionMaterials.forEach((variant) => apply(variant.color));\n }\n\n private syncLighting() {\n // The maplibre style light is private API; fall back to the onAdd defaults when absent.\n const internals = this.map as unknown as MapLibreRenderInternals;\n const properties = internals.style?.light?.properties;\n if (!properties) {\n return;\n }\n\n try {\n // Fixed strength tuned to match the basemap buildings (the standard styles light at ~0.5).\n const intensity = LANDMARK_LIGHT_INTENSITY;\n // `* π` cancels Three.js's internal 1/π Lambert factor, matching maplibre's shading range.\n this.directionalLight.intensity = intensity * Math.PI;\n this.ambientLight.intensity = (1 - intensity) * Math.PI;\n\n const position = properties.get('position');\n this.directionalLight.position.set(position.x, position.y, position.z);\n\n // Viewport-anchored lights rotate with the map bearing, like maplibre's painter.\n const anchor = properties.get('anchor');\n const bearingInRadians = anchor === 'viewport' ? (internals.transform.bearingInRadians ?? 0) : 0;\n const color = properties.get('color');\n this.forEachFillExtrusionMaterial((material) =>\n material.setLight(position, bearingInRadians, intensity, color),\n );\n } catch {\n // Internal API surface changed — keep the configured defaults.\n }\n }\n\n private applyMaterials() {\n this.forEachFillExtrusionMaterial((material) => material.setDiffuseColor(this.diffuseColor));\n\n this.tiles.traverse((child) => {\n if (isMesh(child)) {\n child.material = this.getMaterial(child);\n }\n });\n }\n}\n","import type { TomTomMap } from '@tomtom-org/maps-sdk/map';\nimport type { FilterSpecification } from 'maplibre-gl';\nimport {\n type BasemapBuildingMaterial,\n type Landmarks3DDisplayMode,\n type LandmarksMaterialState,\n resolveDisplayMode,\n} from './displayMode';\nimport { buildLandmarksTileURL } from './landmarksTileURL';\nimport { ModelsLayer } from './ModelsLayer';\nimport type { ModelsLayerSpecification, ModelsSourceSpecification } from './types/modelsSpecifications';\n\n// Load landmark tiles at a single zoom level: the meshes are full-detail at this zoom and upscale\nconst SOURCE_ZOOM = 15;\n\nconst DEFAULT_LAYER_ID = 'orbis-3d-landmarks';\nconst DEFAULT_LAYER_MIN_ZOOM = 10;\nconst DEFAULT_LAYER_MAX_ZOOM = 22;\n\n// Filters out basemap extruded buildings that overlap a landmark, so they don't clip through its mesh.\nconst DEFAULT_BASEMAP_BUILDING_LAYER_ID = '3D - Building';\nconst NO_LANDMARK_FILTER_CLAUSE: FilterSpecification = ['!', ['coalesce', ['get', 'has_landmark'], false]];\n\n// Resolves `fill-extrusion-color` to one CSS colour, for `match` expressions, the last operand is the default.\nconst extractColorFromPaint = (paintColor: unknown): string | null => {\n if (typeof paintColor === 'string') {\n return paintColor;\n }\n\n if (Array.isArray(paintColor) && paintColor[0] === 'match') {\n const fallback = paintColor.at(-1);\n if (typeof fallback === 'string') {\n return fallback;\n }\n }\n\n return null;\n};\n\n/**\n * Configuration for the {@link Landmarks3D} plugin.\n *\n * @group Landmarks 3D\n */\nexport type Landmarks3DOptions = {\n /**\n * Initial display mode for the landmarks. Can also be changed at runtime with\n * {@link Landmarks3D.setDisplayMode}.\n * @defaultValue `'inherited'`\n */\n displayMode?: Landmarks3DDisplayMode;\n /**\n * Whether the landmarks are initially visible. Can also be changed at runtime with\n * {@link Landmarks3D.setVisible}.\n * @defaultValue `true`\n */\n visible?: boolean;\n /**\n * Minimum zoom at which landmarks are rendered.\n * @defaultValue The basemap 3D building layer's minimum zoom, so landmarks appear together\n * with it (falls back to `10` when that layer is absent).\n */\n minZoom?: number;\n /**\n * Maximum zoom at which landmarks are rendered.\n * @defaultValue The basemap 3D building layer's maximum zoom, so landmarks disappear together\n * with it (falls back to `22` when that layer is absent).\n */\n maxZoom?: number;\n};\n\n/**\n * Renders TomTom Orbis 3D Landmarks (high-detail building meshes) on a {@link TomTomMap}.\n *\n * The plugin streams GLB tiles from the Orbis 3D Landmarks API (Private Preview),\n * renders them with Three.js via a MapLibre custom layer, filters overlapping\n * extruded basemap buildings out of the style, and keeps everything restored\n * across map style changes.\n *\n * @remarks\n * Requires an API key with Orbis 3D Landmarks entitlements and a map style that\n * shows extruded 3D buildings for the exclusion filter and `inherited` mode to\n * have an effect.\n *\n * @example\n * ```typescript\n * import { Landmarks3D } from '@tomtom-org/maps-sdk-plugin-landmarks-3d';\n *\n * // assume `map` is your initialized TomTomMap instance\n * const landmarks = new Landmarks3D(map);\n *\n * await landmarks.setDisplayMode('inherited');\n * ```\n *\n * @group Landmarks 3D\n */\nexport class Landmarks3D {\n private readonly map: TomTomMap;\n private readonly layerID = DEFAULT_LAYER_ID;\n private readonly basemapBuildingLayerID = DEFAULT_BASEMAP_BUILDING_LAYER_ID;\n private readonly modelsLayer: ModelsLayer;\n // Explicit zoom-range overrides; when unset, the range tracks the basemap 3D building layer.\n private readonly layerMinZoom?: number;\n private readonly layerMaxZoom?: number;\n private displayMode: Landmarks3DDisplayMode;\n\n /**\n * Creates the plugin bound to a TomTom map and starts rendering landmarks\n * as soon as the map is ready.\n *\n * @param map - The TomTom map instance to render landmarks on.\n * @param options - Optional configuration.\n */\n constructor(map: TomTomMap, options: Landmarks3DOptions = {}) {\n this.map = map;\n this.layerMinZoom = options.minZoom;\n this.layerMaxZoom = options.maxZoom;\n this.displayMode = options.displayMode ?? 'inherited';\n\n const layerSpecification: ModelsLayerSpecification = {\n id: this.layerID,\n type: 'models',\n source: this.layerID,\n minzoom: options.minZoom ?? DEFAULT_LAYER_MIN_ZOOM,\n maxzoom: options.maxZoom ?? DEFAULT_LAYER_MAX_ZOOM,\n };\n const sourceSpecification: ModelsSourceSpecification = {\n type: 'models',\n tiles: [buildLandmarksTileURL()],\n minzoom: SOURCE_ZOOM,\n maxzoom: SOURCE_ZOOM,\n };\n this.modelsLayer = new ModelsLayer(layerSpecification, sourceSpecification);\n this.modelsLayer.visible = options.visible ?? true;\n\n this.map.addStyleChangeHandler({\n onStyleChanged: () => this.install(),\n });\n this.installWhenReady();\n }\n\n // Installs the layer as soon as the map is ready\n private installWhenReady(): void {\n if (this.map.mapReady) {\n this.install();\n } else {\n this.map.mapLibreMap.once('styledata', () => this.installWhenReady());\n }\n }\n\n /**\n * The underlying MapLibre custom layer, for advanced use beyond this facade.\n */\n get layer(): ModelsLayer {\n return this.modelsLayer;\n }\n\n /**\n * The currently applied display mode.\n */\n getDisplayMode(): Landmarks3DDisplayMode {\n return this.displayMode;\n }\n\n /**\n * Changes how landmarks are rendered relative to the base map.\n *\n * @param mode - The display mode to apply.\n * @returns A promise that resolves once the mode has been applied.\n */\n async setDisplayMode(mode: Landmarks3DDisplayMode): Promise<void> {\n this.displayMode = mode;\n // Before the map is ready the layer isn't installed; install() applies the stored\n // mode once it runs, so simply recording it above is enough in that case.\n if (this.map.mapReady) {\n this.applyDisplayMode();\n }\n }\n\n /**\n * Whether the landmarks are currently visible.\n */\n isVisible(): boolean {\n return this.modelsLayer.visible;\n }\n\n /**\n * Shows or hides the landmarks layer.\n *\n * @param visible - `true` to show landmarks, `false` to hide them.\n * @returns A promise that resolves once the visibility has been applied.\n */\n async setVisible(visible: boolean): Promise<void> {\n this.modelsLayer.visible = visible;\n // When the map isn't ready yet the layer is added (with this visibility) by\n // install(); only an already-installed layer needs an explicit repaint here.\n if (this.map.mapReady) {\n this.map.mapLibreMap.triggerRepaint();\n }\n }\n\n // (Re-)adds the layer and re-applies filter + display mode on first readiness and after style changes.\n private install(): void {\n const mapLibreMap = this.map.mapLibreMap;\n if (!mapLibreMap.getLayer(this.layerID)) {\n mapLibreMap.addLayer(this.modelsLayer);\n }\n\n // The standard styles ship the 3D building layer hidden; show it so landmarks get a 3D city context.\n if (mapLibreMap.getLayer(this.basemapBuildingLayerID)) {\n mapLibreMap.setLayoutProperty(this.basemapBuildingLayerID, 'visibility', 'visible');\n }\n\n this.alignZoomRangeWithBasemapBuildings();\n this.excludeLandmarkBuildingsFromBasemap();\n\n // Late-arriving tiles load with opaque materials; re-apply non-opaque looks to them.\n this.modelsLayer.source.onTileLoaded = () => {\n if (this.resolveCurrentMaterialState().opacity < 1) {\n this.applyDisplayMode();\n }\n };\n\n this.applyDisplayMode();\n }\n\n // Tracks the basemap 3D building layer's zoom range so landmarks appear and disappear together with\n // it, unless the caller pinned an explicit min/max zoom. No-op when the building layer is absent.\n private alignZoomRangeWithBasemapBuildings(): void {\n const buildingLayer = this.map.mapLibreMap.getLayer(this.basemapBuildingLayerID);\n if (!buildingLayer) {\n return;\n }\n\n this.modelsLayer.minzoom = this.layerMinZoom ?? buildingLayer.minzoom ?? DEFAULT_LAYER_MIN_ZOOM;\n this.modelsLayer.maxzoom = this.layerMaxZoom ?? buildingLayer.maxzoom ?? DEFAULT_LAYER_MAX_ZOOM;\n }\n\n private excludeLandmarkBuildingsFromBasemap(): void {\n const mapLibreMap = this.map.mapLibreMap;\n if (!mapLibreMap.getLayer(this.basemapBuildingLayerID)) {\n return;\n }\n\n const existing = mapLibreMap.getFilter(this.basemapBuildingLayerID);\n if (existing && JSON.stringify(existing).includes(JSON.stringify(NO_LANDMARK_FILTER_CLAUSE))) {\n return;\n }\n\n // Asserted because `['all', ...]` arms span FilterSpecification's expression and legacy variants.\n const combined = (\n existing ? ['all', existing, NO_LANDMARK_FILTER_CLAUSE] : NO_LANDMARK_FILTER_CLAUSE\n ) as FilterSpecification;\n mapLibreMap.setFilter(this.basemapBuildingLayerID, combined);\n }\n\n // Reads the basemap building colour for `inherited` mode; null when absent or not resolvable to one colour.\n private readBasemapBuildingMaterial(): BasemapBuildingMaterial | null {\n const mapLibreMap = this.map.mapLibreMap;\n if (!mapLibreMap.getLayer(this.basemapBuildingLayerID)) {\n return null;\n }\n\n const paintColor = mapLibreMap.getPaintProperty(this.basemapBuildingLayerID, 'fill-extrusion-color');\n const diffuse = extractColorFromPaint(paintColor);\n if (!diffuse) {\n return null;\n }\n\n return { diffuse };\n }\n\n private resolveCurrentMaterialState(): LandmarksMaterialState {\n const basemapMaterial = this.displayMode === 'inherited' ? this.readBasemapBuildingMaterial() : null;\n return resolveDisplayMode(this.displayMode, basemapMaterial);\n }\n\n private applyDisplayMode(): void {\n const state = this.resolveCurrentMaterialState();\n this.modelsLayer.diffuseColor = state.diffuseColor;\n this.modelsLayer.setOpacity(state.opacity);\n this.map.mapLibreMap.triggerRepaint();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.es.js","names":[],"sources":["../src/displayMode.ts","../src/landmarksTileURL.ts","../src/FillExtrusionMaterial.ts","../src/utils.ts","../src/ModelsSource.ts","../src/ModelsLayer.ts","../src/Landmarks3D.ts"],"sourcesContent":["/**\n * How 3D landmarks are shaded relative to the base map. Every mode renders the\n * landmarks as maplibre-style fill-extrusion buildings:\n * - `inherited`: mirrors the colour and opacity of the basemap 3D building layer (default).\n * - `dark`: uses the 3D building look of the SDK's standard dark style.\n * - `light`: uses the 3D building look of the SDK's standard light style.\n *\n * @group Landmarks 3D\n */\nexport type Landmarks3DDisplayMode = 'inherited' | 'dark' | 'light';\n\n/**\n * The colour of the basemap 3D building layer, read from the map style.\n *\n * @group Landmarks 3D\n */\nexport type BasemapBuildingMaterial = {\n diffuse: string;\n};\n\n/**\n * Material state that a {@link Landmarks3DDisplayMode} resolves to on a {@link ModelsLayer}.\n *\n * @group Landmarks 3D\n */\nexport type LandmarksMaterialState = {\n diffuseColor: string;\n opacity: number;\n};\n\n// The `3D - Building` colour fallback of the SDK's standard light/dark styles.\nconst STANDARD_LIGHT_BUILDINGS: BasemapBuildingMaterial = { diffuse: 'hsl(38, 6%, 90%)' };\nconst STANDARD_DARK_BUILDINGS: BasemapBuildingMaterial = { diffuse: 'hsl(210, 9%, 17%)' };\n\n// Landmark layer opacity, tuned so the meshes blend with the basemap 3D buildings.\nconst LANDMARK_OPACITY = 0.82;\n\n/**\n * Resolves a display mode to the concrete material state to apply on the models layer.\n *\n * @param mode - The display mode to resolve.\n * @param basemapMaterial - The basemap building look used by the `inherited` mode.\n * When `null` (basemap building layer absent), `inherited` falls back to the\n * standard light style look.\n *\n * @group Landmarks 3D\n */\nexport const resolveDisplayMode = (\n mode: Landmarks3DDisplayMode,\n basemapMaterial: BasemapBuildingMaterial | null,\n): LandmarksMaterialState => {\n switch (mode) {\n case 'dark':\n return { diffuseColor: STANDARD_DARK_BUILDINGS.diffuse, opacity: LANDMARK_OPACITY };\n case 'light':\n return { diffuseColor: STANDARD_LIGHT_BUILDINGS.diffuse, opacity: LANDMARK_OPACITY };\n default: {\n // 'inherited' — mirror the basemap building colour, falling back to the standard light look.\n const material = basemapMaterial ?? STANDARD_LIGHT_BUILDINGS;\n return { diffuseColor: material.diffuse, opacity: LANDMARK_OPACITY };\n }\n }\n};\n","import { TomTomConfig } from '@tomtom-org/maps-sdk/core';\n\nconst LANDMARKS_API_VERSION = 2;\n\n/**\n * Builds the TomTom Orbis 3D Landmarks tile URL template.\n *\n * @remarks\n * The API serves meshopt-compressed GLB tiles with KTX2/Basis textures and requires\n * an API key with Orbis 3D Landmarks entitlements (still in Private Preview).\n *\n * @group Landmarks 3D\n */\nexport const buildLandmarksTileURL = (): string => {\n const { commonBaseURL, apiKey } = TomTomConfig.instance.get();\n // Proxy mode passes apiKey='' and lets the proxy inject the real key server-side —\n // skip the param so the proxy key never reaches the browser.\n const keyParam = apiKey ? `key=${apiKey}&` : '';\n return `${commonBaseURL}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${keyParam}apiVersion=${LANDMARKS_API_VERSION}`;\n};\n","import {\n Color,\n CustomBlending,\n DoubleSide,\n FrontSide,\n OneFactor,\n OneMinusSrcAlphaFactor,\n ShaderMaterial,\n SRGBColorSpace,\n type Texture,\n Vector3,\n} from 'three';\n\nconst VERTEX_SHADER = /* glsl */ `\n varying vec3 vWorldNormal;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n`;\n\n// Samples the as-authored texture alpha so untextured shading keeps the mesh's carved silhouette.\nconst ALPHA_MASK_FRAGMENT_CHUNK = /* glsl */ `\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n`;\n\nconst ALPHA_MASK_MAIN_CHUNK = /* glsl */ `\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n`;\n\n// Per-fragment replica of maplibre's fill_extrusion shading, in sRGB on the raw paint colour (no tone-mapping).\nconst FRAGMENT_SHADER = /* glsl */ `\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n varying vec3 vWorldNormal;\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n vec3 normal = normalize(vWorldNormal);\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n // Flat ambient term lifting the paint colour, tuned to match the basemap buildings.\n vec3 color = diffuseColor + vec3(0.145);\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 // Side faces (walls) darken to 0.9; roofs (normal +Z) stay full bright.\n float wallFactor = 1.0 - abs(normal.z);\n directional *= mix(1.0, 0.9, wallFactor);\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`;\n\n// Maplibre's default style light position [1.15, 210, 30] as a cartesian vector.\nconst DEFAULT_LIGHT_POSITION = new Vector3(0.2875, -0.498, 0.996);\n\nconst parsedColor = new Color();\nconst srgbColor = new Color();\n\n/**\n * Alpha mask carried over from a landmark mesh's as-authored texture.\n *\n * Landmark meshes are coarse hulls (e.g. a wall is one rectangle) whose realistic\n * silhouette is carved out by the texture's alpha channel. Untextured shading must keep\n * sampling that alpha and discard the carved-away fragments, otherwise the raw hull\n * shape gets coloured in.\n *\n * @group Landmarks 3D\n */\nexport type FillExtrusionAlphaMask = {\n /** Texture whose alpha channel carves the mesh silhouette (the mesh's base colour map). */\n map: Texture;\n /** Fragments with alpha below this are discarded. */\n cutoff: number;\n};\n\n/**\n * Material that shades untextured landmark meshes exactly like maplibre-gl shades\n * `fill-extrusion` buildings, so they blend in with the basemap building layer.\n *\n * Colour and opacity premultiply like maplibre's `v_color *= u_opacity`, paired with\n * (ONE, ONE_MINUS_SRC_ALPHA) blending. Backfaces are culled like maplibre's extrusion\n * rendering, except for alpha-masked meshes whose cutout surfaces must stay visible from\n * both sides; pair with {@link FillExtrusionDepthMaterial} as a depth prepass so that\n * translucent landmarks blend with the basemap exactly once per pixel.\n *\n * @group Landmarks 3D\n */\nexport class FillExtrusionMaterial extends ShaderMaterial {\n constructor(alphaMask?: FillExtrusionAlphaMask) {\n super({\n uniforms: {\n diffuseColor: { value: new Vector3(1, 1, 1) },\n lightPosition: { value: DEFAULT_LIGHT_POSITION.clone() },\n lightIntensity: { value: 0.5 },\n lightColor: { value: new Vector3(1, 1, 1) },\n layerOpacity: { value: 1 },\n ...(alphaMask && {\n alphaMask: { value: alphaMask.map },\n alphaMaskCutoff: { value: alphaMask.cutoff },\n }),\n },\n ...(alphaMask && { defines: { USE_ALPHA_MASK: '' } }),\n vertexShader: VERTEX_SHADER,\n fragmentShader: FRAGMENT_SHADER,\n side: alphaMask ? DoubleSide : FrontSide,\n transparent: true,\n blending: CustomBlending,\n blendSrc: OneFactor,\n blendDst: OneMinusSrcAlphaFactor,\n });\n }\n\n /**\n * Copies the shading uniforms (colour, light, layer opacity) from another instance,\n * bringing a freshly created alpha-masked variant up to date with the shared material.\n */\n copyShadingFrom(source: FillExtrusionMaterial): void {\n (this.uniforms.diffuseColor.value as Vector3).copy(source.uniforms.diffuseColor.value as Vector3);\n (this.uniforms.lightPosition.value as Vector3).copy(source.uniforms.lightPosition.value as Vector3);\n this.uniforms.lightIntensity.value = source.uniforms.lightIntensity.value;\n (this.uniforms.lightColor.value as Vector3).copy(source.uniforms.lightColor.value as Vector3);\n this.uniforms.layerOpacity.value = source.uniforms.layerOpacity.value;\n }\n\n /**\n * Sets the building colour from a CSS colour string, stored as raw sRGB\n * components — maplibre shades paint colours in sRGB space, not linear.\n */\n setDiffuseColor(cssColor: string): void {\n parsedColor.setStyle(cssColor);\n parsedColor.getRGB(srgbColor, SRGBColorSpace);\n (this.uniforms.diffuseColor.value as Vector3).set(srgbColor.r, srgbColor.g, srgbColor.b);\n }\n\n setLayerOpacity(opacity: number): void {\n this.uniforms.layerOpacity.value = opacity;\n }\n\n /**\n * Mirrors maplibre's fill-extrusion light uniforms: the style light position is\n * rotated by the map bearing for viewport-anchored lights and the light vector is\n * deliberately left unnormalised, exactly like maplibre's `u_lightpos`.\n *\n * @param position - The style light position, in maplibre's y-south axes.\n * @param bearingInRadians - Map bearing to rotate by; pass `0` for map-anchored lights.\n * @param intensity - The style light intensity.\n * @param color - The style light colour.\n */\n setLight(\n position: { x: number; y: number; z: number },\n bearingInRadians: number,\n intensity: number,\n color: { r: number; g: number; b: number },\n ): void {\n const cosine = Math.cos(bearingInRadians);\n const sine = Math.sin(bearingInRadians);\n (this.uniforms.lightPosition.value as Vector3).set(\n position.x * cosine - position.y * sine,\n position.x * sine + position.y * cosine,\n position.z,\n );\n this.uniforms.lightIntensity.value = intensity;\n (this.uniforms.lightColor.value as Vector3).set(color.r, color.g, color.b);\n }\n}\n\n/**\n * Depth-only prepass companion to {@link FillExtrusionMaterial}. Rendering the scene\n * with this material first fills the depth buffer with the nearest landmark surfaces,\n * so the colour pass shades each pixel exactly once — overlapping translucent walls\n * don't blend on top of each other, mirroring how maplibre's depth-writing\n * fill-extrusion pass keeps buildings from showing through one another.\n *\n * Shares {@link FillExtrusionMaterial}'s vertex shader and alpha mask so both passes\n * produce identical depth values and cut out the same fragments.\n *\n * @group Landmarks 3D\n */\nexport class FillExtrusionDepthMaterial extends ShaderMaterial {\n constructor(alphaMask?: FillExtrusionAlphaMask) {\n super({\n ...(alphaMask && {\n uniforms: {\n alphaMask: { value: alphaMask.map },\n alphaMaskCutoff: { value: alphaMask.cutoff },\n },\n defines: { USE_ALPHA_MASK: '' },\n }),\n vertexShader: VERTEX_SHADER,\n fragmentShader: /* glsl */ `\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n gl_FragColor = vec4(0.0);\n }\n `,\n side: alphaMask ? DoubleSide : FrontSide,\n colorWrite: false,\n });\n }\n}\n","import type { Mesh, Object3D } from 'three';\n\n/**\n * @ignore\n */\nexport const isMesh = (object: Object3D): object is Mesh => (object as Mesh).isMesh === true;\n","import type { Map as MapLibreMap } from 'maplibre-gl';\nimport { DoubleSide, Group, LoaderUtils, type MeshStandardMaterial, REVISION, type WebGLRenderer } from 'three';\nimport { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';\nimport { toCreasedNormals } from 'three/examples/jsm/utils/BufferGeometryUtils.js';\nimport type { ModelsSourceSpecification } from './types/modelsSpecifications';\nimport { isMesh } from './utils';\n\n// Edges sharper than 30° keep their own normals (crisp corners); smoother ones share one (no grain).\nconst CREASE_ANGLE_RADIANS = Math.PI / 6;\n\nconst DEFAULT_TRANSCODER_PATH = `https://unpkg.com/three@0.${REVISION}.x/examples/jsm/libs/basis/`;\n\n// Private maplibre-gl internals this source needs, reached via an `unknown` cast.\ninterface MapLibreTransformInternals {\n transform: { tileSize: number };\n}\n\n// Element of maplibre's coveringTiles() result; exposes `.key`, `.canonical.{z,url}` and `.toString()`.\ntype CoveringTile = ReturnType<MapLibreMap['coveringTiles']>[number];\n\n/**\n * Source of tiled glTF/GLB models for a {@link ModelsLayer}.\n *\n * Tracks the tiles covering the current viewport, fetches each tile's models once\n * and keeps them in a Three.js {@link Group} that the owning layer renders.\n *\n * @group Landmarks 3D\n */\nexport class ModelsSource {\n type = 'models' as const;\n id: string;\n minzoom: number;\n maxzoom: number;\n map!: MapLibreMap;\n tiles: Array<string>;\n scene: Group;\n loadedTiles = new Set<string>();\n /** Optional consumer callback fired once per successful tile load. */\n onTileLoaded?: (scene: Group, key: string) => void;\n\n private readonly ktx2Loader: KTX2Loader;\n private readonly gltfLoader: GLTFLoader;\n private readonly specification: ModelsSourceSpecification;\n private readonly withCredentials: boolean;\n\n constructor(id: string, options: ModelsSourceSpecification) {\n this.id = id;\n this.minzoom = options.minzoom ?? 0;\n this.maxzoom = options.maxzoom ?? 22;\n this.tiles = options.tiles;\n this.specification = options;\n this.withCredentials = options.withCredentials ?? false;\n this.scene = new Group();\n this.ktx2Loader = new KTX2Loader()\n .setCrossOrigin('anonymous')\n .setTranscoderPath(options.transcoderPath ?? DEFAULT_TRANSCODER_PATH);\n this.gltfLoader = new GLTFLoader().setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(MeshoptDecoder);\n }\n\n onAdd(map: MapLibreMap, renderer: WebGLRenderer) {\n this.map = map;\n this.ktx2Loader.detectSupport(renderer);\n }\n\n updateTiles() {\n if (this.tiles.length === 0) {\n return;\n }\n\n for (const tile of this.coveringTiles()) {\n this.loadTile(tile);\n }\n }\n\n private coveringTiles(): CoveringTile[] {\n const transform = (this.map as unknown as MapLibreTransformInternals).transform;\n return this.map.coveringTiles({\n tileSize: transform.tileSize,\n minzoom: this.minzoom,\n maxzoom: this.maxzoom,\n });\n }\n\n // The source loads a single zoom level, so tiles never overlap; each is kept on screen once loaded.\n private loadTile(tile: CoveringTile): void {\n if (this.loadedTiles.has(tile.key)) {\n return;\n }\n this.loadedTiles.add(tile.key);\n\n const url = tile.canonical.url(this.tiles, this.map.getPixelRatio(), this.specification.scheme);\n this.fetchTile(url)\n .then((scene) => {\n if (!scene) {\n return; // expected empty tile (no landmark / not produced)\n }\n prepareTileMeshes(scene);\n scene.userData = { isTile: true, key: tile.key };\n this.scene.add(scene);\n this.onTileLoaded?.(scene, tile.key);\n })\n .catch((error: { message?: string }) => {\n console.warn(`Problem with loading ${tile.toString()}: ${error?.message ?? 'unknown'}`);\n });\n }\n\n // Fetch via the global `fetch` (not three.js' XHR) so it rides any installed\n // session wrapper — e.g. the demo-BFF cookie gate — then parse the GLB.\n private async fetchTile(url: string): Promise<Group | null> {\n const response = await fetch(url, this.withCredentials ? { credentials: 'include' } : undefined);\n // 204 = no landmark in this tile, 404 = tile not produced — both expected.\n if (response.status === 204 || response.status === 404) {\n return null;\n }\n if (!response.ok) {\n throw new Error(`tile request failed with ${response.status}`);\n }\n const buffer = await response.arrayBuffer();\n // Resolve relative resources against the tile URL, as GLTFLoader.load would.\n const { scene } = await this.gltfLoader.parseAsync(buffer, LoaderUtils.extractUrlBase(url));\n return scene;\n }\n}\n\n// Stores each mesh's authored material and re-creases its normals so untextured shading keeps crisp edges.\nconst prepareTileMeshes = (scene: Group): void => {\n scene.traverse((child) => {\n if (!isMesh(child)) {\n return;\n }\n\n const material = child.material as MeshStandardMaterial;\n material.alphaTest = 0.1;\n material.side = DoubleSide;\n child.userData.originalMaterial = material;\n child.geometry = toCreasedNormals(child.geometry, CREASE_ANGLE_RADIANS);\n });\n};\n","import type { CustomLayerInterface, CustomRenderMethodInput, Map as MapLibreMap } from 'maplibre-gl';\nimport {\n AmbientLight,\n Camera,\n DirectionalLight,\n Group,\n Matrix4,\n type Mesh,\n type MeshStandardMaterial,\n Scene,\n type Texture,\n WebGLRenderer,\n} from 'three';\nimport { FillExtrusionDepthMaterial, FillExtrusionMaterial } from './FillExtrusionMaterial';\nimport { ModelsSource } from './ModelsSource';\nimport type { ModelsLayerSpecification, ModelsSourceSpecification } from './types/modelsSpecifications';\nimport { isMesh } from './utils';\n\n// EPSG:3857 metres → mercator [0..1]; Z additionally scales by 1/cos(lat) per frame in render().\nconst TILE_SCALE = 1 / (2 * 20037508.34);\nconst TRANSLATE_X = 0.5;\nconst TRANSLATE_Y = 0.5;\n\nconst toRadians = (degrees: number): number => (degrees * Math.PI) / 180;\n\n// Directional light strength for the landmark shading, tuned to blend with the basemap buildings.\nconst LANDMARK_LIGHT_INTENSITY = 0.5;\n\n// Subset of the private maplibre-gl internals this layer needs, reached via an `unknown` cast.\ninterface MapLibreRenderInternals {\n transform: { tileZoom: number; bearingInRadians?: number };\n style: {\n light?: {\n properties?: {\n get(key: 'intensity'): number;\n get(key: 'position'): { x: number; y: number; z: number };\n get(key: 'anchor'): 'map' | 'viewport';\n get(key: 'color'): { r: number; g: number; b: number };\n };\n };\n };\n}\n\n/**\n * MapLibre custom layer that renders tiled glTF/GLB models with Three.js.\n *\n * The layer owns a {@link ModelsSource} that streams model tiles for the current\n * viewport, renders them as maplibre-style fill-extrusion buildings, and keeps its\n * lighting in sync with the map style light.\n *\n * @group Landmarks 3D\n */\nexport class ModelsLayer implements CustomLayerInterface {\n id: string;\n type = 'custom' as const;\n renderingMode = '3d' as const;\n\n renderer!: WebGLRenderer;\n scene!: Scene;\n camera!: Camera;\n tiles: Group;\n directionalLight!: DirectionalLight;\n ambientLight!: AmbientLight;\n map!: MapLibreMap;\n source: ModelsSource;\n minzoom = 0;\n maxzoom = 22;\n visible = true;\n\n diffuseColor = '#ffffff';\n\n private readonly fillExtrusionMaterial = new FillExtrusionMaterial();\n private readonly depthPrepassMaterial = new FillExtrusionDepthMaterial();\n // Per-texture alpha-masked variants so untextured shading keeps each mesh's carved silhouette.\n private readonly maskedFillExtrusionMaterials = new Map<\n Texture,\n { color: FillExtrusionMaterial; depth: FillExtrusionDepthMaterial }\n >();\n\n constructor(options: ModelsLayerSpecification, source: ModelsSourceSpecification) {\n this.id = options.id;\n this.source = new ModelsSource(this.id, source);\n this.tiles = this.source.scene;\n this.updateProperties(options);\n }\n\n updateProperties(options: ModelsLayerSpecification) {\n this.visible = options.layout?.visibility !== 'none';\n this.diffuseColor = options.paint?.['models-base-color'] ?? '#ffffff';\n this.minzoom = options.minzoom ?? 0;\n this.maxzoom = options.maxzoom ?? 22;\n }\n\n setDirectionalLight(radius: number, phi: number, theta: number) {\n this.directionalLight.position.setFromSphericalCoords(radius, toRadians(phi), toRadians(theta));\n }\n\n onAdd(map: MapLibreMap, gl: WebGLRenderingContext | WebGL2RenderingContext) {\n this.map = map;\n\n this.scene = new Scene();\n this.tiles = this.source.scene;\n this.camera = new Camera();\n\n this.directionalLight = new DirectionalLight(0xffffff, 0.5);\n this.setDirectionalLight(1.15, 90, 0);\n this.scene.add(this.directionalLight);\n\n this.ambientLight = new AmbientLight(0xffffff, 0.1);\n this.scene.add(this.ambientLight);\n\n this.scene.add(this.tiles);\n\n this.renderer = new WebGLRenderer({\n canvas: map.getCanvas(),\n context: gl,\n antialias: true,\n });\n this.renderer.autoClear = false;\n\n this.source.onAdd(map, this.renderer);\n }\n\n render(_gl: WebGLRenderingContext | WebGL2RenderingContext, options: CustomRenderMethodInput) {\n const internals = this.map as unknown as MapLibreRenderInternals;\n const tileZoom = internals.transform.tileZoom;\n const inZoomRange = tileZoom >= this.minzoom && tileZoom <= this.maxzoom;\n this.tiles.visible = this.visible && inZoomRange;\n\n if (!(this.visible && inZoomRange)) {\n return;\n }\n\n this.syncLighting();\n this.source.updateTiles();\n this.alignCameraToMap(options);\n\n this.renderer.resetState();\n if (this.needsTranslucentPrepass()) {\n this.renderDepthPrepass();\n }\n\n this.applyMaterials();\n this.renderer.render(this.scene, this.camera);\n this.map.triggerRepaint();\n }\n\n // Canonical maplibre v5 pattern: MVP*scale on camera.projectionMatrix, via mainMatrix because v5 flattens Z.\n private alignCameraToMap(options: CustomRenderMethodInput) {\n const latitudeInRadians = (this.map.getCenter().lat * Math.PI) / 180;\n const zScale = TILE_SCALE / Math.cos(latitudeInRadians);\n const mapLibreScale = new Matrix4();\n mapLibreScale.set(TILE_SCALE, 0, 0, TRANSLATE_X, 0, -TILE_SCALE, 0, TRANSLATE_Y, 0, 0, zScale, 0, 0, 0, 0, 1);\n const projection = new Matrix4().fromArray(options.defaultProjectionData.mainMatrix as ArrayLike<number>);\n this.camera.projectionMatrix.copy(projection).multiply(mapLibreScale);\n }\n\n // Translucent shading needs a depth pass so overlapping walls blend with the basemap once per pixel.\n private needsTranslucentPrepass(): boolean {\n const layerOpacity = this.fillExtrusionMaterial.uniforms.layerOpacity.value as number;\n return layerOpacity < 1;\n }\n\n // Per-mesh (not overrideMaterial) so each mesh's alpha mask carves the prepass depth too.\n private renderDepthPrepass() {\n this.tiles.traverse((child) => {\n if (isMesh(child)) {\n child.material = this.maskedFillExtrusionVariant(child)?.depth ?? this.depthPrepassMaterial;\n }\n });\n this.renderer.render(this.scene, this.camera);\n }\n\n setOpacity(value: number) {\n this.forEachFillExtrusionMaterial((material) => material.setLayerOpacity(value));\n }\n\n getMaterial(mesh: Mesh): FillExtrusionMaterial {\n return this.maskedFillExtrusionVariant(mesh)?.color ?? this.fillExtrusionMaterial;\n }\n\n // Fill-extrusion variant masked by the mesh's own texture alpha; null for untextured meshes.\n private maskedFillExtrusionVariant(mesh: Mesh) {\n const original = mesh.userData.originalMaterial as MeshStandardMaterial | undefined;\n const map = original?.map;\n if (!map) {\n return null;\n }\n\n let variant = this.maskedFillExtrusionMaterials.get(map);\n if (!variant) {\n const alphaMask = { map, cutoff: original.alphaTest || 0.1 };\n variant = {\n color: new FillExtrusionMaterial(alphaMask),\n depth: new FillExtrusionDepthMaterial(alphaMask),\n };\n variant.color.copyShadingFrom(this.fillExtrusionMaterial);\n this.maskedFillExtrusionMaterials.set(map, variant);\n }\n return variant;\n }\n\n private forEachFillExtrusionMaterial(apply: (material: FillExtrusionMaterial) => void) {\n apply(this.fillExtrusionMaterial);\n this.maskedFillExtrusionMaterials.forEach((variant) => apply(variant.color));\n }\n\n private syncLighting() {\n // The maplibre style light is private API; fall back to the onAdd defaults when absent.\n const internals = this.map as unknown as MapLibreRenderInternals;\n const properties = internals.style?.light?.properties;\n if (!properties) {\n return;\n }\n\n try {\n // Fixed strength tuned to match the basemap buildings (the standard styles light at ~0.5).\n const intensity = LANDMARK_LIGHT_INTENSITY;\n // `* π` cancels Three.js's internal 1/π Lambert factor, matching maplibre's shading range.\n this.directionalLight.intensity = intensity * Math.PI;\n this.ambientLight.intensity = (1 - intensity) * Math.PI;\n\n const position = properties.get('position');\n this.directionalLight.position.set(position.x, position.y, position.z);\n\n // Viewport-anchored lights rotate with the map bearing, like maplibre's painter.\n const anchor = properties.get('anchor');\n const bearingInRadians = anchor === 'viewport' ? (internals.transform.bearingInRadians ?? 0) : 0;\n const color = properties.get('color');\n this.forEachFillExtrusionMaterial((material) =>\n material.setLight(position, bearingInRadians, intensity, color),\n );\n } catch {\n // Internal API surface changed — keep the configured defaults.\n }\n }\n\n private applyMaterials() {\n this.forEachFillExtrusionMaterial((material) => material.setDiffuseColor(this.diffuseColor));\n\n this.tiles.traverse((child) => {\n if (isMesh(child)) {\n child.material = this.getMaterial(child);\n }\n });\n }\n}\n","import { isProxyCredentialsMode } from '@tomtom-org/maps-sdk/core';\nimport { mapStyleLayerIDs, type TomTomMap } from '@tomtom-org/maps-sdk/map';\nimport type { FilterSpecification } from 'maplibre-gl';\nimport {\n type BasemapBuildingMaterial,\n type Landmarks3DDisplayMode,\n type LandmarksMaterialState,\n resolveDisplayMode,\n} from './displayMode';\nimport { buildLandmarksTileURL } from './landmarksTileURL';\nimport { ModelsLayer } from './ModelsLayer';\nimport type { ModelsLayerSpecification, ModelsSourceSpecification } from './types/modelsSpecifications';\n\n// Load landmark tiles at a single zoom level: the meshes are full-detail at this zoom and upscale\nconst SOURCE_ZOOM = 15;\n\nconst DEFAULT_LAYER_ID = 'orbis-3d-landmarks';\nconst DEFAULT_LAYER_MIN_ZOOM = 10;\nconst DEFAULT_LAYER_MAX_ZOOM = 22;\n\n// Filters out basemap extruded buildings that overlap a landmark, so they don't clip through its mesh.\nconst DEFAULT_BASEMAP_BUILDING_LAYER_ID = '3D - Building';\nconst NO_LANDMARK_FILTER_CLAUSE: FilterSpecification = ['!', ['coalesce', ['get', 'has_landmark'], false]];\n\n// Resolves `fill-extrusion-color` to one CSS colour, for `match` expressions, the last operand is the default.\nconst extractColorFromPaint = (paintColor: unknown): string | null => {\n if (typeof paintColor === 'string') {\n return paintColor;\n }\n\n if (Array.isArray(paintColor) && paintColor[0] === 'match') {\n const fallback = paintColor.at(-1);\n if (typeof fallback === 'string') {\n return fallback;\n }\n }\n\n return null;\n};\n\n/**\n * Configuration for the {@link Landmarks3D} plugin.\n *\n * @group Landmarks 3D\n */\nexport type Landmarks3DOptions = {\n /**\n * Initial display mode for the landmarks. Can also be changed at runtime with\n * {@link Landmarks3D.setDisplayMode}.\n * @defaultValue `'inherited'`\n */\n displayMode?: Landmarks3DDisplayMode;\n /**\n * Whether the landmarks are initially visible. Can also be changed at runtime with\n * {@link Landmarks3D.setVisible}.\n * @defaultValue `true`\n */\n visible?: boolean;\n /**\n * Minimum zoom at which landmarks are rendered.\n * @defaultValue The basemap 3D building layer's minimum zoom, so landmarks appear together\n * with it (falls back to `10` when that layer is absent).\n */\n minZoom?: number;\n /**\n * Maximum zoom at which landmarks are rendered.\n * @defaultValue The basemap 3D building layer's maximum zoom, so landmarks disappear together\n * with it (falls back to `22` when that layer is absent).\n */\n maxZoom?: number;\n /**\n * Send `credentials: 'include'` with each tile request so a proxy session\n * cookie travels with it. By default this tracks the SDK's proxy config, so\n * tiles behave like every other SDK request; set it explicitly only to\n * override that.\n * @defaultValue Auto-detected — `true` when the SDK is configured for a\n * credentialed proxy (no `apiKey` + non-default `commonBaseURL`).\n */\n withCredentials?: boolean;\n};\n\n/**\n * Renders TomTom Orbis 3D Landmarks (high-detail building meshes) on a {@link TomTomMap}.\n *\n * The plugin streams GLB tiles from the Orbis 3D Landmarks API (Private Preview),\n * renders them with Three.js via a MapLibre custom layer, filters overlapping\n * extruded basemap buildings out of the style, and keeps everything restored\n * across map style changes.\n *\n * @remarks\n * Requires an API key with Orbis 3D Landmarks entitlements and a map style that\n * shows extruded 3D buildings for the exclusion filter and `inherited` mode to\n * have an effect.\n *\n * @example\n * ```typescript\n * import { Landmarks3D } from '@tomtom-org/maps-sdk-plugin-landmarks-3d';\n *\n * // assume `map` is your initialized TomTomMap instance\n * const landmarks = new Landmarks3D(map);\n *\n * await landmarks.setDisplayMode('inherited');\n * ```\n *\n * @group Landmarks 3D\n */\nexport class Landmarks3D {\n private readonly map: TomTomMap;\n private readonly layerID = DEFAULT_LAYER_ID;\n private readonly basemapBuildingLayerID = DEFAULT_BASEMAP_BUILDING_LAYER_ID;\n private readonly modelsLayer: ModelsLayer;\n // Explicit zoom-range overrides; when unset, the range tracks the basemap 3D building layer.\n private readonly layerMinZoom?: number;\n private readonly layerMaxZoom?: number;\n private displayMode: Landmarks3DDisplayMode;\n\n /**\n * Creates the plugin bound to a TomTom map and starts rendering landmarks\n * as soon as the map is ready.\n *\n * @param map - The TomTom map instance to render landmarks on.\n * @param options - Optional configuration.\n */\n constructor(map: TomTomMap, options: Landmarks3DOptions = {}) {\n this.map = map;\n this.layerMinZoom = options.minZoom;\n this.layerMaxZoom = options.maxZoom;\n this.displayMode = options.displayMode ?? 'inherited';\n\n const layerSpecification: ModelsLayerSpecification = {\n id: this.layerID,\n type: 'models',\n source: this.layerID,\n minzoom: options.minZoom ?? DEFAULT_LAYER_MIN_ZOOM,\n maxzoom: options.maxZoom ?? DEFAULT_LAYER_MAX_ZOOM,\n };\n const sourceSpecification: ModelsSourceSpecification = {\n type: 'models',\n tiles: [buildLandmarksTileURL()],\n minzoom: SOURCE_ZOOM,\n maxzoom: SOURCE_ZOOM,\n withCredentials: options.withCredentials ?? isProxyCredentialsMode(),\n };\n this.modelsLayer = new ModelsLayer(layerSpecification, sourceSpecification);\n this.modelsLayer.visible = options.visible ?? true;\n\n this.map.addStyleChangeHandler({\n onStyleChanged: () => this.install(),\n });\n this.installWhenReady();\n }\n\n // Installs the layer as soon as the map is ready\n private installWhenReady(): void {\n if (this.map.mapReady) {\n this.install();\n } else {\n this.map.mapLibreMap.once('styledata', () => this.installWhenReady());\n }\n }\n\n /**\n * The underlying MapLibre custom layer, for advanced use beyond this facade.\n */\n get layer(): ModelsLayer {\n return this.modelsLayer;\n }\n\n /**\n * The currently applied display mode.\n */\n getDisplayMode(): Landmarks3DDisplayMode {\n return this.displayMode;\n }\n\n /**\n * Changes how landmarks are rendered relative to the base map.\n *\n * @param mode - The display mode to apply.\n * @returns A promise that resolves once the mode has been applied.\n */\n async setDisplayMode(mode: Landmarks3DDisplayMode): Promise<void> {\n this.displayMode = mode;\n // Before the map is ready the layer isn't installed; install() applies the stored\n // mode once it runs, so simply recording it above is enough in that case.\n if (this.map.mapReady) {\n this.applyDisplayMode();\n }\n }\n\n /**\n * Whether the landmarks are currently visible.\n */\n isVisible(): boolean {\n return this.modelsLayer.visible;\n }\n\n /**\n * Shows or hides the landmarks layer.\n *\n * @param visible - `true` to show landmarks, `false` to hide them.\n * @returns A promise that resolves once the visibility has been applied.\n */\n async setVisible(visible: boolean): Promise<void> {\n this.modelsLayer.visible = visible;\n // When the map isn't ready yet the layer is added (with this visibility) by\n // install(); only an already-installed layer needs an explicit repaint here.\n if (this.map.mapReady) {\n this.map.mapLibreMap.triggerRepaint();\n }\n }\n\n // (Re-)adds the layer and re-applies filter + display mode on first readiness and after style changes.\n private install(): void {\n const mapLibreMap = this.map.mapLibreMap;\n if (!mapLibreMap.getLayer(this.layerID)) {\n mapLibreMap.addLayer(this.modelsLayer, this.landmarkInsertBeforeID());\n }\n\n // The standard styles ship the 3D building layer hidden; show it so landmarks get a 3D city context.\n if (mapLibreMap.getLayer(this.basemapBuildingLayerID)) {\n mapLibreMap.setLayoutProperty(this.basemapBuildingLayerID, 'visibility', 'visible');\n }\n\n this.alignZoomRangeWithBasemapBuildings();\n this.excludeLandmarkBuildingsFromBasemap();\n\n // Late-arriving tiles load with opaque materials; re-apply non-opaque looks to them.\n this.modelsLayer.source.onTileLoaded = () => {\n if (this.resolveCurrentMaterialState().opacity < 1) {\n this.applyDisplayMode();\n }\n };\n\n this.applyDisplayMode();\n }\n\n // Insert before the POI anchor so POIs draw over the meshes and roads stay below; undefined = on top.\n private landmarkInsertBeforeID(): string | undefined {\n const poiAnchor = mapStyleLayerIDs.poi;\n return this.map.mapLibreMap.getLayer(poiAnchor) ? poiAnchor : undefined;\n }\n\n // Tracks the basemap 3D building layer's zoom range so landmarks appear and disappear together with\n // it, unless the caller pinned an explicit min/max zoom. No-op when the building layer is absent.\n private alignZoomRangeWithBasemapBuildings(): void {\n const buildingLayer = this.map.mapLibreMap.getLayer(this.basemapBuildingLayerID);\n if (!buildingLayer) {\n return;\n }\n\n this.modelsLayer.minzoom = this.layerMinZoom ?? buildingLayer.minzoom ?? DEFAULT_LAYER_MIN_ZOOM;\n this.modelsLayer.maxzoom = this.layerMaxZoom ?? buildingLayer.maxzoom ?? DEFAULT_LAYER_MAX_ZOOM;\n }\n\n private excludeLandmarkBuildingsFromBasemap(): void {\n const mapLibreMap = this.map.mapLibreMap;\n if (!mapLibreMap.getLayer(this.basemapBuildingLayerID)) {\n return;\n }\n\n const existing = mapLibreMap.getFilter(this.basemapBuildingLayerID);\n if (existing && JSON.stringify(existing).includes(JSON.stringify(NO_LANDMARK_FILTER_CLAUSE))) {\n return;\n }\n\n // Asserted because `['all', ...]` arms span FilterSpecification's expression and legacy variants.\n const combined = (\n existing ? ['all', existing, NO_LANDMARK_FILTER_CLAUSE] : NO_LANDMARK_FILTER_CLAUSE\n ) as FilterSpecification;\n mapLibreMap.setFilter(this.basemapBuildingLayerID, combined);\n }\n\n // Reads the basemap building colour for `inherited` mode; null when absent or not resolvable to one colour.\n private readBasemapBuildingMaterial(): BasemapBuildingMaterial | null {\n const mapLibreMap = this.map.mapLibreMap;\n if (!mapLibreMap.getLayer(this.basemapBuildingLayerID)) {\n return null;\n }\n\n const paintColor = mapLibreMap.getPaintProperty(this.basemapBuildingLayerID, 'fill-extrusion-color');\n const diffuse = extractColorFromPaint(paintColor);\n if (!diffuse) {\n return null;\n }\n\n return { diffuse };\n }\n\n private resolveCurrentMaterialState(): LandmarksMaterialState {\n const basemapMaterial = this.displayMode === 'inherited' ? this.readBasemapBuildingMaterial() : null;\n return resolveDisplayMode(this.displayMode, basemapMaterial);\n }\n\n private applyDisplayMode(): void {\n const state = this.resolveCurrentMaterialState();\n this.modelsLayer.diffuseColor = state.diffuseColor;\n this.modelsLayer.setOpacity(state.opacity);\n this.map.mapLibreMap.triggerRepaint();\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -2,13 +2,13 @@
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.2",
5
+ "version": "0.0.4",
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
9
  "peerDependencies": {
10
10
  "three": "^0.170.0",
11
- "@tomtom-org/maps-sdk": "0.49.0"
11
+ "@tomtom-org/maps-sdk": "0.50.4"
12
12
  },
13
13
  "devDependencies": {
14
14
  "@microsoft/api-extractor": "7.58.9",