furniture/public/js/main.js
2025-08-16 07:20:36 +00:00

284 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// public/js/main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// --- Базовая настройка сцены ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xd0d0d0);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.set(1500, 1400, 2500);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 1.5 );
scene.add( hemiLight );
const dirLight = new THREE.DirectionalLight( 0xffffff, 1.5 );
dirLight.position.set( -1000, 2000, 1000 );
dirLight.castShadow = true;
dirLight.shadow.camera.top = 2000; dirLight.shadow.camera.bottom = -2000; dirLight.shadow.camera.left = -2000; dirLight.shadow.camera.right = 2000; dirLight.shadow.camera.near = 0.1; dirLight.shadow.camera.far = 5000;
scene.add( dirLight );
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 500, 0);
const ground = new THREE.Mesh(new THREE.PlaneGeometry(10000, 10000), new THREE.MeshLambertMaterial({ color: 0xbbbbbb }));
ground.rotation.x = -Math.PI / 2;
ground.position.y = 0;
ground.receiveShadow = true;
scene.add(ground);
const textureLoader = new THREE.TextureLoader();
// --- Данные материалов ---
const MATERIALS = [
{ name: 'Белый', type: 'color', value: 0xffffff },
{ name: 'Серый', type: 'color', value: 0x808080 },
{ name: 'Черный', type: 'color', value: 0x333333 },
{ name: 'Образец1', type: 'texture', value: 'assets/wood1.jpg' },
{ name: 'Образец2', type: 'texture', value: 'assets/wood2.jpg' },
];
let currentMaterial = new THREE.MeshStandardMaterial({ side: THREE.DoubleSide });
let currentMaterialData = MATERIALS[0];
// --- ИСПРАВЛЕНИЕ: Функция объявлена ОДИН раз ---
function updateMaterial(materialData) {
currentMaterialData = materialData;
if (materialData.type === 'color') {
currentMaterial.map = null;
currentMaterial.color.set(materialData.value);
currentMaterial.needsUpdate = true;
} else if (materialData.type === 'texture') {
textureLoader.load(materialData.value, (texture) => {
currentMaterial.map = texture;
currentMaterial.color.set(0xffffff);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
currentMaterial.needsUpdate = true;
});
}
}
// --- Логика выделения ячеек ---
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let currentlyIntersected = null;
const highlighterMaterial = new THREE.MeshBasicMaterial({ color: 0xff8c00, transparent: true, opacity: 0.4, side: THREE.DoubleSide });
const highlighterGeometry = new THREE.PlaneGeometry(1, 1);
const highlighterMesh = new THREE.Mesh(highlighterGeometry, highlighterMaterial);
highlighterMesh.visible = false;
scene.add(highlighterMesh);
function onPointerMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
window.addEventListener('pointermove', onPointerMove);
// --- Логика загрузки модели человека ---
let humanModel = null;
function loadHumanModel() {
const loader = new GLTFLoader();
const stencilMaterial = new THREE.MeshStandardMaterial({
color: 0x4682B4,
metalness: 0.1,
roughness: 0.8,
});
loader.load(
'assets/model.glb',
(gltf) => {
humanModel = gltf.scene;
humanModel.traverse((child) => {
if (child.isMesh) {
child.material = stencilMaterial;
child.castShadow = true;
child.receiveShadow = false;
}
});
humanModel.scale.set(590, 590, 590);
scene.add(humanModel);
updateConfiguration();
},
undefined,
(error) => { console.error('Ошибка при загрузке модели человека:', error); }
);
}
// --- Логика конструктора ---
const PANEL_THICKNESS = 16;
let wardrobeGroup = new THREE.Group();
let hitboxesGroup = new THREE.Group();
scene.add(wardrobeGroup);
scene.add(hitboxesGroup);
function createWardrobe(width, height, depth, columns, rows) {
const modelGroup = new THREE.Group();
const cellHitboxesGroup = new THREE.Group();
modelGroup.position.y = height / 2;
cellHitboxesGroup.position.y = height / 2;
const createPanel = (geometry) => {
const mesh = new THREE.Mesh(geometry, currentMaterial);
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
};
const innerWidth = width - 2 * PANEL_THICKNESS;
const innerHeight = height - 2 * PANEL_THICKNESS;
const shelfDepth = depth - PANEL_THICKNESS;
modelGroup.add(createPanel(new THREE.BoxGeometry(width, PANEL_THICKNESS, depth)).translateY(-height / 2 + PANEL_THICKNESS / 2));
modelGroup.add(createPanel(new THREE.BoxGeometry(width, PANEL_THICKNESS, depth)).translateY(height / 2 - PANEL_THICKNESS / 2));
modelGroup.add(createPanel(new THREE.BoxGeometry(PANEL_THICKNESS, innerHeight, depth)).translateX(-width / 2 + PANEL_THICKNESS / 2));
modelGroup.add(createPanel(new THREE.BoxGeometry(PANEL_THICKNESS, innerHeight, depth)).translateX(width / 2 - PANEL_THICKNESS / 2));
modelGroup.add(createPanel(new THREE.BoxGeometry(innerWidth, innerHeight, PANEL_THICKNESS)).translateZ(-depth / 2 + PANEL_THICKNESS / 2));
const numDividers = columns - 1;
const sectionWidth = (innerWidth - (numDividers * PANEL_THICKNESS)) / columns;
if (numDividers > 0) {
for (let i = 0; i < numDividers; i++) {
const posX = -innerWidth / 2 + sectionWidth * (i + 1) + PANEL_THICKNESS * (i + 0.5);
modelGroup.add(createPanel(new THREE.BoxGeometry(PANEL_THICKNESS, innerHeight, shelfDepth)).translateX(posX).translateZ(PANEL_THICKNESS/2));
}
}
const numShelves = rows - 1;
const cellHeight = (innerHeight - (numShelves * PANEL_THICKNESS)) / rows;
if (numShelves > 0) {
for (let c = 0; c < columns; c++) {
for (let r = 0; r < numShelves; r++) {
const posX = -innerWidth / 2 + sectionWidth * (c + 0.5) + PANEL_THICKNESS * c;
const posY = height / 2 - PANEL_THICKNESS - cellHeight * (r + 1) - PANEL_THICKNESS * r;
const shelf = createPanel(new THREE.BoxGeometry(sectionWidth, PANEL_THICKNESS, shelfDepth));
shelf.position.set(posX, posY, PANEL_THICKNESS / 2);
modelGroup.add(shelf);
}
}
}
const hitboxMaterial = new THREE.MeshBasicMaterial({visible: false});
for (let c = 0; c < columns; c++) {
for (let r = 0; r < rows; r++) {
const hitboxGeometry = new THREE.PlaneGeometry(sectionWidth, cellHeight);
const hitbox = new THREE.Mesh(hitboxGeometry, hitboxMaterial);
const cellCenterX = -innerWidth / 2 + sectionWidth * (c + 0.5) + PANEL_THICKNESS * c;
const cellCenterY = height / 2 - PANEL_THICKNESS / 2 - cellHeight * (r + 0.5) - PANEL_THICKNESS * r;
hitbox.position.set(cellCenterX, cellCenterY, depth / 2);
cellHitboxesGroup.add(hitbox);
}
}
return { model: modelGroup, hitboxes: cellHitboxesGroup };
}
// --- Инициализация UI и связь с 3D ---
(function init() {
const selector = document.getElementById('material-selector');
const priceDisplay = document.getElementById('price-display');
MATERIALS.forEach((materialData) => {
const swatch = document.createElement('div');
swatch.classList.add('material-swatch');
if (materialData.type === 'color') {
swatch.style.backgroundColor = `#${materialData.value.toString(16).padStart(6, '0')}`;
} else if (materialData.type === 'texture') {
swatch.style.backgroundImage = `url(${materialData.value})`;
}
swatch.addEventListener('click', () => {
updateMaterial(materialData);
selector.querySelector('.selected')?.classList.remove('selected');
swatch.classList.add('selected');
updateConfiguration();
});
selector.appendChild(swatch);
});
const uiControls = { width: document.getElementById('width-slider'), height: document.getElementById('height-slider'), depth: document.getElementById('depth-slider'), columns: document.getElementById('columns-input'), rows: document.getElementById('rows-input'), };
const uiValues = { width: document.getElementById('width-value'), height: document.getElementById('height-value'), depth: document.getElementById('depth-value'), };
window.updateConfiguration = async function() {
priceDisplay.textContent = 'Расчет...';
const params = {
width: parseFloat(uiControls.width.value),
height: parseFloat(uiControls.height.value),
depth: parseFloat(uiControls.depth.value),
columns: parseInt(uiControls.columns.value),
rows: parseInt(uiControls.rows.value),
materialName: currentMaterialData.name,
};
uiValues.width.textContent = params.width;
uiValues.height.textContent = params.height;
uiValues.depth.textContent = params.depth;
controls.target.set(0, params.height / 2, 0);
scene.remove(wardrobeGroup);
scene.remove(hitboxesGroup);
wardrobeGroup.traverse(child => { if (child.isMesh) child.geometry.dispose(); });
hitboxesGroup.traverse(child => { if (child.isMesh) child.geometry.dispose(); });
const result = createWardrobe(params.width, params.height, params.depth, params.columns, params.rows);
wardrobeGroup = result.model;
hitboxesGroup = result.hitboxes;
scene.add(wardrobeGroup);
scene.add(hitboxesGroup);
if (humanModel) {
const humanX = params.width / 2 + 550;
const humanY = 0;
const humanZ = params.depth / 2;
humanModel.position.set(humanX, humanY, humanZ);
}
try {
const response = await fetch('api/calculate-price', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await response.json();
if(response.ok) { priceDisplay.textContent = `${data.price.toLocaleString('ru-RU')}`; } else { console.error(data.error); priceDisplay.textContent = 'Ошибка'; }
} catch (error) { console.error('Ошибка сети при расчете цены:', error); priceDisplay.textContent = 'Ошибка сети'; }
}
Object.values(uiControls).forEach(control => control.addEventListener('input', window.updateConfiguration));
loadHumanModel();
selector.children[0].classList.add('selected');
updateMaterial(MATERIALS[0]);
})();
// --- Рендер-цикл ---
function animate() {
requestAnimationFrame(animate);
controls.update();
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(hitboxesGroup.children);
if (intersects.length > 0) {
const intersectedObject = intersects[0].object;
if (currentlyIntersected !== intersectedObject) {
currentlyIntersected = intersectedObject;
highlighterMesh.position.copy(intersectedObject.position);
highlighterMesh.quaternion.copy(intersectedObject.quaternion);
highlighterMesh.position.add(hitboxesGroup.position);
highlighterMesh.scale.set(
intersectedObject.geometry.parameters.width,
intersectedObject.geometry.parameters.height,
1
);
highlighterMesh.visible = true;
}
} else {
if (currentlyIntersected) {
highlighterMesh.visible = false;
currentlyIntersected = null;
}
}
renderer.render(scene, camera);
}
animate();
// --- Адаптивность ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});