622 lines
29 KiB
TypeScript
622 lines
29 KiB
TypeScript
import * as THREE from 'three';
|
||
import { STLExporter, mergeBufferGeometries } from 'three-stdlib';
|
||
import { AppConfig, LayoutSplits, GeneratedPart, Partition, PerforationConfig } from '../types';
|
||
|
||
export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => {
|
||
const parts: GeneratedPart[] = [];
|
||
const safeX = Array.isArray(splits?.x) ? splits.x : [];
|
||
const safeY = Array.isArray(splits?.y) ? splits.y : [];
|
||
const safeParts = splits?.partitions || {};
|
||
|
||
const xPoints = [0, ...[...safeX].sort((a, b) => a - b), 1];
|
||
const yPoints = [0, ...[...safeY].sort((a, b) => a - b), 1];
|
||
|
||
let partCounter = 1;
|
||
|
||
for (let i = 0; i < xPoints.length - 1; i++) {
|
||
for (let j = 0; j < yPoints.length - 1; j++) {
|
||
const rawW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width;
|
||
const rawD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth;
|
||
|
||
if (rawW < 5 || rawD < 5) continue;
|
||
|
||
const rawX = xPoints[i] * config.drawer.width;
|
||
const rawY = yPoints[j] * config.drawer.depth;
|
||
const internalPartitions = safeParts[`${i}-${j}`] || [];
|
||
|
||
const realWidth = rawW - config.printerTolerance;
|
||
const realDepth = rawD - config.printerTolerance;
|
||
const realX = rawX + (config.printerTolerance / 2);
|
||
const realY = rawY + (config.printerTolerance / 2);
|
||
|
||
parts.push({
|
||
id: `part-${partCounter}`,
|
||
name: `Ячейка ${i + 1}-${j + 1}`,
|
||
width: realWidth,
|
||
depth: realDepth,
|
||
height: config.drawer.height,
|
||
x: realX,
|
||
y: realY,
|
||
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
|
||
internalPartitions: internalPartitions
|
||
});
|
||
partCounter++;
|
||
}
|
||
}
|
||
return parts;
|
||
};
|
||
|
||
// --- ГЕОМЕТРИЯ ---
|
||
|
||
const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => {
|
||
const shape = new THREE.Shape();
|
||
const x = -width / 2;
|
||
const y = -height / 2;
|
||
const r = Math.min(radius, width / 2 - 0.1, height / 2 - 0.1);
|
||
|
||
if (r <= 0.1) {
|
||
shape.moveTo(x, y);
|
||
shape.lineTo(x + width, y);
|
||
shape.lineTo(x + width, y + height);
|
||
shape.lineTo(x, y + height);
|
||
shape.lineTo(x, y);
|
||
} else {
|
||
shape.moveTo(x, y + r);
|
||
shape.lineTo(x, y + height - r);
|
||
shape.quadraticCurveTo(x, y + height, x + r, y + height);
|
||
shape.lineTo(x + width - r, y + height);
|
||
shape.quadraticCurveTo(x + width, y + height, x + width, y + height - r);
|
||
shape.lineTo(x + width, y + r);
|
||
shape.quadraticCurveTo(x + width, y, x + width - r, y);
|
||
shape.lineTo(x + r, y);
|
||
shape.quadraticCurveTo(x, y, x, y + r);
|
||
}
|
||
return shape;
|
||
};
|
||
|
||
const createConcaveFilletShape = (radius: number): THREE.Shape => {
|
||
const shape = new THREE.Shape();
|
||
shape.moveTo(0, 0);
|
||
shape.lineTo(radius, 0);
|
||
shape.absarc(radius, radius, radius, -Math.PI / 2, -Math.PI, true);
|
||
shape.lineTo(0, 0);
|
||
return shape;
|
||
};
|
||
|
||
// Вспомогательная функция: Создание перфорированной пластины (Вертикальная стенка)
|
||
const createPerforatedPlate = (
|
||
width: number,
|
||
height: number,
|
||
thickness: number,
|
||
perf: PerforationConfig,
|
||
marginLeft: number = 0,
|
||
marginRight: number = 0,
|
||
exclusions: { start: number, end: number, yMax?: number }[] = []
|
||
): THREE.BufferGeometry => {
|
||
const shape = new THREE.Shape();
|
||
shape.moveTo(0, 0);
|
||
shape.lineTo(width, 0);
|
||
shape.lineTo(width, height);
|
||
shape.lineTo(0, height);
|
||
shape.lineTo(0, 0);
|
||
|
||
// Генерация отверстий
|
||
if (perf && perf.enabled && width > perf.size && height > perf.size) {
|
||
const { shape: shapeType, size, gap } = perf;
|
||
const step = size + gap;
|
||
// Отступы: предотвращаем попадание отверстий на сплошные края
|
||
const startX = Math.max(gap, marginLeft + gap);
|
||
const startY = gap;
|
||
const endX = width - Math.max(gap, marginRight + gap);
|
||
const endY = height - gap;
|
||
|
||
// Строки
|
||
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);
|
||
const xOffset = (isStaggered && (shapeType === 'honeycomb' || shapeType === 'triangle')) ? step / 2 : 0;
|
||
|
||
for (let x = startX + size / 2 + xOffset; x < endX; x += step) {
|
||
const hole = new THREE.Path();
|
||
const r = size / 2;
|
||
|
||
// Проверка границ
|
||
if (x - r < marginLeft || x + r > width - marginRight || y - r < 0 || y + r > height) continue;
|
||
|
||
// Проверка зон исключения
|
||
// Зона активна, если 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
|
||
if (zone.yMax !== undefined && y - r > zone.yMax) return false; // Проверка по оси Y (отверстие выше стенки)
|
||
return true;
|
||
});
|
||
|
||
if (inExclusion) continue;
|
||
|
||
if (shapeType === 'circle') {
|
||
hole.absarc(x, y, r, 0, Math.PI * 2, true);
|
||
} else if (shapeType === 'honeycomb') {
|
||
// Шестиугольник (Соты)
|
||
for (let i = 0; i < 6; i++) {
|
||
const ang = (i * 60 + 30) * Math.PI / 180;
|
||
const px = x + r * Math.cos(ang);
|
||
const py = y + r * Math.sin(ang);
|
||
if (i === 0) hole.moveTo(px, py);
|
||
else hole.lineTo(px, py);
|
||
}
|
||
hole.closePath();
|
||
} else if (shapeType === 'triangle') {
|
||
// Треугольник
|
||
const ang1 = -90 * Math.PI / 180;
|
||
const ang2 = 30 * Math.PI / 180;
|
||
const ang3 = 150 * Math.PI / 180;
|
||
hole.moveTo(x + r * Math.cos(ang1), y + r * Math.sin(ang1));
|
||
hole.lineTo(x + r * Math.cos(ang2), y + r * Math.sin(ang2));
|
||
hole.lineTo(x + r * Math.cos(ang3), y + r * Math.sin(ang3));
|
||
hole.closePath();
|
||
}
|
||
shape.holes.push(hole);
|
||
}
|
||
row++;
|
||
}
|
||
}
|
||
|
||
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
|
||
// Выдавливание по Z. Стенка плоская в XY.
|
||
// Мы хотим, чтобы "thickness" была глубиной по Z.
|
||
return geo;
|
||
};
|
||
|
||
// Вспомогательная функция: Создание профиля угла (Выдавленный вертикально)
|
||
const createCornerProfile = (radius: number, thickness: number, height: number): THREE.BufferGeometry => {
|
||
if (radius <= 0) return new THREE.BufferGeometry();
|
||
const shape = new THREE.Shape();
|
||
|
||
// Создаем сегмент кольца (Полый угол) как единый контур.
|
||
// Центр в (0,0).
|
||
const innerRadius = Math.max(0.01, radius - thickness); // Гарантируем чуть > 0 для целостности формы
|
||
|
||
// 1. Начало внешней дуги
|
||
shape.moveTo(radius, 0);
|
||
|
||
// 2. Внешняя дуга (против часовой стрелки) -> К (0, radius)
|
||
shape.absarc(0, 0, radius, 0, Math.PI / 2, false);
|
||
|
||
// 3. Линия к внутреннему концу (0, innerRadius)
|
||
shape.lineTo(0, innerRadius);
|
||
|
||
// 4. Внутренняя дуга (по часовой стрелке) -> К (innerRadius, 0)
|
||
shape.absarc(0, 0, innerRadius, Math.PI / 2, 0, true);
|
||
|
||
// 5. Замыкаем контур
|
||
shape.lineTo(radius, 0);
|
||
|
||
// Выдавливание
|
||
// curveSegments 32 для гладкости
|
||
const geo = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: false, curveSegments: 32 });
|
||
return geo;
|
||
};
|
||
|
||
export const createBinGeometry = (
|
||
width: number,
|
||
depth: number,
|
||
height: number,
|
||
thickness: number,
|
||
radius: number = 0,
|
||
partitions: Partition[] = [],
|
||
perforation?: PerforationConfig
|
||
): THREE.BufferGeometry => {
|
||
const geometries: THREE.BufferGeometry[] = [];
|
||
|
||
// ДНО (Floor) - Always same
|
||
const floorShape = createRoundedRectShape(width, depth, radius);
|
||
const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false });
|
||
floorGeo.rotateX(-Math.PI / 2); // Lay flat
|
||
geometries.push(floorGeo);
|
||
|
||
// СТЕНКИ
|
||
if (!perforation || !perforation.enabled) {
|
||
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ СТЕН (Без перфорации) ---
|
||
const outerShape = createRoundedRectShape(width, depth, radius);
|
||
const innerRadius = Math.max(0.1, radius - thickness);
|
||
const innerWidth = width - (2 * thickness);
|
||
const innerDepth = depth - (2 * thickness);
|
||
|
||
if (innerWidth > 0.1 && innerDepth > 0.1) {
|
||
const innerHole = createRoundedRectShape(innerWidth, innerDepth, innerRadius);
|
||
outerShape.holes.push(innerHole);
|
||
}
|
||
|
||
const wallHeight = height - thickness;
|
||
const wallGeo = new THREE.ExtrudeGeometry(outerShape, { depth: wallHeight, bevelEnabled: false });
|
||
wallGeo.rotateX(-Math.PI / 2);
|
||
wallGeo.translate(0, thickness, 0);
|
||
geometries.push(wallGeo);
|
||
} else {
|
||
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ СТЕН (Раздельные части) ---
|
||
const wallHeight = height - thickness;
|
||
// Ограничиваем радиус минимум толщиной стенки для корректных углов
|
||
const effRadius = Math.max(radius, thickness);
|
||
|
||
const straightW = width - 2 * effRadius;
|
||
const straightD = depth - 2 * effRadius;
|
||
|
||
// 1. Углы (4 шт)
|
||
if (effRadius > 0) {
|
||
const cornerGeoBase = createCornerProfile(effRadius, thickness, wallHeight);
|
||
|
||
// 1. Поднимаем: Выдавливание Z -> Y. Форма перемещается в X(+)/Z(+).
|
||
cornerGeoBase.rotateX(-Math.PI / 2);
|
||
|
||
// Базовая форма угла (после rotateX) это Q4 (+X, -Z).
|
||
// Логика вращения: -90 градусов на квадрант (Стандартный Y-Rot в Three.js).
|
||
// Q4 (0) -> Q1 (-90) -> Q2 (-180/180) -> Q3 (-270/+90).
|
||
|
||
const positions = [
|
||
// Задний Правый (+X, +Z). Q1.
|
||
// Поворот -90 (-PI/2).
|
||
{ x: width / 2 - effRadius, z: depth / 2 - effRadius, rot: -Math.PI / 2 },
|
||
|
||
// Задний Левый (-X, +Z). Q2.
|
||
// Поворот 180 (PI).
|
||
{ x: -(width / 2 - effRadius), z: depth / 2 - effRadius, rot: Math.PI },
|
||
|
||
// Передний Левый (-X, -Z). Q3.
|
||
// Поворот 90 (PI/2). (Эквивалентно -270).
|
||
{ x: -(width / 2 - effRadius), z: -(depth / 2 - effRadius), rot: Math.PI / 2 },
|
||
|
||
// Передний Правый (+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); // Старт на Y=0
|
||
corner.translate(0, thickness, 0); // Сдвиг на толщину дна
|
||
geometries.push(corner);
|
||
});
|
||
}
|
||
|
||
// 2. Прямые стены (4 шт) - Центрированы по краям
|
||
|
||
// ЛОГИКА СБОРА ИСКЛЮЧЕНИЙ ДЛЯ СТЕН
|
||
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 }[] = [];
|
||
|
||
// Внутренние размеры
|
||
const effectiveInnerW = width - 2 * thickness;
|
||
const effectiveInnerD = depth - 2 * thickness;
|
||
|
||
partitions.forEach(p => {
|
||
const hEff = Math.max(0.1, p.height - thickness); // Исключаем только до высоты перегородки
|
||
// Гарантируем сплошную полосу вокруг перегородки добавлением отступа к зоне исключения
|
||
const padding = (perforation?.gap ?? 2) + 1;
|
||
|
||
if (p.axis === 'x') {
|
||
// Идет по глубине (Ось Y в макете).
|
||
// Пересекает Переднюю и Заднюю стенки.
|
||
|
||
const partGlobalX = (-effectiveInnerW / 2) + (effectiveInnerW * p.offset);
|
||
const sW = width - 2 * effRadius;
|
||
const localXOnWall = partGlobalX + sW / 2;
|
||
|
||
// Проверка пересечения с активной зоной стены + отступ
|
||
if (localXOnWall + thickness / 2 + padding > 0 && localXOnWall - thickness / 2 - padding < sW) {
|
||
const zone = {
|
||
start: localXOnWall - thickness / 2 - padding,
|
||
end: localXOnWall + thickness / 2 + padding,
|
||
yMax: hEff
|
||
};
|
||
|
||
if ((p.max ?? 1) > 0.99) exclusionsFront.push(zone); // Ближняя
|
||
if ((p.min ?? 0) < 0.01) exclusionsBack.push(zone); // Дальняя
|
||
}
|
||
|
||
} else { // p.axis === 'y'
|
||
// Идет по ширине (Ось X в макете).
|
||
// Пересекает Левую и Правую стенки.
|
||
|
||
const partGlobalZ = (-effectiveInnerD / 2) + (effectiveInnerD * p.offset);
|
||
const sD = depth - 2 * effRadius;
|
||
const localXOnWall = partGlobalZ + sD / 2;
|
||
|
||
if (localXOnWall + thickness / 2 + padding > 0 && localXOnWall - thickness / 2 - padding < sD) {
|
||
const zone = {
|
||
start: localXOnWall - thickness / 2 - padding,
|
||
end: localXOnWall + thickness / 2 + padding,
|
||
yMax: hEff
|
||
};
|
||
|
||
if ((p.min ?? 0) < 0.01) {
|
||
exclusionsLeft.push(zone);
|
||
}
|
||
if ((p.max ?? 1) > 0.99) {
|
||
exclusionsRight.push(zone);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Передняя/Задняя
|
||
if (straightW > 0.1) {
|
||
// Стенка 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);
|
||
|
||
// Стенка 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,
|
||
yMax: e.yMax
|
||
}));
|
||
|
||
const wGeoBack = createPerforatedPlate(straightW, wallHeight, thickness, perforation, 0, 0, exclusionsBackMapped);
|
||
wGeoBack.translate(-straightW / 2, 0, 0);
|
||
const w2 = wGeoBack.clone();
|
||
w2.rotateY(Math.PI);
|
||
w2.translate(0, thickness, -(depth / 2 - thickness));
|
||
geometries.push(w2);
|
||
}
|
||
|
||
// Левая/Правая
|
||
if (straightD > 0.1) {
|
||
// Стенка 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); // Центр X пластины
|
||
const w3 = wGeoRight.clone();
|
||
w3.rotateY(-Math.PI / 2); // Поворот
|
||
w3.translate(width / 2, thickness, 0); // Позиция на X=width/2
|
||
geometries.push(w3);
|
||
|
||
// Стенка 4 (Левая -X).
|
||
// Также повернута на PI/2.
|
||
// Толщина по Z станем Мировой X (направо).
|
||
// Хотим на X=-width/2.
|
||
const exclusionsLeftMapped = exclusionsLeft.map(e => ({
|
||
start: straightD - e.end,
|
||
end: straightD - e.start,
|
||
yMax: e.yMax
|
||
}));
|
||
|
||
const wGeoLeft = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsLeftMapped);
|
||
wGeoLeft.translate(-straightD / 2, 0, 0); // Центр X пластины
|
||
const w4 = wGeoLeft.clone();
|
||
w4.rotateY(Math.PI / 2);
|
||
w4.translate(-width / 2, thickness, 0); // Позиция на X=-width/2
|
||
geometries.push(w4);
|
||
}
|
||
}
|
||
|
||
|
||
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ
|
||
const innerWidth = width - (2 * thickness);
|
||
const innerDepth = depth - (2 * thickness); // Приблизительное полезное пространство
|
||
|
||
partitions.forEach(p => {
|
||
const pMin = p.min ?? 0;
|
||
const pMax = p.max ?? 1;
|
||
|
||
if (pMax - pMin < 0.01) return;
|
||
|
||
const lengthRatio = pMax - pMin;
|
||
const midRatio = pMin + (lengthRatio / 2);
|
||
|
||
// ИСПРАВЛЕНИЕ: Вычитаем толщину, так как перегородки стоят НА дне
|
||
const effectiveHeight = Math.max(0.1, p.height - thickness);
|
||
|
||
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ ПЕРЕГОРОДОК ---
|
||
// Если включено, используем Пластину. Иначе - сплошное Выдавливание.
|
||
const usePerf = perforation && perforation.enabled;
|
||
let pX = 0, pY = 0; // Объявляем здесь для видимости в Скруглениях
|
||
|
||
if (usePerf) {
|
||
// Вычисляем точную геометрию
|
||
let pLen = 0;
|
||
|
||
if (p.axis === 'x') {
|
||
// Ось X -> Разделитель идет вдоль Y (Глубина)
|
||
pLen = lengthRatio * innerDepth;
|
||
pX = (-innerWidth / 2) + (innerWidth * p.offset);
|
||
pY = (-innerDepth / 2) + (innerDepth * midRatio); // Центр перегородки
|
||
|
||
// Вычисляем исключения для внутренней перегородки
|
||
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
|
||
// Эта перегородка 'p' (Ось X, идет по Глубине).
|
||
// Пересекается перегородками 'n' (Ось Y, идут по Ширине).
|
||
|
||
partitions.forEach(n => {
|
||
if (n.axis !== 'y') return; // Пересекаются только ортогональные перегородки
|
||
// Проверяем, пересекает ли n перегородку p
|
||
// Y-координата пересечения:
|
||
const nY = (-innerDepth / 2) + (innerDepth * n.offset);
|
||
|
||
const pStartGlobal = pY - pLen / 2;
|
||
const pEndGlobal = pY + pLen / 2;
|
||
|
||
// И проверяем, покрывает ли 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) {
|
||
// Пересечение подтверждено.
|
||
// Вычисляем локальные координаты на пластине 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 });
|
||
}
|
||
});
|
||
|
||
// Добавляем отступы (сплошные концы)
|
||
const margin = thickness;
|
||
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
|
||
plate.translate(-pLen / 2, 0, 0); // Центр X
|
||
|
||
// Поворачиваем для выравнивания по Глубине (вдоль Z)
|
||
// Пластина X -> Z
|
||
plate.rotateY(-Math.PI / 2);
|
||
|
||
// Позиционирование
|
||
// Пластина теперь вертикально по Z. Толщина по X.
|
||
plate.translate(pX + thickness / 2, thickness, pY);
|
||
geometries.push(plate);
|
||
|
||
} else {
|
||
// Ось Y -> Разделитель идет вдоль X (Ширина)
|
||
pLen = lengthRatio * innerWidth;
|
||
pX = (-innerWidth / 2) + (innerWidth * midRatio);
|
||
pY = (-innerDepth / 2) + (innerDepth * p.offset);
|
||
|
||
// Вычисление исключений
|
||
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
|
||
// Эта перегородка '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 для p
|
||
const pEndGlobal = pX + pLen / 2;
|
||
|
||
const nYStart = (-innerDepth / 2) + (innerDepth * (n.min ?? 0));
|
||
const nYEnd = (-innerDepth / 2) + (innerDepth * (n.max ?? 1));
|
||
const pYLoc = pY;
|
||
|
||
if (nX >= pStartGlobal && nX <= pEndGlobal && pYLoc >= nYStart && pYLoc <= nYEnd) {
|
||
// Пересечение подтверждено
|
||
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 });
|
||
}
|
||
});
|
||
|
||
const margin = thickness;
|
||
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
|
||
plate.translate(-pLen / 2, 0, 0); // Центр X
|
||
|
||
// Уже выровнено по X. Толщина по Z.
|
||
// Диапазон Z [0, th]. Мы хотим [-th/2, th/2] относительно pY.
|
||
plate.translate(0, 0, -thickness / 2);
|
||
|
||
// Перемещение на позицию
|
||
plate.translate(pX, thickness, pY);
|
||
geometries.push(plate);
|
||
}
|
||
|
||
} else {
|
||
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ ПЕРЕГОРОДОК ---
|
||
let pWidth = 0, pDepth = 0;
|
||
if (p.axis === 'x') {
|
||
pWidth = thickness;
|
||
pDepth = lengthRatio * innerDepth;
|
||
pX = (-innerWidth / 2) + (innerWidth * p.offset);
|
||
pY = (-innerDepth / 2) + (innerDepth * midRatio);
|
||
} else {
|
||
pWidth = lengthRatio * innerWidth;
|
||
pDepth = thickness;
|
||
pX = (-innerWidth / 2) + (innerWidth * midRatio);
|
||
pY = (-innerDepth / 2) + (innerDepth * p.offset);
|
||
}
|
||
|
||
const partShape = createRoundedRectShape(pWidth, pDepth, 0.1);
|
||
const partGeo = new THREE.ExtrudeGeometry(partShape, { depth: effectiveHeight, bevelEnabled: false });
|
||
partGeo.rotateX(-Math.PI / 2);
|
||
partGeo.translate(pX, thickness, pY);
|
||
geometries.push(partGeo);
|
||
}
|
||
|
||
// Логика скруглений для перегородок (Оставляем сплошными для прочности/эстетики)
|
||
if (p.rounded && radius > 1) {
|
||
const filletR = Math.min(radius, 5);
|
||
const filletShape = createConcaveFilletShape(filletR);
|
||
|
||
// Помощник для получения высоты соседа
|
||
const getNeighborHeight = (pos: number) => {
|
||
if (pos < 0.001 || pos > 0.999) return height;
|
||
const neighbor = partitions.find(n => {
|
||
if (n.axis === p.axis) return false;
|
||
const nMin = n.min ?? 0;
|
||
const nMax = n.max ?? 1;
|
||
if (Math.abs(n.offset - pos) > 0.002) return false;
|
||
return p.offset > nMin && p.offset < nMax;
|
||
});
|
||
return neighbor ? neighbor.height : 0;
|
||
};
|
||
|
||
const hStart = Math.min(p.height, getNeighborHeight(pMin));
|
||
const hEnd = Math.min(p.height, getNeighborHeight(pMax));
|
||
|
||
const addFillet = (x: number, y: number, rotY: number, h: number) => {
|
||
const hEff = Math.max(0.1, h - thickness);
|
||
if (hEff <= 1) return;
|
||
const geo = new THREE.ExtrudeGeometry(filletShape, { depth: hEff, bevelEnabled: false });
|
||
geo.rotateX(-Math.PI / 2);
|
||
geo.rotateY(rotY);
|
||
geo.translate(x, thickness, y);
|
||
geometries.push(geo);
|
||
};
|
||
|
||
const t = thickness / 2;
|
||
|
||
if (p.axis === 'x') {
|
||
const topY = (-innerDepth / 2) + (innerDepth * pMin);
|
||
const botY = (-innerDepth / 2) + (innerDepth * pMax);
|
||
|
||
addFillet(pX - t, topY, Math.PI, hStart);
|
||
addFillet(pX + t, topY, -Math.PI / 2, hStart);
|
||
addFillet(pX - t, botY, Math.PI / 2, hEnd);
|
||
addFillet(pX + t, botY, 0, hEnd);
|
||
} else {
|
||
const leftX = (-innerWidth / 2) + (innerWidth * pMin);
|
||
const rightX = (-innerWidth / 2) + (innerWidth * pMax);
|
||
|
||
addFillet(leftX, pY - t, 0, hStart);
|
||
addFillet(leftX, pY + t, -Math.PI / 2, hStart);
|
||
addFillet(rightX, pY - t, Math.PI / 2, hEnd);
|
||
addFillet(rightX, pY + t, Math.PI, hEnd);
|
||
}
|
||
}
|
||
});
|
||
|
||
const merged = mergeBufferGeometries(geometries);
|
||
if (merged) merged.computeVertexNormals();
|
||
return merged || new THREE.BoxGeometry(1, 1, 1);
|
||
};
|
||
|
||
export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => {
|
||
const exporter = new STLExporter();
|
||
const result = exporter.parse(mesh, { binary: true });
|
||
if (result instanceof DataView) return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
||
return result as string;
|
||
};
|
||
|
||
export const exportSTL = (mesh: THREE.Object3D, filename: string) => {
|
||
const result = generateSTL(mesh);
|
||
const blob = new Blob([result as any], { type: 'application/octet-stream' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = filename;
|
||
link.click();
|
||
}; |