From b1b11b47f75f90ae4593452fb021d4acd4a6b085 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 2 Mar 2026 22:56:51 +0000 Subject: [PATCH] Render image assets as textured planes in MapLibre demo. This loads image files via backend asset links (with auth when available), applies them as Three.js textures on plane meshes, and falls back to primitive placeholders if texture loading fails. Made-with: Cursor --- web/maplibre-demo.js | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/web/maplibre-demo.js b/web/maplibre-demo.js index 9c34905..54a6199 100644 --- a/web/maplibre-demo.js +++ b/web/maplibre-demo.js @@ -39,6 +39,7 @@ let threeCamera; const featureMeshes = new Map(); const modelTemplateCache = new Map(); const gltfLoader = new GLTFLoader(); +const textureLoader = new THREE.TextureLoader(); const ownFeatureMarkers = new Map(); const ownFeatureCoords = new Map(); let renderCycle = 0; @@ -143,6 +144,13 @@ function is3DAsset(asset) { return kind === "3d" && ext === "glb"; } +function isImageAsset(asset) { + if (!asset) return false; + const kind = String(asset.kind || "").toLowerCase(); + const ext = normalizeExt(asset.ext) || extFromLink(asset.link); + return kind === "image" && ["jpg", "jpeg", "png", "webp"].includes(ext); +} + async function renderSharedAssetFromQuery() { const params = new URLSearchParams(window.location.search); if (params.get("shared") !== "1") return; @@ -283,6 +291,42 @@ function addFallbackMesh(featureId, lng, lat, isPublic, kind) { map.triggerRepaint(); } +async function loadTextureFromAssetLink(assetLink) { + const assetURL = client.resolveRelativeLink(assetLink); + const headers = accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined; + const response = await fetch(assetURL, { headers }); + if (!response.ok) { + throw new Error(`Failed to load image asset: HTTP ${response.status}`); + } + const blob = await response.blob(); + const objectURL = URL.createObjectURL(blob); + try { + const texture = await new Promise((resolve, reject) => { + textureLoader.load(objectURL, resolve, undefined, reject); + }); + texture.colorSpace = THREE.SRGBColorSpace; + texture.needsUpdate = true; + return texture; + } finally { + URL.revokeObjectURL(objectURL); + } +} + +async function addImagePlaneMesh(featureId, lng, lat, asset) { + const texture = await loadTextureFromAssetLink(asset.link); + const geometry = new THREE.PlaneGeometry(1.8, 1.8); + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + side: THREE.DoubleSide, + }); + const mesh = new THREE.Mesh(geometry, material); + positionObjectOnMap(mesh, lng, lat, 20); + threeScene.add(mesh); + featureMeshes.set(featureId, mesh); + map.triggerRepaint(); +} + function prepareModelRoot(modelRoot) { const box = new THREE.Box3().setFromObject(modelRoot); if (box.isEmpty()) return; @@ -344,6 +388,14 @@ async function addObjectMeshFromAsset(featureId, lng, lat, asset, cycleID) { } if (!is3DAsset(asset) || !asset.link) { + if (isImageAsset(asset) && asset.link) { + try { + await addImagePlaneMesh(featureId, lng, lat, asset); + return true; + } catch (error) { + console.warn(`Failed to load image texture for feature ${featureId}:`, error); + } + } const ext = normalizeExt(asset.ext) || extFromLink(asset.link); if (String(asset.kind || "").toLowerCase() === "3d" && ext === "gltf") { console.warn(