284 lines
13 KiB
JavaScript
284 lines
13 KiB
JavaScript
// 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);
|
||
}); |