metalness: 0.05, ...opts });
return cache[key];
}
function box(w, h, d, mat, x, y, z) {
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
m.position.set(x, y, z); m.castShadow = true; m.receiveShadow = true; scene.add(m); return m;
}
function cyl(r, h, mat, x, y, z) {
const m = new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, 20), mat);
m.position.set(x, y, z); m.castShadow = true; scene.add(m); return m;
}
// ---------- Материалы ----------
const woodTop = M('#7a4a22');
const legMat = M('#2a2a2a', { metalness: 0.4 });
const chairDark = M('#2f2f32');
const chairOrange = M('#e0662b');
const sofaVelvet = M('#4a2a22', { roughness: 0.9 });
const cushionGreen = M('#1f6b3a');
const kidsSofaMat = M('#ffb347');
const stageMat = M('#c9c2b8');
const danceMat = M('#e8c9a0');
const lampBlack = M('#111111', { roughness: 0.9 });
const lampWhite = M('#f5f5f0');
// ---------- Пол, стены, окна ----------
const floor = new THREE.Mesh(new THREE.PlaneGeometry(26, 18), M('#a0683a', { roughness: 0.8 }));
floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);
const winMesh = new THREE.Mesh(new THREE.BoxGeometry(26, 4, 0.2), M('#9fd3ff', { transparent: true, opacity: 0.35 }));
winMesh.position.set(0, 2, -9.1); scene.add(winMesh);
box(0.3, 4, 18, M('#3a2f2a'), -13.1, 2, 0);
box(0.3, 4, 18, M('#3a2f2a'), 13.1, 2, 0);
// ---------- Стулья и столы ----------
function chairAt(x, z, mat, rot) {
const g = new THREE.Group();
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.08, 0.45), mat); seat.position.y = 0.45;
const back = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.5, 0.06), mat); back.position.set(0, 0.72, -0.2);
g.add(seat, back);
g.position.set(x, 0, z); g.rotation.y = rot;
g.traverse(o => { if (o.isMesh) o.castShadow = true; });
scene.add(g);
}
// стол вдоль оси X (dirX=true) или Z; n стульев с каждой стороны
function table(cx, cz, len, dirX, n) {
const w = dirX ? len : 0.9, d = dirX ? 0.9 : len;
box(w, 0.08, d, woodTop, cx, 0.75, cz);
cyl(0.12, 0.7, legMat, cx, 0.37, cz);
const step = len / n;
for (let i = 0; i < n; i++) {
const p = -len / 2 + step * (i + 0.5);
const mat = i % 2 ? chairOrange : chairDark;
if (dirX) {
chairAt(cx + p, cz - 0.7, mat, 0);
chairAt(cx + p, cz + 0.7, mat, Math.PI);
} else {
chairAt(cx - 0.7, cz + p, mat, -Math.PI / 2);
chairAt(cx + 0.7, cz + p, mat, Math.PI / 2);
}
}
}
// Ряд у окна: 3 стола по 10
table(-4, -6.5, 3.2, true, 5);
table(0, -6.5, 3.2, true, 5);
table(4, -6.5, 3.2, true, 5);
// Длинный ряд: 20 мест (10 с каждой стороны)
table(0, -3, 10, true, 10);
// Голубая зона у фотозоны: стол на 10
table(-9.5, 0, 4.5, false, 5);
// Нижние ряды: 4 стола по 12 (6 с каждой стороны)
table(-4, 1.5, 4.5, true, 6);
table(3.5, 1.5, 4.5, true, 6);
table(-4, 4.5, 4.5, true, 6);
table(3.5, 4.5, 4.5, true, 6);
// ---------- Диваны детской зоны (12 детей) ----------
for (const z of [-7.5, -5, -2.5]) {
box(1.0, 0.45, 2.3, kidsSofaMat, 11.2, 0.25, z);
box(0.3, 0.8, 2.3, kidsSofaMat, 11.9, 0.6, z);
}
// Игровая / детская комната
const play = new THREE.Mesh(new THREE.PlaneGeometry(3, 2.5), M('#2d6b2d'));
play.rotation.x = -Math.PI / 2; play.position.set(11, 0.02, -0.2); scene.add(play);
box(1.2, 0.5, 0.9, M('#e8e0d0'), 11, 0.3, 0.8);
// ---------- Сцены и танцпол ----------
box(14, 0.5, 1.2, stageMat, 1, 0.25, 9); // сцена внизу
box(2, 0.5, 6, stageMat, 10.5, 0.25, 5.5); // сцена справа
const dance = new THREE.Mesh(new THREE.CircleGeometry(1, 48), danceMat);
dance.rotation.x = -Math.PI / 2; dance.scale.set(4, 1.5, 1); dance.position.set(0, 0.02, 6.8); dance.receiveShadow = true; scene.add(dance);
// ---------- Фотозона ----------
const photoZone = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), M('#fff3d6'));
photoZone.rotation.x = -Math.PI / 2; photoZone.position.set(-10, 0.02, -6); scene.add(photoZone);
for (const [x, z] of [[-11.5, -7.5], [-8.5, -7.5], [-11.5, -4.5]]) {
cyl(0.35, 0.8, lampBlack, x, 0.4, z);
const leaf = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 12), M('#2d7a3a'));
leaf.position.set(x, 1.1, z); scene.add(leaf);
}
// Фиолетовый декор у окон
box(8, 0.1, 0.3, M('#b56cd9'), 0, 0.1, -8.5);
// ---------- Подвесные светильники ----------
for (const [x, z, mat] of [[0, -3, lampBlack], [-4, 1.5, lampWhite], [3.5, 4.5, lampBlack], [-9.5, 0, lampWhite], [4, -6.5, lampBlack], [-4, -6.5, lampWhite]]) {
const sph = new THREE.Mesh(new THREE.SphereGeometry(0.35, 20, 16), mat);
sph.position.set(x, 3.2, z); scene.add(sph);
}
// ---------- Подписи зон (HTML поверх 3D) ----------
const labelEls = [];
function addLabel(text, x, y, z) {
const el = document.createElement('div');
el.textContent = text;
el.style.cssText = 'position:absolute;color:#fff;background:rgba(0,0,0,.55);padding:3px 7px;border-radius:6px;font-size:12px;transform:translate(-50%,-50%);pointer-events:none;white-space:nowrap;z-index:1;';
document.body.appendChild(el);
labelEls.push({ el, p: new THREE.Vector3(x, y, z) });
}
addLabel('ФОТОЗОНА', -10, 1.6, -6);
addLabel('30 мест (3 стола × 10)', 0, 1.6, -9.2);
addLabel('20 мест (4 стола сдвинуты)', 0, 1.6, -1.2);
addLabel('стол на 10', -9.5, 1.6, -2.8);
addLabel('каждый стол — 12 мест', 0, 1.6, 3);
addLabel('ТАНЦПОЛ', 0, 1.6, 6.8);
addLabel('СЦЕНА', 1, 1.2, 10);
addLabel('ДЕТСКАЯ — 12 мест', 11, 1.8, -5);
addLabel('СЦЕНА', 10.5, 1.2, 8.5);
function updateLabels() {
const w = window.innerWidth, h = window.innerHeight;
for (const L of labelEls) {
const v = L.p.clone().project(camera);
L.el.style.left = ((v.x + 1) / 2 * w) + 'px';
L.el.style.top = ((-v.y + 1) / 2 * h) + 'px';
L.el.style.display = v.z > 1 ? 'none' : 'block';
}
}
// ---------- Ракурсы ----------
const views = {
top: [[0, 30, 0.01], [0, 0, 0]],
general: [[-16, 14, 18], [0, 0, 0]],
stage: [[0, 3, 14], [0, 1, -4]],
photo: [[-11, 2.5, -7.5], [3, 1, 2]],
kids: [[9, 2.5, -1], [-3, 1, -3]],
window: [[0, 2.5, -8], [0, 1, 6]],
closeup: [[1, 2.5, 9], [1, 0.8, 4.5]]
};
let anim = null;
window.view = (k) => {
const v = views[k];
anim = { from: camera.position.clone(), to: new THREE.Vector3(...v[0]),
tf: controls.target.clone(), tt: new THREE.Vector3(...v[1]), t: 0 };
};
// ---------- Цикл отрисовки ----------
function tick() {
requestAnimationFrame(tick);
if (anim) {
anim.t = Math.min(1, anim.t + 0.025);
const e = anim.t < 0.5 ? 2 * anim.t * anim.t : 1 - Math.pow(-2 * anim.t + 2, 2) / 2;
camera.position.lerpVectors(anim.from, anim.to, e);
controls.target.lerpVectors(anim.tf, anim.tt, e);
if (anim.t >= 1) anim = null;
}
controls.update();
renderer.render(scene, camera);
updateLabels();
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
tick();