// src/modules/AppBuilder.js
import initRenderer from './initRenderer.js';
import initScene from './initScene.js';
import initCamera from './initCamera.js';
import initControls from './initControls.js';
import ModelLoader from './ModelLoader.js';
import { setupResizeHandler } from './resizeHandler.js';
import { createVideoMeshes } from './createVideoMeshes.js';
import { PointerHandler } from './PointerHandler.js';
import { AmbientLight, Clock, BufferGeometry, Mesh, MeshBasicMaterial, OrthographicCamera, WebGLRenderer, RingGeometry, DoubleSide, Vector3 } from 'three';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js';
import Visitor from './Visitor.js';
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';
// loader globals
const ktx2Loader = new KTX2Loader().setTranscoderPath('./libs/basis/');
const clock = new Clock();
const cameraDir = new Vector3();
function resolveIpfs(p) {
// 1) Full URLs: leave them alone
if (/^[a-z]+:\/\//i.test(p)) {
return p;
}
// 2) Bare CIDs: alphanumeric-only, no dots or slashes, typical CID lengths ≥46
if (/^[A-Za-z0-9]+$/.test(p) && p.length >= 46) {
return `https://ipfs.io/ipfs/${p}`;
}
// 3) Everything else (e.g. relative paths), leave alone
return p;
}
export async function buildGallery(config) {
// 1. Pull everything from the user‑fetched JSON
const {
modelPath,
interactivesPath,
backgroundTexture,
enablePostProcessing,
sidebar
} = config;
// 2. Turn raw CIDs into full URLs
//const modelUrl = resolveIpfs(modelPath);
const modelUrl = modelPath
const backgroundUrl = backgroundTexture
//const backgroundUrl = backgroundTexture
// ? resolveIpfs(backgroundTexture)
// : null;
console.log('modelUrl', modelUrl);
console.log('backgroundUrl', backgroundUrl);
// 3. Three.js + BVH setup
Mesh.prototype.raycast = acceleratedRaycast;
BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
// 4. Main scene + renderer
const renderer = initRenderer();
const scene = initScene(backgroundUrl, renderer);
scene.name = 'mainScene';
scene.add(new AmbientLight(0xffffff, 2));
// 5. Camera, controls, resize
const camera = initCamera();
setupResizeHandler(renderer, camera);
const controls = initControls(camera, renderer.domElement);
// 6. 2D labels overlay
const css2DMain = new CSS2DRenderer();
css2DMain.setSize(window.innerWidth, window.innerHeight);
css2DMain.domElement.style.cssText = 'position:absolute;top:0;pointer-events:none';
document.body.appendChild(css2DMain.domElement);
// 7. Mini‑map scene + renderer
const rendererMap = new WebGLRenderer();
rendererMap.setClearColor(0x142236);
rendererMap.setSize(500, 500);
const sceneMap = initScene(null, rendererMap);
sceneMap.name = 'sceneMap';
sceneMap.add(new AmbientLight(0xffffff, 2));
const cameraMap = new OrthographicCamera(-40, 40, 40, -40, 0.1, 1000);
cameraMap.position.set(10, 50, 10);
cameraMap.up.set(0, 0, -1);
cameraMap.lookAt(0, 0, 0);
const css2DMap = new CSS2DRenderer();
css2DMap.setSize(500, 500);
css2DMap.domElement.style.cssText = 'position:absolute;top:0;pointer-events:none';
// 8. Gather dependencies
const deps = {
ktx2Loader,
camera,
controls,
scene,
sceneMap,
cameraMap,
renderer,
rendererMap,
css2DRenderer: css2DMap,
params: {
gravity: -10,
visitorSpeed: 10,
heightOffset: { x: 0, y: 3.5, z: 0 },
enablePostProcessing,
},
};
// 9. Visitor + interaction
const visitor = new Visitor(deps);
deps.visitor = visitor;
scene.add(visitor);
const popupCallback = setupModal();
new PointerHandler({ camera, scene, visitor, popupCallback, deps });
// 10. Load model & videos
const modelLoader = new ModelLoader(deps, scene);
await modelLoader.loadModel(modelUrl, interactivesPath);
createVideoMeshes(scene);
scene.updateMatrixWorld(true);
visitor.reset();
console.log('App built!');
// 11. Sidebar (if provided)
if (sidebar) {
buildSidebar(sidebar);
setupSidebarButtons(deps);
}
addSidebarListeners();
// 12. Visitor circle on mini‑map
const circle = new Mesh(
new RingGeometry(0.1, 1, 32),
new MeshBasicMaterial({ color: 0xa2c7ff, side: DoubleSide, transparent: true, opacity: 0.8, depthWrite: false })
);
circle.name = 'circleMap';
circle.rotation.x = Math.PI / 2;
circle.material.depthTest = false;
deps.sceneMap.add(circle);
deps.visitorMapCircle = circle;
// 13. Animation loops
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
if (deps.visitor && deps.collider) deps.visitor.update(dt, deps.collider);
if (deps.visitorMapCircle) {
const pos = deps.visitor.position.clone();
pos.y += 4;
deps.visitorMapCircle.position.copy(pos);
}
controls.update();
css2DMain.render(scene, camera);
renderer.render(scene, camera);
}
function animateMap() {
requestAnimationFrame(animateMap);
camera.getWorldDirection(cameraDir);
sceneMap.rotation.y = -Math.atan2(cameraDir.x, cameraDir.z) + Math.PI;
sceneMap.updateMatrixWorld();
rendererMap.render(sceneMap, cameraMap);
css2DMap.render(sceneMap, cameraMap);
}
animate();
animateMap();
hideOverlay();
}
// … rest of your helper functions (hideOverlay, setupModal, etc.) remain unchanged …
function hideOverlay() {
const overlay = document.getElementById('overlay');
const sidebar = document.querySelector('.sidebar');
const btn = document.getElementById('btn');
if (!overlay) return;
overlay.style.transition = 'opacity 1s ease';
overlay.style.opacity = '0';
setTimeout(() => {
overlay.style.display = 'none';
if (sidebar && !sidebar.classList.contains('open')) {
sidebar.style.display = 'flex';
sidebar.classList.add('open');
}
if (btn && !btn.classList.contains('open')) {
btn.style.display = 'block';
btn.classList.add('open');
}
}, 1000);
}
function setupModal() {
const sidebar = document.querySelector('.sidebar');
const btn = document.getElementById('btn');
const modalOverlay = document.getElementById('modalOverlay');
const modal = modalOverlay.querySelector('.modal');
const modalImg = modalOverlay.querySelector('img');
const modalDesc = modalOverlay.querySelector('.modal-description');
const closeBtn = document.getElementById('closeModal');
modalOverlay.addEventListener('pointerdown', (e) => {
if (!modal.contains(e.target)) {
modalOverlay.classList.add('hidden');
modalOverlay.classList.remove('show');
modalImg.src = '';
modalDesc.textContent = '';
}
});
['touchstart', 'touchmove'].forEach(evt => {
modalDesc.addEventListener(evt, e => e.stopPropagation(), { passive: true });
});
closeBtn.addEventListener('click', () => {
modalOverlay.classList.add('hidden');
modalOverlay.classList.remove('show');
modalImg.src = '';
modalDesc.textContent = '';
modalDesc.scrollTop = 0;
});
makeModalDraggable(modal);
return function showModal(userData) {
if (!userData) return;
if (sidebar?.classList.contains('open')) {
sidebar.classList.remove('open');
btn?.classList.remove('open');
}
if (userData.Map) modalImg.src = userData.Map;
if (userData.opis) modalDesc.textContent = userData.opis;
modalOverlay.classList.remove('hidden');
modalOverlay.classList.add('show');
setTimeout(() => {
modalDesc.scrollTop = 0;
}, 50);
};
}
function makeModalDraggable(modal) {
const dragHandle = modal.querySelector('.modal-image-container') || modal;
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
const startDrag = (e) => {
if (e.target.closest('.modal-description')) return;
isDragging = true;
const rect = modal.getBoundingClientRect();
offsetX = (e.clientX || e.touches[0].clientX) - rect.left;
offsetY = (e.clientY || e.touches[0].clientY) - rect.top;
modal.style.transition = 'none';
modal.style.position = 'absolute';
modal.style.zIndex = 1001;
document.addEventListener('pointermove', onDrag);
document.addEventListener('pointerup', stopDrag);
};
const onDrag = (e) => {
if (!isDragging) return;
const x = (e.clientX || e.touches[0].clientX) - offsetX;
const y = (e.clientY || e.touches[0].clientY) - offsetY;
modal.style.left = `${x}px`;
modal.style.top = `${y}px`;
modal.style.margin = '0';
};
const stopDrag = () => {
isDragging = false;
document.removeEventListener('pointermove', onDrag);
document.removeEventListener('pointerup', stopDrag);
};
dragHandle.style.cursor = 'grab';
dragHandle.addEventListener('pointerdown', startDrag);
}
function buildSidebar(sidebarConfig) {
const sidebar = document.querySelector('.sidebar');
if (!sidebar) return;
const navList = sidebar.querySelector('.nav-list');
const logoDiv = sidebar.querySelector('.logo_name');
logoDiv.textContent = sidebarConfig.logo.text;
sidebarConfig.items.forEach(item => {
const li = document.createElement('li');
if (item.link) {
li.innerHTML = `
${item.label}
`;
} else {
li.innerHTML = `
${item.label}