Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de36479da1 | ||
|
|
5664e99554 | ||
|
|
9ff2c76912 |
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine as build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
86
README.md
86
README.md
@@ -1,20 +1,80 @@
|
|||||||
<div align="center">
|
# PrintFit - Генератор Органайзеров для 3D Печати
|
||||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
# Run and deploy your AI Studio app
|
PrintFit — это мощный веб-инструмент для создания параметрических органайзеров, разделителей и ящиков, оптимизированных для 3D-печати. Приложение позволяет легко проектировать системы хранения под любые размеры с возможностью детальной настройки перегородок и перфорации.
|
||||||
|
|
||||||
This contains everything you need to run your app locally.
|

|
||||||
|
|
||||||
View your app in AI Studio: https://ai.studio/apps/drive/1gUfO_tgl_CPfAGf1eXuSFyC0K7nUZFgu
|
## ✨ Основные Возможности
|
||||||
|
|
||||||
## Run Locally
|
### 🛠 Параметрическое Моделирование
|
||||||
|
- **Полный контроль размеров**: Настраивайте ширину, глубину и высоту ящика с точностью до миллиметра.
|
||||||
|
- **Толщина стенок и допуски**: Установка толщины внешних стенок и внутренних перегородок, а также компенсация усадки пластика (tolerance) для идеальной совместимости.
|
||||||
|
- **Скругление углов**: Настройка радиуса углов для эстетики и совместимости с различными стандартами (например, Gridfinity).
|
||||||
|
|
||||||
**Prerequisites:** Node.js
|
### 📐 Гибкая Система Перегородок
|
||||||
|
- **Визуальный редактор макета**: Интуитивно понятный интерфейс для добавления вертикальных и горизонтальных разделителей.
|
||||||
|
- **Вложенность**: Создание сложных сеток и ячеек разных размеров внутри одного органайзера.
|
||||||
|
- **Умное отображение размеров**: Автоматический расчет и отображение внутренних габаритов каждого отсека прямо на 3D-модели.
|
||||||
|
|
||||||
|
### 💨 Продвинутая Перфорация
|
||||||
|
- **Экономия материала и времени**: Возможность создания перфорированных стенок.
|
||||||
|
- **Различные узоры**: Выбор формы отверстий (соты/гексагоны, круги, треугольники и др.).
|
||||||
|
- **Умные исключения**: Алгоритм автоматически убирает перфорацию в местах стыков перегородок и углов, обеспечивая прочность конструкции (Solid Zones).
|
||||||
|
- **Настройка плотности**: Регулировка размера отверстий и отступов между ними.
|
||||||
|
|
||||||
1. Install dependencies:
|
### 🖥 Интерактивный 3D Предпросмотр
|
||||||
`npm install`
|
- **Real-time рендеринг**: Мгновенное отображение изменений в браузере с помощью Three.js.
|
||||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
- **Управление камерой**: Вращение, масштабирование и панорамирование для детального осмотра модели со всех сторон.
|
||||||
3. Run the app:
|
- **Индикация размеров**: Внутренние размеры отсеков отображаются прямо внутри ячеек.
|
||||||
`npm run dev`
|
|
||||||
|
### 💾 Экспорт и Шеринг
|
||||||
|
- **STL Экспорт**: Генерация готовых к печати STL файлов. Можно скачать отдельные детали или весь проект архивом (ZIP).
|
||||||
|
- **Сохранение проектов**: Возможность поделиться ссылкой на конфигурацию (все параметры кодируются в URL).
|
||||||
|
|
||||||
|
## 🚀 Запуск Локально
|
||||||
|
|
||||||
|
Для работы требуется установленный **Node.js**.
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-username/printfit-box-generator.git
|
||||||
|
cd printfit-box-generator
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Установите зависимости:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Запустите локальный сервер разработки:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Откройте приложение в браузере (обычно http://localhost:5173).
|
||||||
|
|
||||||
|
## 🐳 Запуск в Docker
|
||||||
|
|
||||||
|
Приложение можно легко развернуть в контейнере Docker (используется Nginx для раздачи статики).
|
||||||
|
|
||||||
|
1. Соберите образ:
|
||||||
|
```bash
|
||||||
|
docker build -t printfit-app .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Запустите контейнер:
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:80 printfit-app
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Откройте http://localhost:8080
|
||||||
|
|
||||||
|
## 🛠 Технологический Стек
|
||||||
|
- **React**: UI и управление состоянием.
|
||||||
|
- **Three.js (@react-three/fiber)**: 3D рендеринг и геометрия.
|
||||||
|
- **Tailwind CSS**: Стилизация интерфейса.
|
||||||
|
- **Vite**: Сборщик проекта.
|
||||||
|
- **Lucide React**: Иконки.
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
Этот проект распространяется под лицензией MIT. Вы можете свободно использовать, изменять и распространять его.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const App = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col font-sans text-gray-100 bg-slate-950 overflow-hidden">
|
<div className="h-screen flex flex-col font-sans text-gray-100 bg-slate-950 overflow-hidden">
|
||||||
{/* Header */}
|
{/* Шапка */}
|
||||||
<header className="bg-slate-900 border-b border-slate-800 p-4 shadow-md shrink-0 z-50">
|
<header className="bg-slate-900 border-b border-slate-800 p-4 shadow-md shrink-0 z-50">
|
||||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -93,7 +93,7 @@ const App = () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Основной контент */}
|
||||||
<main className="flex-1 w-full max-w-7xl mx-auto p-4 overflow-hidden flex flex-col min-h-0">
|
<main className="flex-1 w-full max-w-7xl mx-auto p-4 overflow-hidden flex flex-col min-h-0">
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="h-full overflow-y-auto animate-fade-in custom-scrollbar">
|
<div className="h-full overflow-y-auto animate-fade-in custom-scrollbar">
|
||||||
@@ -116,7 +116,7 @@ const App = () => {
|
|||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Подвал */}
|
||||||
<footer className="bg-slate-900 border-t border-slate-800 p-4 shrink-0 z-50">
|
<footer className="bg-slate-900 border-t border-slate-800 p-4 shrink-0 z-50">
|
||||||
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
<button disabled={step === 1} onClick={() => setStep(s => Math.max(1, s - 1))} className="flex items-center gap-2 px-6 py-3 rounded-lg font-semibold bg-slate-800 text-white disabled:opacity-50 disabled:cursor-not-allowed hover:bg-slate-700 transition-colors"><ChevronLeft size={18} /> Назад</button>
|
<button disabled={step === 1} onClick={() => setStep(s => Math.max(1, s - 1))} className="flex items-center gap-2 px-6 py-3 rounded-lg font-semibold bg-slate-800 text-white disabled:opacity-50 disabled:cursor-not-allowed hover:bg-slate-700 transition-colors"><ChevronLeft size={18} /> Назад</button>
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row h-full gap-6 relative flex-1 min-h-0">
|
<div className="flex flex-col lg:flex-row h-full gap-6 relative flex-1 min-h-0">
|
||||||
{/* 3D Viewer */}
|
{/* 3D Просмотр */}
|
||||||
<div className="flex-1 bg-slate-900 rounded-xl overflow-hidden shadow-2xl border border-slate-800 relative min-h-[400px]">
|
<div className="flex-1 bg-slate-900 rounded-xl overflow-hidden shadow-2xl border border-slate-800 relative min-h-[400px]">
|
||||||
<div className="absolute top-4 right-4 z-10 bg-black/60 p-3 rounded-lg text-xs text-gray-300 backdrop-blur pointer-events-none border border-slate-700">
|
<div className="absolute top-4 right-4 z-10 bg-black/60 p-3 rounded-lg text-xs text-gray-300 backdrop-blur pointer-events-none border border-slate-700">
|
||||||
<div className="flex items-center gap-2 mb-1 text-primary font-bold">
|
<div className="flex items-center gap-2 mb-1 text-primary font-bold">
|
||||||
@@ -387,7 +387,7 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
|||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar List (Grid Layout) */}
|
{/* Боковая панель (Сетка) */}
|
||||||
<div className="w-full lg:w-96 bg-slate-900 p-6 rounded-xl border border-slate-800 flex flex-col h-full shadow-xl">
|
<div className="w-full lg:w-96 bg-slate-900 p-6 rounded-xl border border-slate-800 flex flex-col h-full shadow-xl">
|
||||||
<div className="flex justify-between items-center mb-6 shrink-0">
|
<div className="flex justify-between items-center mb-6 shrink-0">
|
||||||
<h2 className="text-xl font-bold flex items-center gap-2 text-primary">
|
<h2 className="text-xl font-bold flex items-center gap-2 text-primary">
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const createConcaveFilletShape = (radius: number): THREE.Shape => {
|
|||||||
return shape;
|
return shape;
|
||||||
};
|
};
|
||||||
|
|
||||||
// New Helper: Create Perforated Plate (Vertical Wall)
|
// Вспомогательная функция: Создание перфорированной пластины (Вертикальная стенка)
|
||||||
const createPerforatedPlate = (
|
const createPerforatedPlate = (
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
@@ -100,17 +100,17 @@ const createPerforatedPlate = (
|
|||||||
shape.lineTo(0, height);
|
shape.lineTo(0, height);
|
||||||
shape.lineTo(0, 0);
|
shape.lineTo(0, 0);
|
||||||
|
|
||||||
// Hole Generation
|
// Генерация отверстий
|
||||||
if (perf && perf.enabled && width > perf.size && height > perf.size) {
|
if (perf && perf.enabled && width > perf.size && height > perf.size) {
|
||||||
const { shape: shapeType, size, gap } = perf;
|
const { shape: shapeType, size, gap } = perf;
|
||||||
const step = size + gap;
|
const step = size + gap;
|
||||||
// Margins: ensure holes don't cut into the solid edge zones
|
// Отступы: предотвращаем попадание отверстий на сплошные края
|
||||||
const startX = Math.max(gap, marginLeft + gap);
|
const startX = Math.max(gap, marginLeft + gap);
|
||||||
const startY = gap;
|
const startY = gap;
|
||||||
const endX = width - Math.max(gap, marginRight + gap);
|
const endX = width - Math.max(gap, marginRight + gap);
|
||||||
const endY = height - gap;
|
const endY = height - gap;
|
||||||
|
|
||||||
// Rows
|
// Строки
|
||||||
let row = 0;
|
let row = 0;
|
||||||
for (let y = startY + size / 2; y < endY; y += (shapeType === 'triangle' || shapeType === 'honeycomb' ? step * 0.866 : step)) {
|
for (let y = startY + size / 2; y < endY; y += (shapeType === 'triangle' || shapeType === 'honeycomb' ? step * 0.866 : step)) {
|
||||||
const isStaggered = (row % 2 !== 0);
|
const isStaggered = (row % 2 !== 0);
|
||||||
@@ -120,18 +120,18 @@ const createPerforatedPlate = (
|
|||||||
const hole = new THREE.Path();
|
const hole = new THREE.Path();
|
||||||
const r = size / 2;
|
const r = size / 2;
|
||||||
|
|
||||||
// Boundary check
|
// Проверка границ
|
||||||
if (x - r < marginLeft || x + r > width - marginRight || y - r < 0 || y + r > height) continue;
|
if (x - r < marginLeft || x + r > width - marginRight || y - r < 0 || y + r > height) continue;
|
||||||
|
|
||||||
// Exclusion Zone Check
|
// Проверка зон исключения
|
||||||
// Zone active if hole X is within [start, end] AND hole Y is below yMax (if specified).
|
// Зона активна, если X отверстия входит в [start, end] И Y отверстия ниже yMax (если указан).
|
||||||
// If y > yMax, the exclusion doesn't apply (it's above the intersecting wall).
|
// Если y > yMax, исключение не применяется (отверстие выше пересекающей стенки).
|
||||||
// Note: y is measured from bottom (0) to top (height).
|
// Примечание: y измеряется от низа (0) до верха (height).
|
||||||
// yMax is the height of the intersecting partition.
|
// yMax - высота пересекающей перегородки.
|
||||||
|
|
||||||
const inExclusion = exclusions.some(zone => {
|
const inExclusion = exclusions.some(zone => {
|
||||||
if (x + r <= zone.start || x - r >= zone.end) return false; // X-axis check
|
if (x + r <= zone.start || x - r >= zone.end) return false; // Проверка по оси X
|
||||||
if (zone.yMax !== undefined && y - r > zone.yMax) return false; // Y-axis check (hole above wall)
|
if (zone.yMax !== undefined && y - r > zone.yMax) return false; // Проверка по оси Y (отверстие выше стенки)
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ const createPerforatedPlate = (
|
|||||||
if (shapeType === 'circle') {
|
if (shapeType === 'circle') {
|
||||||
hole.absarc(x, y, r, 0, Math.PI * 2, true);
|
hole.absarc(x, y, r, 0, Math.PI * 2, true);
|
||||||
} else if (shapeType === 'honeycomb') {
|
} else if (shapeType === 'honeycomb') {
|
||||||
// Hexagon
|
// Шестиугольник (Соты)
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const ang = (i * 60 + 30) * Math.PI / 180;
|
const ang = (i * 60 + 30) * Math.PI / 180;
|
||||||
const px = x + r * Math.cos(ang);
|
const px = x + r * Math.cos(ang);
|
||||||
@@ -150,7 +150,7 @@ const createPerforatedPlate = (
|
|||||||
}
|
}
|
||||||
hole.closePath();
|
hole.closePath();
|
||||||
} else if (shapeType === 'triangle') {
|
} else if (shapeType === 'triangle') {
|
||||||
// Triangle
|
// Треугольник
|
||||||
const ang1 = -90 * Math.PI / 180;
|
const ang1 = -90 * Math.PI / 180;
|
||||||
const ang2 = 30 * Math.PI / 180;
|
const ang2 = 30 * Math.PI / 180;
|
||||||
const ang3 = 150 * Math.PI / 180;
|
const ang3 = 150 * Math.PI / 180;
|
||||||
@@ -166,37 +166,37 @@ const createPerforatedPlate = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
|
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
|
||||||
// Extruded along Z. Wall is flat on XY.
|
// Выдавливание по Z. Стенка плоская в XY.
|
||||||
// We want "thickness" to be Z depth.
|
// Мы хотим, чтобы "thickness" была глубиной по Z.
|
||||||
return geo;
|
return geo;
|
||||||
};
|
};
|
||||||
|
|
||||||
// New Helper: Create Corner Profile (Extruded Vertical)
|
// Вспомогательная функция: Создание профиля угла (Выдавленный вертикально)
|
||||||
const createCornerProfile = (radius: number, thickness: number, height: number): THREE.BufferGeometry => {
|
const createCornerProfile = (radius: number, thickness: number, height: number): THREE.BufferGeometry => {
|
||||||
if (radius <= 0) return new THREE.BufferGeometry();
|
if (radius <= 0) return new THREE.BufferGeometry();
|
||||||
const shape = new THREE.Shape();
|
const shape = new THREE.Shape();
|
||||||
|
|
||||||
// Create a Ring Segment (Hollow Corner) as a single loop.
|
// Создаем сегмент кольца (Полый угол) как единый контур.
|
||||||
// Center at (0,0).
|
// Центр в (0,0).
|
||||||
const innerRadius = Math.max(0.01, radius - thickness); // Ensure slightly > 0 to maintain shape integrity
|
const innerRadius = Math.max(0.01, radius - thickness); // Гарантируем чуть > 0 для целостности формы
|
||||||
|
|
||||||
// 1. Start at Outer Start
|
// 1. Начало внешней дуги
|
||||||
shape.moveTo(radius, 0);
|
shape.moveTo(radius, 0);
|
||||||
|
|
||||||
// 2. Outer Arc (CCW) -> To (0, radius)
|
// 2. Внешняя дуга (против часовой стрелки) -> К (0, radius)
|
||||||
shape.absarc(0, 0, radius, 0, Math.PI / 2, false);
|
shape.absarc(0, 0, radius, 0, Math.PI / 2, false);
|
||||||
|
|
||||||
// 3. Line to Inner End (0, innerRadius)
|
// 3. Линия к внутреннему концу (0, innerRadius)
|
||||||
shape.lineTo(0, innerRadius);
|
shape.lineTo(0, innerRadius);
|
||||||
|
|
||||||
// 4. Inner Arc (CW) -> To (innerRadius, 0)
|
// 4. Внутренняя дуга (по часовой стрелке) -> К (innerRadius, 0)
|
||||||
shape.absarc(0, 0, innerRadius, Math.PI / 2, 0, true);
|
shape.absarc(0, 0, innerRadius, Math.PI / 2, 0, true);
|
||||||
|
|
||||||
// 5. Close loop
|
// 5. Замыкаем контур
|
||||||
shape.lineTo(radius, 0);
|
shape.lineTo(radius, 0);
|
||||||
|
|
||||||
// Extrude
|
// Выдавливание
|
||||||
// curveSegments 32 for smoothness
|
// curveSegments 32 для гладкости
|
||||||
const geo = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: false, curveSegments: 32 });
|
const geo = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: false, curveSegments: 32 });
|
||||||
return geo;
|
return geo;
|
||||||
};
|
};
|
||||||
@@ -218,9 +218,9 @@ export const createBinGeometry = (
|
|||||||
floorGeo.rotateX(-Math.PI / 2); // Lay flat
|
floorGeo.rotateX(-Math.PI / 2); // Lay flat
|
||||||
geometries.push(floorGeo);
|
geometries.push(floorGeo);
|
||||||
|
|
||||||
// WALLS
|
// СТЕНКИ
|
||||||
if (!perforation || !perforation.enabled) {
|
if (!perforation || !perforation.enabled) {
|
||||||
// --- ORIGINAL LOGIC (Optimized for Solid Walls) ---
|
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ СТЕН (Без перфорации) ---
|
||||||
const outerShape = createRoundedRectShape(width, depth, radius);
|
const outerShape = createRoundedRectShape(width, depth, radius);
|
||||||
const innerRadius = Math.max(0.1, radius - thickness);
|
const innerRadius = Math.max(0.1, radius - thickness);
|
||||||
const innerWidth = width - (2 * thickness);
|
const innerWidth = width - (2 * thickness);
|
||||||
@@ -237,78 +237,78 @@ export const createBinGeometry = (
|
|||||||
wallGeo.translate(0, thickness, 0);
|
wallGeo.translate(0, thickness, 0);
|
||||||
geometries.push(wallGeo);
|
geometries.push(wallGeo);
|
||||||
} else {
|
} else {
|
||||||
// --- PERFORATED LOGIC (Split Walls) ---
|
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ СТЕН (Раздельные части) ---
|
||||||
const wallHeight = height - thickness;
|
const wallHeight = height - thickness;
|
||||||
// Clamp radius to at least thickness for valid corners in this mode
|
// Ограничиваем радиус минимум толщиной стенки для корректных углов
|
||||||
const effRadius = Math.max(radius, thickness);
|
const effRadius = Math.max(radius, thickness);
|
||||||
|
|
||||||
const straightW = width - 2 * effRadius;
|
const straightW = width - 2 * effRadius;
|
||||||
const straightD = depth - 2 * effRadius;
|
const straightD = depth - 2 * effRadius;
|
||||||
|
|
||||||
// 1. Corners (4 pcs)
|
// 1. Углы (4 шт)
|
||||||
if (effRadius > 0) {
|
if (effRadius > 0) {
|
||||||
const cornerGeoBase = createCornerProfile(effRadius, thickness, wallHeight);
|
const cornerGeoBase = createCornerProfile(effRadius, thickness, wallHeight);
|
||||||
|
|
||||||
// 1. Stand Up: Extrusion Z -> Y. Shape moves to X(+)/Z(+).
|
// 1. Поднимаем: Выдавливание Z -> Y. Форма перемещается в X(+)/Z(+).
|
||||||
cornerGeoBase.rotateX(-Math.PI / 2);
|
cornerGeoBase.rotateX(-Math.PI / 2);
|
||||||
|
|
||||||
// Base Corner Shape (after rotateX) is Q4 (+X, -Z).
|
// Базовая форма угла (после rotateX) это Q4 (+X, -Z).
|
||||||
// Rotation Logic: -90 degrees per Quadrant (Standard Three.js Y-Rot).
|
// Логика вращения: -90 градусов на квадрант (Стандартный Y-Rot в Three.js).
|
||||||
// Q4 (0) -> Q1 (-90) -> Q2 (-180/180) -> Q3 (-270/+90).
|
// Q4 (0) -> Q1 (-90) -> Q2 (-180/180) -> Q3 (-270/+90).
|
||||||
|
|
||||||
const positions = [
|
const positions = [
|
||||||
// Back Right (+X, +Z). Q1.
|
// Задний Правый (+X, +Z). Q1.
|
||||||
// Rot -90 (-PI/2).
|
// Поворот -90 (-PI/2).
|
||||||
{ x: width / 2 - effRadius, z: depth / 2 - effRadius, rot: -Math.PI / 2 },
|
{ x: width / 2 - effRadius, z: depth / 2 - effRadius, rot: -Math.PI / 2 },
|
||||||
|
|
||||||
// Back Left (-X, +Z). Q2.
|
// Задний Левый (-X, +Z). Q2.
|
||||||
// Rot 180 (PI).
|
// Поворот 180 (PI).
|
||||||
{ x: -(width / 2 - effRadius), z: depth / 2 - effRadius, rot: Math.PI },
|
{ x: -(width / 2 - effRadius), z: depth / 2 - effRadius, rot: Math.PI },
|
||||||
|
|
||||||
// Front Left (-X, -Z). Q3.
|
// Передний Левый (-X, -Z). Q3.
|
||||||
// Rot 90 (PI/2). (Equivalent to -270).
|
// Поворот 90 (PI/2). (Эквивалентно -270).
|
||||||
{ x: -(width / 2 - effRadius), z: -(depth / 2 - effRadius), rot: Math.PI / 2 },
|
{ x: -(width / 2 - effRadius), z: -(depth / 2 - effRadius), rot: Math.PI / 2 },
|
||||||
|
|
||||||
// Front Right (+X, -Z). Q4.
|
// Передний Правый (+X, -Z). Q4.
|
||||||
// Rot 0.
|
// Поворот 0.
|
||||||
{ x: width / 2 - effRadius, z: -(depth / 2 - effRadius), rot: 0 }
|
{ x: width / 2 - effRadius, z: -(depth / 2 - effRadius), rot: 0 }
|
||||||
];
|
];
|
||||||
|
|
||||||
positions.forEach(pos => {
|
positions.forEach(pos => {
|
||||||
const corner = cornerGeoBase.clone();
|
const corner = cornerGeoBase.clone();
|
||||||
corner.rotateY(pos.rot);
|
corner.rotateY(pos.rot);
|
||||||
corner.translate(pos.x, 0, pos.z); // Start at Y=0
|
corner.translate(pos.x, 0, pos.z); // Старт на Y=0
|
||||||
corner.translate(0, thickness, 0); // Move on top of floor
|
corner.translate(0, thickness, 0); // Сдвиг на толщину дна
|
||||||
geometries.push(corner);
|
geometries.push(corner);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Straight Walls (4 pcs) - Centered on edges
|
// 2. Прямые стены (4 шт) - Центрированы по краям
|
||||||
|
|
||||||
// REWRITE EXCLUSION COLLECTION LOGIC TO BE WALL-SPECIFIC
|
// ЛОГИКА СБОРА ИСКЛЮЧЕНИЙ ДЛЯ СТЕН
|
||||||
const exclusionsBack: { start: number, end: number, yMax: number }[] = [];
|
const exclusionsBack: { start: number, end: number, yMax: number }[] = [];
|
||||||
const exclusionsFront: { start: number, end: number, yMax: number }[] = [];
|
const exclusionsFront: { start: number, end: number, yMax: number }[] = [];
|
||||||
const exclusionsLeft: { start: number, end: number, yMax: number }[] = [];
|
const exclusionsLeft: { start: number, end: number, yMax: number }[] = [];
|
||||||
const exclusionsRight: { start: number, end: number, yMax: number }[] = [];
|
const exclusionsRight: { start: number, end: number, yMax: number }[] = [];
|
||||||
|
|
||||||
// Inner Dimensions
|
// Внутренние размеры
|
||||||
const effectiveInnerW = width - 2 * thickness;
|
const effectiveInnerW = width - 2 * thickness;
|
||||||
const effectiveInnerD = depth - 2 * thickness;
|
const effectiveInnerD = depth - 2 * thickness;
|
||||||
|
|
||||||
partitions.forEach(p => {
|
partitions.forEach(p => {
|
||||||
const hEff = Math.max(0.1, p.height - thickness); // Exclude only up to partition height
|
const hEff = Math.max(0.1, p.height - thickness); // Исключаем только до высоты перегородки
|
||||||
// Ensure solid strip around partition by adding padding to exclusion zone
|
// Гарантируем сплошную полосу вокруг перегородки добавлением отступа к зоне исключения
|
||||||
const padding = (perforation?.gap ?? 2) + 1;
|
const padding = (perforation?.gap ?? 2) + 1;
|
||||||
|
|
||||||
if (p.axis === 'x') {
|
if (p.axis === 'x') {
|
||||||
// Runs Depth-wise (Y axis in Layout).
|
// Идет по глубине (Ось Y в макете).
|
||||||
// Intersects Front and Back walls.
|
// Пересекает Переднюю и Заднюю стенки.
|
||||||
|
|
||||||
const partGlobalX = (-effectiveInnerW / 2) + (effectiveInnerW * p.offset);
|
const partGlobalX = (-effectiveInnerW / 2) + (effectiveInnerW * p.offset);
|
||||||
const sW = width - 2 * effRadius;
|
const sW = width - 2 * effRadius;
|
||||||
const localXOnWall = partGlobalX + sW / 2;
|
const localXOnWall = partGlobalX + sW / 2;
|
||||||
|
|
||||||
// Check intersection with active straight wall area + padding
|
// Проверка пересечения с активной зоной стены + отступ
|
||||||
if (localXOnWall + thickness / 2 + padding > 0 && localXOnWall - thickness / 2 - padding < sW) {
|
if (localXOnWall + thickness / 2 + padding > 0 && localXOnWall - thickness / 2 - padding < sW) {
|
||||||
const zone = {
|
const zone = {
|
||||||
start: localXOnWall - thickness / 2 - padding,
|
start: localXOnWall - thickness / 2 - padding,
|
||||||
@@ -316,13 +316,13 @@ export const createBinGeometry = (
|
|||||||
yMax: hEff
|
yMax: hEff
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((p.max ?? 1) > 0.99) exclusionsFront.push(zone); // Near
|
if ((p.max ?? 1) > 0.99) exclusionsFront.push(zone); // Ближняя
|
||||||
if ((p.min ?? 0) < 0.01) exclusionsBack.push(zone); // Far
|
if ((p.min ?? 0) < 0.01) exclusionsBack.push(zone); // Дальняя
|
||||||
}
|
}
|
||||||
|
|
||||||
} else { // p.axis === 'y'
|
} else { // p.axis === 'y'
|
||||||
// Runs Width-wise (X axis in Layout).
|
// Идет по ширине (Ось X в макете).
|
||||||
// Intersects Left and Right walls.
|
// Пересекает Левую и Правую стенки.
|
||||||
|
|
||||||
const partGlobalZ = (-effectiveInnerD / 2) + (effectiveInnerD * p.offset);
|
const partGlobalZ = (-effectiveInnerD / 2) + (effectiveInnerD * p.offset);
|
||||||
const sD = depth - 2 * effRadius;
|
const sD = depth - 2 * effRadius;
|
||||||
@@ -345,21 +345,21 @@ export const createBinGeometry = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Front/Back
|
// Передняя/Задняя
|
||||||
if (straightW > 0.1) {
|
if (straightW > 0.1) {
|
||||||
// Wall 1 (at +Z). This is "Front" (Near). Contacts p.max.
|
// Стенка 1 (+Z). Это "Передняя" (Ближняя). Контактирует с p.max.
|
||||||
// Uses exclusionsFront.
|
// Использует exclusionsFront.
|
||||||
const wGeoFront = createPerforatedPlate(straightW, wallHeight, thickness, perforation, 0, 0, exclusionsFront);
|
const wGeoFront = createPerforatedPlate(straightW, wallHeight, thickness, perforation, 0, 0, exclusionsFront);
|
||||||
wGeoFront.translate(-straightW / 2, 0, 0);
|
wGeoFront.translate(-straightW / 2, 0, 0);
|
||||||
const w1 = wGeoFront.clone();
|
const w1 = wGeoFront.clone();
|
||||||
w1.translate(0, thickness, depth / 2 - thickness);
|
w1.translate(0, thickness, depth / 2 - thickness);
|
||||||
geometries.push(w1);
|
geometries.push(w1);
|
||||||
|
|
||||||
// Wall 2 (at -Z). This is "Back" (Far). Contacts p.min.
|
// Стенка 2 (-Z). Это "Задняя" (Дальняя). Контактирует с p.min.
|
||||||
// Uses exclusionsBack.
|
// Использует exclusionsBack.
|
||||||
// Needs checking mapping (Rot 180).
|
// Нужна проверка маппинга (Поворот 180).
|
||||||
// p.offset increases X. Wall rotated 180 means X is inverted.
|
// p.offset увеличивает X. Стенка повернута на 180, значит X инвертирован.
|
||||||
// So we map exclusionsBack.
|
// Маппим exclusionsBack.
|
||||||
const exclusionsBackMapped = exclusionsBack.map(e => ({
|
const exclusionsBackMapped = exclusionsBack.map(e => ({
|
||||||
start: straightW - e.end,
|
start: straightW - e.end,
|
||||||
end: straightW - e.start,
|
end: straightW - e.start,
|
||||||
@@ -374,27 +374,23 @@ export const createBinGeometry = (
|
|||||||
geometries.push(w2);
|
geometries.push(w2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left/Right
|
// Левая/Правая
|
||||||
if (straightD > 0.1) {
|
if (straightD > 0.1) {
|
||||||
// Wall 3 (Right? +X).
|
// Стенка 3 (Правая +X).
|
||||||
// Plate is 0..straightD in X, 0..wallHeight in Y. Thickness along Z.
|
// Пластина 0..straightD по X, 0..wallHeight по Y. Толщина по Z.
|
||||||
// We want it to be at X = width/2.
|
// Хотим разместить на X = width/2.
|
||||||
// It needs to be rotated -PI/2 around Y to align its X-axis with World Z-axis.
|
// Нужно повернуть -PI/2 вокруг Y чтобы выровнять ось X пластины с мировой осью Z.
|
||||||
// After rotateY(-PI/2): Plate X (0..straightD) becomes World Z (0..straightD).
|
|
||||||
// Plate Z (thickness) becomes World -X (towards Left).
|
|
||||||
// So if we place it at X=width/2, it extrudes to width/2 - thickness. Correct for Right Wall.
|
|
||||||
const wGeoRight = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsRight);
|
const wGeoRight = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsRight);
|
||||||
wGeoRight.translate(-straightD / 2, 0, 0); // Center X of plate
|
wGeoRight.translate(-straightD / 2, 0, 0); // Центр X пластины
|
||||||
const w3 = wGeoRight.clone();
|
const w3 = wGeoRight.clone();
|
||||||
w3.rotateY(-Math.PI / 2); // Rotate to align with Z-axis
|
w3.rotateY(-Math.PI / 2); // Поворот
|
||||||
w3.translate(width / 2, thickness, 0); // Position at X=width/2
|
w3.translate(width / 2, thickness, 0); // Позиция на X=width/2
|
||||||
geometries.push(w3);
|
geometries.push(w3);
|
||||||
|
|
||||||
// Wall 4 (Left? -X).
|
// Стенка 4 (Левая -X).
|
||||||
// This wall is also rotated PI/2.
|
// Также повернута на PI/2.
|
||||||
// Plate Z (thickness) becomes World X (towards Right).
|
// Толщина по Z станем Мировой X (направо).
|
||||||
// We want it at X=-width/2.
|
// Хотим на X=-width/2.
|
||||||
// It will extrude to -width/2 + thickness. Correct for Left Wall.
|
|
||||||
const exclusionsLeftMapped = exclusionsLeft.map(e => ({
|
const exclusionsLeftMapped = exclusionsLeft.map(e => ({
|
||||||
start: straightD - e.end,
|
start: straightD - e.end,
|
||||||
end: straightD - e.start,
|
end: straightD - e.start,
|
||||||
@@ -402,10 +398,10 @@ export const createBinGeometry = (
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const wGeoLeft = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsLeftMapped);
|
const wGeoLeft = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsLeftMapped);
|
||||||
wGeoLeft.translate(-straightD / 2, 0, 0); // Center X of plate
|
wGeoLeft.translate(-straightD / 2, 0, 0); // Центр X пластины
|
||||||
const w4 = wGeoLeft.clone();
|
const w4 = wGeoLeft.clone();
|
||||||
w4.rotateY(Math.PI / 2);
|
w4.rotateY(Math.PI / 2);
|
||||||
w4.translate(-width / 2, thickness, 0); // Position at X=-width/2
|
w4.translate(-width / 2, thickness, 0); // Позиция на X=-width/2
|
||||||
geometries.push(w4);
|
geometries.push(w4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,7 +409,7 @@ export const createBinGeometry = (
|
|||||||
|
|
||||||
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ
|
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ
|
||||||
const innerWidth = width - (2 * thickness);
|
const innerWidth = width - (2 * thickness);
|
||||||
const innerDepth = depth - (2 * thickness); // Approximate usable space logic
|
const innerDepth = depth - (2 * thickness); // Приблизительное полезное пространство
|
||||||
|
|
||||||
partitions.forEach(p => {
|
partitions.forEach(p => {
|
||||||
const pMin = p.min ?? 0;
|
const pMin = p.min ?? 0;
|
||||||
@@ -424,97 +420,84 @@ export const createBinGeometry = (
|
|||||||
const lengthRatio = pMax - pMin;
|
const lengthRatio = pMax - pMin;
|
||||||
const midRatio = pMin + (lengthRatio / 2);
|
const midRatio = pMin + (lengthRatio / 2);
|
||||||
|
|
||||||
// FIX: Subtract thickness because partitions sit ON TOP of the floor
|
// ИСПРАВЛЕНИЕ: Вычитаем толщину, так как перегородки стоят НА дне
|
||||||
const effectiveHeight = Math.max(0.1, p.height - thickness);
|
const effectiveHeight = Math.max(0.1, p.height - thickness);
|
||||||
|
|
||||||
// --- PERFORATED LOGIC FOR PARTITIONS ---
|
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ ПЕРЕГОРОДОК ---
|
||||||
// If enabled, use Plate. Else use Extrude Solid.
|
// Если включено, используем Пластину. Иначе - сплошное Выдавливание.
|
||||||
const usePerf = perforation && perforation.enabled;
|
const usePerf = perforation && perforation.enabled;
|
||||||
let pX = 0, pY = 0; // Declare here for visibility in Fillets
|
let pX = 0, pY = 0; // Объявляем здесь для видимости в Скруглениях
|
||||||
|
|
||||||
if (usePerf) {
|
if (usePerf) {
|
||||||
// Calculate exact geometry
|
// Вычисляем точную геометрию
|
||||||
let pLen = 0;
|
let pLen = 0;
|
||||||
|
|
||||||
if (p.axis === 'x') {
|
if (p.axis === 'x') {
|
||||||
// Axis X -> Divider runs along Y (Depth)
|
// Ось X -> Разделитель идет вдоль Y (Глубина)
|
||||||
pLen = lengthRatio * innerDepth;
|
pLen = lengthRatio * innerDepth;
|
||||||
pX = (-innerWidth / 2) + (innerWidth * p.offset);
|
pX = (-innerWidth / 2) + (innerWidth * p.offset);
|
||||||
pY = (-innerDepth / 2) + (innerDepth * midRatio); // Center of partition
|
pY = (-innerDepth / 2) + (innerDepth * midRatio); // Центр перегородки
|
||||||
|
|
||||||
// Calculate Exclusions for Internal Partition
|
// Вычисляем исключения для внутренней перегородки
|
||||||
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
|
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
|
||||||
// This partition is 'p' (Axis X, runs along Depth).
|
// Эта перегородка 'p' (Ось X, идет по Глубине).
|
||||||
// Intersected by partitions 'n' (Axis Y, runs along Width).
|
// Пересекается перегородками 'n' (Ось Y, идут по Ширине).
|
||||||
// p global Y range: pY - pLen/2 to pY + pLen/2.
|
|
||||||
// n global Y pos: (-innerDepth/2) + (innerDepth * n.offset).
|
|
||||||
|
|
||||||
partitions.forEach(n => {
|
partitions.forEach(n => {
|
||||||
if (n.axis !== 'y') return; // Only orthogonal partitions intersect
|
if (n.axis !== 'y') return; // Пересекаются только ортогональные перегородки
|
||||||
// Check if n intersects p
|
// Проверяем, пересекает ли n перегородку p
|
||||||
// Intersection Y location:
|
// Y-координата пересечения:
|
||||||
const nY = (-innerDepth / 2) + (innerDepth * n.offset);
|
const nY = (-innerDepth / 2) + (innerDepth * n.offset);
|
||||||
|
|
||||||
// Does nY (which is global Y of the intersecting partition) match p's position?
|
|
||||||
// p is Axis X, runs along DEPTH (Global Y).
|
|
||||||
// p is centered at pX (Width) and covers pLen in Depth (Y).
|
|
||||||
// Wait. p (Axis X) runs along Y?
|
|
||||||
// p.axis='x' -> "Runs along Y (Depth)". Correct.
|
|
||||||
// So p covers Y range [pY - pLen/2, pY + pLen/2].
|
|
||||||
// n is Axis Y, runs along X (Width).
|
|
||||||
// n is centered at nY (Depth).
|
|
||||||
// So intersection happens if nY is within p's Y range.
|
|
||||||
|
|
||||||
const pStartGlobal = pY - pLen / 2;
|
const pStartGlobal = pY - pLen / 2;
|
||||||
const pEndGlobal = pY + pLen / 2;
|
const pEndGlobal = pY + pLen / 2;
|
||||||
|
|
||||||
// And check if n covers p's X location.
|
// И проверяем, покрывает ли n позицию X перегородки p.
|
||||||
// n runs along X from nMin to nMax.
|
// n идет по X от nMin до nMax.
|
||||||
const nXStart = (-innerWidth / 2) + (innerWidth * (n.min ?? 0));
|
const nXStart = (-innerWidth / 2) + (innerWidth * (n.min ?? 0));
|
||||||
const nXEnd = (-innerWidth / 2) + (innerWidth * (n.max ?? 1));
|
const nXEnd = (-innerWidth / 2) + (innerWidth * (n.max ?? 1));
|
||||||
const pXLoc = pX;
|
const pXLoc = pX;
|
||||||
|
|
||||||
if (nY >= pStartGlobal && nY <= pEndGlobal && pXLoc >= nXStart && pXLoc <= nXEnd) {
|
if (nY >= pStartGlobal && nY <= pEndGlobal && pXLoc >= nXStart && pXLoc <= nXEnd) {
|
||||||
// Intersection confirmed.
|
// Пересечение подтверждено.
|
||||||
// Calculate local coord on p's plate.
|
// Вычисляем локальные координаты на пластине p.
|
||||||
// p runs along Y. Plate local X maps to Global Y.
|
// p идет по Y. Локальный X пластины мапится на глобальный Y.
|
||||||
// local = global - pY + pLen/2.
|
|
||||||
const nHeight = Math.max(0.1, n.height - thickness);
|
const nHeight = Math.max(0.1, n.height - thickness);
|
||||||
const localX = nY - pY + pLen / 2;
|
const localX = nY - pY + pLen / 2;
|
||||||
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
|
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add margins (solid ends)
|
// Добавляем отступы (сплошные концы)
|
||||||
const margin = thickness;
|
const margin = thickness;
|
||||||
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
|
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
|
||||||
plate.translate(-pLen / 2, 0, 0); // Center X
|
plate.translate(-pLen / 2, 0, 0); // Центр X
|
||||||
|
|
||||||
// Rotate to align with Depth (along Z)
|
// Поворачиваем для выравнивания по Глубине (вдоль Z)
|
||||||
// Plate X -> Z
|
// Пластина X -> Z
|
||||||
plate.rotateY(-Math.PI / 2);
|
plate.rotateY(-Math.PI / 2);
|
||||||
|
|
||||||
// Position
|
// Позиционирование
|
||||||
// Plate is now vertical Z-aligned. Thickness along X.
|
// Пластина теперь вертикально по Z. Толщина по X.
|
||||||
plate.translate(pX + thickness / 2, thickness, pY);
|
plate.translate(pX + thickness / 2, thickness, pY);
|
||||||
geometries.push(plate);
|
geometries.push(plate);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Axis Y -> Divider runs along X (Width)
|
// Ось Y -> Разделитель идет вдоль X (Ширина)
|
||||||
pLen = lengthRatio * innerWidth;
|
pLen = lengthRatio * innerWidth;
|
||||||
pX = (-innerWidth / 2) + (innerWidth * midRatio);
|
pX = (-innerWidth / 2) + (innerWidth * midRatio);
|
||||||
pY = (-innerDepth / 2) + (innerDepth * p.offset);
|
pY = (-innerDepth / 2) + (innerDepth * p.offset);
|
||||||
|
|
||||||
// Calculate Exclusions
|
// Вычисление исключений
|
||||||
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
|
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
|
||||||
// This partition is 'p' (Axis Y, runs along Width).
|
// Эта перегородка 'p' (Ось Y, идет по Ширине).
|
||||||
// Intersected by partitions 'n' (Axis X, runs along Depth).
|
// Пересекается перегородками 'n' (Ось X, идут по Глубине).
|
||||||
|
|
||||||
partitions.forEach(n => {
|
partitions.forEach(n => {
|
||||||
if (n.axis !== 'x') return;
|
if (n.axis !== 'x') return;
|
||||||
const nX = (-innerWidth / 2) + (innerWidth * n.offset);
|
const nX = (-innerWidth / 2) + (innerWidth * n.offset);
|
||||||
|
|
||||||
const pStartGlobal = pX - pLen / 2; // X range of p
|
const pStartGlobal = pX - pLen / 2; // Диапазон X для p
|
||||||
const pEndGlobal = pX + pLen / 2;
|
const pEndGlobal = pX + pLen / 2;
|
||||||
|
|
||||||
const nYStart = (-innerDepth / 2) + (innerDepth * (n.min ?? 0));
|
const nYStart = (-innerDepth / 2) + (innerDepth * (n.min ?? 0));
|
||||||
@@ -522,9 +505,8 @@ export const createBinGeometry = (
|
|||||||
const pYLoc = pY;
|
const pYLoc = pY;
|
||||||
|
|
||||||
if (nX >= pStartGlobal && nX <= pEndGlobal && pYLoc >= nYStart && pYLoc <= nYEnd) {
|
if (nX >= pStartGlobal && nX <= pEndGlobal && pYLoc >= nYStart && pYLoc <= nYEnd) {
|
||||||
// Intersection confirmed
|
// Пересечение подтверждено
|
||||||
// local = global - pX + pLen/2
|
const nHeight = Math.max(0.1, n.height - thickness); // ВЫСОТА ПЕРЕСЕКАЮЩЕЙ ПЕРЕГОРОДКИ
|
||||||
const nHeight = Math.max(0.1, n.height - thickness); // INTERSECTING PARTITION HEIGHT
|
|
||||||
const localX = nX - pX + pLen / 2;
|
const localX = nX - pX + pLen / 2;
|
||||||
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
|
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
|
||||||
}
|
}
|
||||||
@@ -532,20 +514,19 @@ export const createBinGeometry = (
|
|||||||
|
|
||||||
const margin = thickness;
|
const margin = thickness;
|
||||||
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
|
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
|
||||||
plate.translate(-pLen / 2, 0, 0); // Center X
|
plate.translate(-pLen / 2, 0, 0); // Центр X
|
||||||
|
|
||||||
// Already aligned with X. Thickness along Z.
|
// Уже выровнено по X. Толщина по Z.
|
||||||
// Z range [0, th]. We want [-th/2, th/2] relative to pY.
|
// Диапазон Z [0, th]. Мы хотим [-th/2, th/2] относительно pY.
|
||||||
// Translate Z by -th/2.
|
|
||||||
plate.translate(0, 0, -thickness / 2);
|
plate.translate(0, 0, -thickness / 2);
|
||||||
|
|
||||||
// Move to position
|
// Перемещение на позицию
|
||||||
plate.translate(pX, thickness, pY);
|
plate.translate(pX, thickness, pY);
|
||||||
geometries.push(plate);
|
geometries.push(plate);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// --- SOLID LOGIC ---
|
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ ПЕРЕГОРОДОК ---
|
||||||
let pWidth = 0, pDepth = 0;
|
let pWidth = 0, pDepth = 0;
|
||||||
if (p.axis === 'x') {
|
if (p.axis === 'x') {
|
||||||
pWidth = thickness;
|
pWidth = thickness;
|
||||||
@@ -566,12 +547,12 @@ export const createBinGeometry = (
|
|||||||
geometries.push(partGeo);
|
geometries.push(partGeo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fillets Logic for partitions (Keep solid for strength/aesthetics)
|
// Логика скруглений для перегородок (Оставляем сплошными для прочности/эстетики)
|
||||||
if (p.rounded && radius > 1) {
|
if (p.rounded && radius > 1) {
|
||||||
const filletR = Math.min(radius, 5);
|
const filletR = Math.min(radius, 5);
|
||||||
const filletShape = createConcaveFilletShape(filletR);
|
const filletShape = createConcaveFilletShape(filletR);
|
||||||
|
|
||||||
// Helper to get neighbor height
|
// Помощник для получения высоты соседа
|
||||||
const getNeighborHeight = (pos: number) => {
|
const getNeighborHeight = (pos: number) => {
|
||||||
if (pos < 0.001 || pos > 0.999) return height;
|
if (pos < 0.001 || pos > 0.999) return height;
|
||||||
const neighbor = partitions.find(n => {
|
const neighbor = partitions.find(n => {
|
||||||
|
|||||||
Reference in New Issue
Block a user