Compare commits

3 Commits
main ... test

Author SHA1 Message Date
Халимов Рустам
de36479da1 Summary 2026-01-13 01:38:50 +03:00
Халимов Рустам
5664e99554 README.md 2026-01-13 01:30:05 +03:00
Халимов Рустам
9ff2c76912 Russian 2026-01-13 01:26:20 +03:00
4 changed files with 144 additions and 155 deletions

View File

@@ -1,23 +1,31 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# PrintFit - Генератор Органайзеров для 3D Печати
# Run and deploy your AI Studio app
PrintFit — это мощный веб-инструмент для создания параметрических органайзеров, разделителей и ящиков, оптимизированных для 3D-печати. Приложение позволяет легко проектировать системы хранения под любые размеры с возможностью детальной настройки перегородок и перфорации.
This contains everything you need to run your app locally.
![PrintFit Preview](https://via.placeholder.com/1200x600?text=PrintFit+3D+Preview)
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:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`
### 🖥 Интерактивный 3D Предпросмотр
- **Real-time рендеринг**: Мгновенное отображение изменений в браузере с помощью Three.js.
- **Управление камерой**: Вращение, масштабирование и панорамирование для детального осмотра модели со всех сторон.
- **Индикация размеров**: Внутренние размеры отсеков отображаются прямо внутри ячеек.
### 💾 Экспорт и Шеринг
- **STL Экспорт**: Генерация готовых к печати STL файлов. Можно скачать отдельные детали или весь проект архивом (ZIP).

View File

@@ -75,7 +75,7 @@ const App = () => {
return (
<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">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -93,7 +93,7 @@ const App = () => {
</div>
</header>
{/* Main Content */}
{/* Основной контент */}
<main className="flex-1 w-full max-w-7xl mx-auto p-4 overflow-hidden flex flex-col min-h-0">
{step === 1 && (
<div className="h-full overflow-y-auto animate-fade-in custom-scrollbar">
@@ -116,7 +116,7 @@ const App = () => {
)}
</main>
{/* Footer */}
{/* Подвал */}
<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">
<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>

View File

@@ -338,7 +338,7 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
</div>
<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="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">
@@ -387,7 +387,7 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
</Canvas>
</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="flex justify-between items-center mb-6 shrink-0">
<h2 className="text-xl font-bold flex items-center gap-2 text-primary">

View File

@@ -83,7 +83,7 @@ const createConcaveFilletShape = (radius: number): THREE.Shape => {
return shape;
};
// New Helper: Create Perforated Plate (Vertical Wall)
// Вспомогательная функция: Создание перфорированной пластины (Вертикальная стенка)
const createPerforatedPlate = (
width: number,
height: number,
@@ -100,17 +100,17 @@ const createPerforatedPlate = (
shape.lineTo(0, height);
shape.lineTo(0, 0);
// Hole Generation
// Генерация отверстий
if (perf && perf.enabled && width > perf.size && height > perf.size) {
const { shape: shapeType, size, gap } = perf;
const step = size + gap;
// Margins: ensure holes don't cut into the solid edge zones
// Отступы: предотвращаем попадание отверстий на сплошные края
const startX = Math.max(gap, marginLeft + gap);
const startY = gap;
const endX = width - Math.max(gap, marginRight + gap);
const endY = height - gap;
// Rows
// Строки
let row = 0;
for (let y = startY + size / 2; y < endY; y += (shapeType === 'triangle' || shapeType === 'honeycomb' ? step * 0.866 : step)) {
const isStaggered = (row % 2 !== 0);
@@ -120,18 +120,18 @@ const createPerforatedPlate = (
const hole = new THREE.Path();
const r = size / 2;
// Boundary check
// Проверка границ
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).
// If y > yMax, the exclusion doesn't apply (it's above the intersecting wall).
// Note: y is measured from bottom (0) to top (height).
// yMax is the height of the intersecting partition.
// Проверка зон исключения
// Зона активна, если X отверстия входит в [start, end] И Y отверстия ниже yMax (если указан).
// Если y > yMax, исключение не применяется (отверстие выше пересекающей стенки).
// Примечание: y измеряется от низа (0) до верха (height).
// yMax - высота пересекающей перегородки.
const inExclusion = exclusions.some(zone => {
if (x + r <= zone.start || x - r >= zone.end) return false; // X-axis check
if (zone.yMax !== undefined && y - r > zone.yMax) return false; // Y-axis check (hole above wall)
if (x + r <= zone.start || x - r >= zone.end) return false; // Проверка по оси X
if (zone.yMax !== undefined && y - r > zone.yMax) return false; // Проверка по оси Y (отверстие выше стенки)
return true;
});
@@ -140,7 +140,7 @@ const createPerforatedPlate = (
if (shapeType === 'circle') {
hole.absarc(x, y, r, 0, Math.PI * 2, true);
} else if (shapeType === 'honeycomb') {
// Hexagon
// Шестиугольник (Соты)
for (let i = 0; i < 6; i++) {
const ang = (i * 60 + 30) * Math.PI / 180;
const px = x + r * Math.cos(ang);
@@ -150,7 +150,7 @@ const createPerforatedPlate = (
}
hole.closePath();
} else if (shapeType === 'triangle') {
// Triangle
// Треугольник
const ang1 = -90 * Math.PI / 180;
const ang2 = 30 * 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 });
// Extruded along Z. Wall is flat on XY.
// We want "thickness" to be Z depth.
// Выдавливание по Z. Стенка плоская в XY.
// Мы хотим, чтобы "thickness" была глубиной по Z.
return geo;
};
// New Helper: Create Corner Profile (Extruded Vertical)
// Вспомогательная функция: Создание профиля угла (Выдавленный вертикально)
const createCornerProfile = (radius: number, thickness: number, height: number): THREE.BufferGeometry => {
if (radius <= 0) return new THREE.BufferGeometry();
const shape = new THREE.Shape();
// Create a Ring Segment (Hollow Corner) as a single loop.
// Center at (0,0).
const innerRadius = Math.max(0.01, radius - thickness); // Ensure slightly > 0 to maintain shape integrity
// Создаем сегмент кольца (Полый угол) как единый контур.
// Центр в (0,0).
const innerRadius = Math.max(0.01, radius - thickness); // Гарантируем чуть > 0 для целостности формы
// 1. Start at Outer Start
// 1. Начало внешней дуги
shape.moveTo(radius, 0);
// 2. Outer Arc (CCW) -> To (0, radius)
// 2. Внешняя дуга (против часовой стрелки) -> К (0, radius)
shape.absarc(0, 0, radius, 0, Math.PI / 2, false);
// 3. Line to Inner End (0, innerRadius)
// 3. Линия к внутреннему концу (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);
// 5. Close loop
// 5. Замыкаем контур
shape.lineTo(radius, 0);
// Extrude
// curveSegments 32 for smoothness
// Выдавливание
// curveSegments 32 для гладкости
const geo = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: false, curveSegments: 32 });
return geo;
};
@@ -218,9 +218,9 @@ export const createBinGeometry = (
floorGeo.rotateX(-Math.PI / 2); // Lay flat
geometries.push(floorGeo);
// WALLS
// СТЕНКИ
if (!perforation || !perforation.enabled) {
// --- ORIGINAL LOGIC (Optimized for Solid Walls) ---
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ СТЕН (Без перфорации) ---
const outerShape = createRoundedRectShape(width, depth, radius);
const innerRadius = Math.max(0.1, radius - thickness);
const innerWidth = width - (2 * thickness);
@@ -237,78 +237,78 @@ export const createBinGeometry = (
wallGeo.translate(0, thickness, 0);
geometries.push(wallGeo);
} else {
// --- PERFORATED LOGIC (Split Walls) ---
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ СТЕН (Раздельные части) ---
const wallHeight = height - thickness;
// Clamp radius to at least thickness for valid corners in this mode
// Ограничиваем радиус минимум толщиной стенки для корректных углов
const effRadius = Math.max(radius, thickness);
const straightW = width - 2 * effRadius;
const straightD = depth - 2 * effRadius;
// 1. Corners (4 pcs)
// 1. Углы (4 шт)
if (effRadius > 0) {
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);
// Base Corner Shape (after rotateX) is Q4 (+X, -Z).
// Rotation Logic: -90 degrees per Quadrant (Standard Three.js Y-Rot).
// Базовая форма угла (после rotateX) это Q4 (+X, -Z).
// Логика вращения: -90 градусов на квадрант (Стандартный Y-Rot в Three.js).
// Q4 (0) -> Q1 (-90) -> Q2 (-180/180) -> Q3 (-270/+90).
const positions = [
// Back Right (+X, +Z). Q1.
// Rot -90 (-PI/2).
// Задний Правый (+X, +Z). Q1.
// Поворот -90 (-PI/2).
{ x: width / 2 - effRadius, z: depth / 2 - effRadius, rot: -Math.PI / 2 },
// Back Left (-X, +Z). Q2.
// Rot 180 (PI).
// Задний Левый (-X, +Z). Q2.
// Поворот 180 (PI).
{ x: -(width / 2 - effRadius), z: depth / 2 - effRadius, rot: Math.PI },
// Front Left (-X, -Z). Q3.
// Rot 90 (PI/2). (Equivalent to -270).
// Передний Левый (-X, -Z). Q3.
// Поворот 90 (PI/2). (Эквивалентно -270).
{ x: -(width / 2 - effRadius), z: -(depth / 2 - effRadius), rot: Math.PI / 2 },
// Front Right (+X, -Z). Q4.
// Rot 0.
// Передний Правый (+X, -Z). Q4.
// Поворот 0.
{ x: width / 2 - effRadius, z: -(depth / 2 - effRadius), rot: 0 }
];
positions.forEach(pos => {
const corner = cornerGeoBase.clone();
corner.rotateY(pos.rot);
corner.translate(pos.x, 0, pos.z); // Start at Y=0
corner.translate(0, thickness, 0); // Move on top of floor
corner.translate(pos.x, 0, pos.z); // Старт на Y=0
corner.translate(0, thickness, 0); // Сдвиг на толщину дна
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 exclusionsFront: { start: number, end: number, yMax: number }[] = [];
const exclusionsLeft: { start: number, end: number, yMax: number }[] = [];
const exclusionsRight: { start: number, end: number, yMax: number }[] = [];
// Inner Dimensions
// Внутренние размеры
const effectiveInnerW = width - 2 * thickness;
const effectiveInnerD = depth - 2 * thickness;
partitions.forEach(p => {
const hEff = Math.max(0.1, p.height - thickness); // Exclude only up to partition height
// Ensure solid strip around partition by adding padding to exclusion zone
const hEff = Math.max(0.1, p.height - thickness); // Исключаем только до высоты перегородки
// Гарантируем сплошную полосу вокруг перегородки добавлением отступа к зоне исключения
const padding = (perforation?.gap ?? 2) + 1;
if (p.axis === 'x') {
// Runs Depth-wise (Y axis in Layout).
// Intersects Front and Back walls.
// Идет по глубине (Ось Y в макете).
// Пересекает Переднюю и Заднюю стенки.
const partGlobalX = (-effectiveInnerW / 2) + (effectiveInnerW * p.offset);
const sW = width - 2 * effRadius;
const localXOnWall = partGlobalX + sW / 2;
// Check intersection with active straight wall area + padding
// Проверка пересечения с активной зоной стены + отступ
if (localXOnWall + thickness / 2 + padding > 0 && localXOnWall - thickness / 2 - padding < sW) {
const zone = {
start: localXOnWall - thickness / 2 - padding,
@@ -316,13 +316,13 @@ export const createBinGeometry = (
yMax: hEff
};
if ((p.max ?? 1) > 0.99) exclusionsFront.push(zone); // Near
if ((p.min ?? 0) < 0.01) exclusionsBack.push(zone); // Far
if ((p.max ?? 1) > 0.99) exclusionsFront.push(zone); // Ближняя
if ((p.min ?? 0) < 0.01) exclusionsBack.push(zone); // Дальняя
}
} else { // p.axis === 'y'
// Runs Width-wise (X axis in Layout).
// Intersects Left and Right walls.
// Идет по ширине (Ось X в макете).
// Пересекает Левую и Правую стенки.
const partGlobalZ = (-effectiveInnerD / 2) + (effectiveInnerD * p.offset);
const sD = depth - 2 * effRadius;
@@ -345,21 +345,21 @@ export const createBinGeometry = (
}
});
// Front/Back
// Передняя/Задняя
if (straightW > 0.1) {
// Wall 1 (at +Z). This is "Front" (Near). Contacts p.max.
// Uses exclusionsFront.
// Стенка 1 (+Z). Это "Передняя" (Ближняя). Контактирует с p.max.
// Использует exclusionsFront.
const wGeoFront = createPerforatedPlate(straightW, wallHeight, thickness, perforation, 0, 0, exclusionsFront);
wGeoFront.translate(-straightW / 2, 0, 0);
const w1 = wGeoFront.clone();
w1.translate(0, thickness, depth / 2 - thickness);
geometries.push(w1);
// Wall 2 (at -Z). This is "Back" (Far). Contacts p.min.
// Uses exclusionsBack.
// Needs checking mapping (Rot 180).
// p.offset increases X. Wall rotated 180 means X is inverted.
// So we map exclusionsBack.
// Стенка 2 (-Z). Это "Задняя" (Дальняя). Контактирует с p.min.
// Использует exclusionsBack.
// Нужна проверка маппинга (Поворот 180).
// p.offset увеличивает X. Стенка повернута на 180, значит X инвертирован.
// Маппим exclusionsBack.
const exclusionsBackMapped = exclusionsBack.map(e => ({
start: straightW - e.end,
end: straightW - e.start,
@@ -374,27 +374,23 @@ export const createBinGeometry = (
geometries.push(w2);
}
// Left/Right
// Левая/Правая
if (straightD > 0.1) {
// Wall 3 (Right? +X).
// Plate is 0..straightD in X, 0..wallHeight in Y. Thickness along Z.
// We want it to be at X = width/2.
// It needs to be rotated -PI/2 around Y to align its X-axis with World Z-axis.
// 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.
// Стенка 3 (Правая +X).
// Пластина 0..straightD по X, 0..wallHeight по Y. Толщина по Z.
// Хотим разместить на X = width/2.
// Нужно повернуть -PI/2 вокруг Y чтобы выровнять ось X пластины с мировой осью Z.
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();
w3.rotateY(-Math.PI / 2); // Rotate to align with Z-axis
w3.translate(width / 2, thickness, 0); // Position at X=width/2
w3.rotateY(-Math.PI / 2); // Поворот
w3.translate(width / 2, thickness, 0); // Позиция на X=width/2
geometries.push(w3);
// Wall 4 (Left? -X).
// This wall is also rotated PI/2.
// Plate Z (thickness) becomes World X (towards Right).
// We want it at X=-width/2.
// It will extrude to -width/2 + thickness. Correct for Left Wall.
// Стенка 4 (Левая -X).
// Также повернута на PI/2.
// Толщина по Z станем Мировой X (направо).
// Хотим на X=-width/2.
const exclusionsLeftMapped = exclusionsLeft.map(e => ({
start: straightD - e.end,
end: straightD - e.start,
@@ -402,10 +398,10 @@ export const createBinGeometry = (
}));
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();
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);
}
}
@@ -413,7 +409,7 @@ export const createBinGeometry = (
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ
const innerWidth = width - (2 * thickness);
const innerDepth = depth - (2 * thickness); // Approximate usable space logic
const innerDepth = depth - (2 * thickness); // Приблизительное полезное пространство
partitions.forEach(p => {
const pMin = p.min ?? 0;
@@ -424,97 +420,84 @@ export const createBinGeometry = (
const lengthRatio = pMax - pMin;
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);
// --- PERFORATED LOGIC FOR PARTITIONS ---
// If enabled, use Plate. Else use Extrude Solid.
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ ПЕРЕГОРОДОК ---
// Если включено, используем Пластину. Иначе - сплошное Выдавливание.
const usePerf = perforation && perforation.enabled;
let pX = 0, pY = 0; // Declare here for visibility in Fillets
let pX = 0, pY = 0; // Объявляем здесь для видимости в Скруглениях
if (usePerf) {
// Calculate exact geometry
// Вычисляем точную геометрию
let pLen = 0;
if (p.axis === 'x') {
// Axis X -> Divider runs along Y (Depth)
// Ось X -> Разделитель идет вдоль Y (Глубина)
pLen = lengthRatio * innerDepth;
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 }[] = [];
// This partition is 'p' (Axis X, runs along Depth).
// Intersected by partitions 'n' (Axis Y, runs along Width).
// p global Y range: pY - pLen/2 to pY + pLen/2.
// n global Y pos: (-innerDepth/2) + (innerDepth * n.offset).
// Эта перегородка 'p' (Ось X, идет по Глубине).
// Пересекается перегородками 'n' (Ось Y, идут по Ширине).
partitions.forEach(n => {
if (n.axis !== 'y') return; // Only orthogonal partitions intersect
// Check if n intersects p
// Intersection Y location:
if (n.axis !== 'y') return; // Пересекаются только ортогональные перегородки
// Проверяем, пересекает ли n перегородку p
// Y-координата пересечения:
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 pEndGlobal = pY + pLen / 2;
// And check if n covers p's X location.
// n runs along X from nMin to nMax.
// И проверяем, покрывает ли n позицию X перегородки p.
// n идет по X от nMin до nMax.
const nXStart = (-innerWidth / 2) + (innerWidth * (n.min ?? 0));
const nXEnd = (-innerWidth / 2) + (innerWidth * (n.max ?? 1));
const pXLoc = pX;
if (nY >= pStartGlobal && nY <= pEndGlobal && pXLoc >= nXStart && pXLoc <= nXEnd) {
// Intersection confirmed.
// Calculate local coord on p's plate.
// p runs along Y. Plate local X maps to Global Y.
// local = global - pY + pLen/2.
// Пересечение подтверждено.
// Вычисляем локальные координаты на пластине p.
// p идет по Y. Локальный X пластины мапится на глобальный Y.
const nHeight = Math.max(0.1, n.height - thickness);
const localX = nY - pY + pLen / 2;
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
}
});
// Add margins (solid ends)
// Добавляем отступы (сплошные концы)
const margin = thickness;
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)
// Plate X -> Z
// Поворачиваем для выравнивания по Глубине (вдоль Z)
// Пластина X -> Z
plate.rotateY(-Math.PI / 2);
// Position
// Plate is now vertical Z-aligned. Thickness along X.
// Позиционирование
// Пластина теперь вертикально по Z. Толщина по X.
plate.translate(pX + thickness / 2, thickness, pY);
geometries.push(plate);
} else {
// Axis Y -> Divider runs along X (Width)
// Ось Y -> Разделитель идет вдоль X (Ширина)
pLen = lengthRatio * innerWidth;
pX = (-innerWidth / 2) + (innerWidth * midRatio);
pY = (-innerDepth / 2) + (innerDepth * p.offset);
// Calculate Exclusions
// Вычисление исключений
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
// This partition is 'p' (Axis Y, runs along Width).
// Intersected by partitions 'n' (Axis X, runs along Depth).
// Эта перегородка 'p' (Ось Y, идет по Ширине).
// Пересекается перегородками 'n' (Ось X, идут по Глубине).
partitions.forEach(n => {
if (n.axis !== 'x') return;
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 nYStart = (-innerDepth / 2) + (innerDepth * (n.min ?? 0));
@@ -522,9 +505,8 @@ export const createBinGeometry = (
const pYLoc = pY;
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); // INTERSECTING PARTITION HEIGHT
// Пересечение подтверждено
const nHeight = Math.max(0.1, n.height - thickness); // ВЫСОТА ПЕРЕСЕКАЮЩЕЙ ПЕРЕГОРОДКИ
const localX = nX - pX + pLen / 2;
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
}
@@ -532,20 +514,19 @@ export const createBinGeometry = (
const margin = thickness;
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.
// Z range [0, th]. We want [-th/2, th/2] relative to pY.
// Translate Z by -th/2.
// Уже выровнено по X. Толщина по Z.
// Диапазон Z [0, th]. Мы хотим [-th/2, th/2] относительно pY.
plate.translate(0, 0, -thickness / 2);
// Move to position
// Перемещение на позицию
plate.translate(pX, thickness, pY);
geometries.push(plate);
}
} else {
// --- SOLID LOGIC ---
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ ПЕРЕГОРОДОК ---
let pWidth = 0, pDepth = 0;
if (p.axis === 'x') {
pWidth = thickness;
@@ -566,12 +547,12 @@ export const createBinGeometry = (
geometries.push(partGeo);
}
// Fillets Logic for partitions (Keep solid for strength/aesthetics)
// Логика скруглений для перегородок (Оставляем сплошными для прочности/эстетики)
if (p.rounded && radius > 1) {
const filletR = Math.min(radius, 5);
const filletShape = createConcaveFilletShape(filletR);
// Helper to get neighbor height
// Помощник для получения высоты соседа
const getNeighborHeight = (pos: number) => {
if (pos < 0.001 || pos > 0.999) return height;
const neighbor = partitions.find(n => {