first commit
This commit is contained in:
commit
af3c2bf481
73
furniture.js
Normal file
73
furniture.js
Normal file
@ -0,0 +1,73 @@
|
||||
// server.js
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
// --- ИЗМЕНЕНИЕ: Используйте порт 3201, как в вашем конфиге прокси ---
|
||||
const PORT = 3201;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Раздаем ТОЛЬКО нашу папку 'public'
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// --- УДАЛЕНО: Раздача папок build и jsm больше не нужна ---
|
||||
// app.use('/build', express.static(path.join(__dirname, 'node_modules/three/build')));
|
||||
// app.use('/jsm', express.static(path.join(__dirname, 'node_modules/three/examples/jsm')));
|
||||
|
||||
|
||||
// API эндпоинт для расчета цены (без изменений)
|
||||
const PRICE_CONFIG = {
|
||||
edgeBandingPerMeter: 50,
|
||||
baseWorkCost: 3000,
|
||||
};
|
||||
const MATERIALS_PRICES = {
|
||||
'Белый': 1500,
|
||||
'Серый': 1650,
|
||||
'Черный': 1800,
|
||||
'Образец1': 2200,
|
||||
'Образец2': 2500,
|
||||
};
|
||||
const PANEL_THICKNESS = 16;
|
||||
|
||||
app.post('/api/calculate-price', (req, res) => {
|
||||
const params = req.body;
|
||||
const pricePerM2 = MATERIALS_PRICES[params.materialName];
|
||||
if (!pricePerM2) {
|
||||
return res.status(400).json({ error: 'Invalid material name' });
|
||||
}
|
||||
const { width, height, depth, columns, rows } = params;
|
||||
const M_IN_MM = 1000;
|
||||
let totalAreaM2 = 0;
|
||||
let totalEdgeLengthM = 0;
|
||||
const innerWidth = width - 2 * PANEL_THICKNESS;
|
||||
const innerHeight = height - 2 * PANEL_THICKNESS;
|
||||
const shelfDepth = depth - PANEL_THICKNESS;
|
||||
totalAreaM2 += (2 * height * depth) / (M_IN_MM * M_IN_MM);
|
||||
totalAreaM2 += (2 * width * depth) / (M_IN_MM * M_IN_MM);
|
||||
totalEdgeLengthM += (2 * height + 2 * width) / M_IN_MM;
|
||||
totalAreaM2 += (innerWidth * innerHeight) / (M_IN_MM * M_IN_MM);
|
||||
const numDividers = columns - 1;
|
||||
if (numDividers > 0) {
|
||||
totalAreaM2 += (numDividers * innerHeight * shelfDepth) / (M_IN_MM * M_IN_MM);
|
||||
totalEdgeLengthM += (numDividers * innerHeight) / M_IN_MM;
|
||||
}
|
||||
const numShelves = rows - 1;
|
||||
if (numShelves > 0) {
|
||||
const sectionWidth = (innerWidth - (numDividers * PANEL_THICKNESS)) / columns;
|
||||
const totalShelfArea = (columns * numShelves * sectionWidth * shelfDepth) / (M_IN_MM * M_IN_MM);
|
||||
totalAreaM2 += totalShelfArea;
|
||||
const totalShelfEdgeLength = (columns * numShelves * sectionWidth) / M_IN_MM;
|
||||
totalEdgeLengthM += totalShelfEdgeLength;
|
||||
}
|
||||
const materialCost = totalAreaM2 * pricePerM2;
|
||||
const edgeBandingCost = totalEdgeLengthM * PRICE_CONFIG.edgeBandingPerMeter;
|
||||
const finalPrice = PRICE_CONFIG.baseWorkCost + materialCost + edgeBandingCost;
|
||||
res.json({ price: Math.round(finalPrice) });
|
||||
});
|
||||
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Сервер конструктора мебели запущен на порту: ${PORT}`);
|
||||
});
|
1277
package-lock.json
generated
Normal file
1277
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
Normal file
16
package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "3deditor_ai",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0"
|
||||
}
|
||||
}
|
BIN
public/assets/model.glb
Normal file
BIN
public/assets/model.glb
Normal file
Binary file not shown.
BIN
public/assets/wood1.jpg
Normal file
BIN
public/assets/wood1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
public/assets/wood2.jpg
Normal file
BIN
public/assets/wood2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
59
public/css/style.css
Normal file
59
public/css/style.css
Normal file
@ -0,0 +1,59 @@
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#canvas-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
body { margin: 0; font-family: sans-serif; }
|
||||
canvas { display: block; }
|
||||
#ui-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 10px;
|
||||
width: 320px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
z-index: 10; /* Ставим панель поверх 3D сцены */
|
||||
}
|
||||
.control-group { margin-bottom: 20px; border-bottom: 1px solid #e0e0e0; padding-bottom: 20px; }
|
||||
.control-group:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
|
||||
.control-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.control-row label { font-weight: bold; }
|
||||
.control-row input[type="range"] { width: 160px; }
|
||||
.control-row input[type="number"] { width: 60px; text-align: center; }
|
||||
.control-row span { min-width: 40px; text-align: right; }
|
||||
h3, h4 { margin-top: 0; margin-bottom: 15px; text-align: center; color: #333; }
|
||||
#material-selector { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
||||
.material-swatch { width: 40px; height: 40px; border-radius: 50%; border: 2px solid #ccc; cursor: pointer; transition: transform 0.2s, border-color 0.2s; background-size: cover; }
|
||||
.material-swatch:hover { transform: scale(1.1); }
|
||||
.material-swatch.selected { border-color: #007bff; border-width: 3px; }
|
||||
#price-container {
|
||||
text-align: center;
|
||||
padding-top: 15px;
|
||||
}
|
||||
#price-container .price-label {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#price-container .price-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #d9534f;
|
||||
}
|
68
public/index.html
Normal file
68
public/index.html
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D Конструктор Шкафа</title>
|
||||
<!-- Путь к CSS оставляем относительным -->
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- UI панель без изменений -->
|
||||
<div id="ui-panel">
|
||||
<div class="control-group">
|
||||
<h3>Габариты (мм)</h3>
|
||||
<div class="control-row">
|
||||
<label for="width-slider">Ширина</label>
|
||||
<input type="range" id="width-slider" min="600" max="2400" value="1200" step="10">
|
||||
<span id="width-value">1200</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<label for="height-slider">Высота</label>
|
||||
<input type="range" id="height-slider" min="800" max="2700" value="2000" step="10">
|
||||
<span id="height-value">2000</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<label for="depth-slider">Глубина</label>
|
||||
<input type="range" id="depth-slider" min="300" max="800" value="500" step="10">
|
||||
<span id="depth-value">500</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h4>Наполнение (ячейки)</h4>
|
||||
<div class="control-row">
|
||||
<label for="columns-input">Колонки</label>
|
||||
<input type="number" id="columns-input" min="1" max="10" value="3">
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<label for="rows-input">Ряды</label>
|
||||
<input type="number" id="rows-input" min="1" max="10" value="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h4>Материал корпуса</h4>
|
||||
<div id="material-selector"></div>
|
||||
</div>
|
||||
<div id="price-container">
|
||||
<div class="price-label">Итоговая стоимость</div>
|
||||
<div class="price-value" id="price-display">0 ₽</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
### ИЗМЕНЕНИЕ: Возвращаем importmap с абсолютными ссылками на внешний CDN ###
|
||||
-->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://unpkg.com/three@0.165.0/build/three.module.js",
|
||||
"three/addons/": "https://unpkg.com/three@0.165.0/examples/jsm/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Путь к main.js оставляем относительным -->
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
284
public/js/main.js
Normal file
284
public/js/main.js
Normal file
@ -0,0 +1,284 @@
|
||||
// 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);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user