Russian
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user