Add perforation
This commit is contained in:
@@ -1,77 +1,77 @@
|
||||
import * as THREE from 'three';
|
||||
import { STLExporter, mergeBufferGeometries } from 'three-stdlib';
|
||||
import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types';
|
||||
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 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];
|
||||
const xPoints = [0, ...[...safeX].sort((a, b) => a - b), 1];
|
||||
const yPoints = [0, ...[...safeY].sort((a, b) => a - b), 1];
|
||||
|
||||
let partCounter = 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;
|
||||
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;
|
||||
|
||||
const rawX = xPoints[i] * config.drawer.width;
|
||||
const rawY = yPoints[j] * config.drawer.depth;
|
||||
const internalPartitions = safeParts[`${i}-${j}`] || [];
|
||||
if (rawW < 5 || rawD < 5) continue;
|
||||
|
||||
const realWidth = rawW - config.printerTolerance;
|
||||
const realDepth = rawD - config.printerTolerance;
|
||||
const realX = rawX + (config.printerTolerance / 2);
|
||||
const realY = rawY + (config.printerTolerance / 2);
|
||||
const rawX = xPoints[i] * config.drawer.width;
|
||||
const rawY = yPoints[j] * config.drawer.depth;
|
||||
const internalPartitions = safeParts[`${i}-${j}`] || [];
|
||||
|
||||
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++;
|
||||
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;
|
||||
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);
|
||||
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;
|
||||
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 => {
|
||||
@@ -83,122 +83,365 @@ const createConcaveFilletShape = (radius: number): THREE.Shape => {
|
||||
return shape;
|
||||
};
|
||||
|
||||
// New Helper: Create Perforated Plate (Vertical Wall)
|
||||
const createPerforatedPlate = (width: number, height: number, thickness: number, perf: PerforationConfig): 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);
|
||||
|
||||
// Hole Generation
|
||||
if (perf && perf.enabled && width > perf.size && height > perf.size) {
|
||||
const { shape: shapeType, size, gap } = perf;
|
||||
const step = size + gap;
|
||||
const startX = gap; // Margin
|
||||
const startY = gap; // Margin
|
||||
const endX = width - gap; // Margin
|
||||
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);
|
||||
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;
|
||||
|
||||
// Boundary check (approximate center check)
|
||||
if (x - r < 0 || x + r > width || y - r < 0 || y + r > height) continue;
|
||||
|
||||
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);
|
||||
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') {
|
||||
// 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 });
|
||||
// Extruded along Z. Wall is flat on XY.
|
||||
// We want "thickness" to be Z depth.
|
||||
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();
|
||||
// External Arc (from X-axis to Y-axis)
|
||||
shape.absarc(0, 0, radius, 0, Math.PI / 2, false);
|
||||
// Line to inner
|
||||
shape.lineTo(0, radius - thickness); // Assuming innerRadius = radius - thickness
|
||||
// Inner Arc (backwards)
|
||||
const innerRadius = Math.max(0.1, radius - thickness);
|
||||
shape.absarc(0, 0, innerRadius, Math.PI / 2, 0, true);
|
||||
// Close
|
||||
shape.lineTo(radius, 0);
|
||||
|
||||
// Extrude vertically (Height is Z for now, usually Extrude goes Z)
|
||||
const geo = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: false, curveSegments: 16 });
|
||||
// Rotate so height is along Y? No, Extrude defaults to Z depth.
|
||||
// We want the Profile on XZ plane extruded up Y?
|
||||
// Shape is on XY. Extrude is Z.
|
||||
// If shape is on XY (top view of corner), Extrude Z creates Height.
|
||||
// This matches standard logic if we rotate whole object later.
|
||||
return geo;
|
||||
};
|
||||
|
||||
export const createBinGeometry = (
|
||||
width: number, depth: number, height: number, thickness: number, radius: number = 0, partitions: Partition[] = []
|
||||
width: number,
|
||||
depth: number,
|
||||
height: number,
|
||||
thickness: number,
|
||||
radius: number = 0,
|
||||
partitions: Partition[] = [],
|
||||
perforation?: PerforationConfig
|
||||
): THREE.BufferGeometry => {
|
||||
const geometries: THREE.BufferGeometry[] = [];
|
||||
const geometries: THREE.BufferGeometry[] = [];
|
||||
|
||||
// ДНО И ВНЕШНИЕ СТЕНКИ
|
||||
const floorShape = createRoundedRectShape(width, depth, radius);
|
||||
const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false });
|
||||
floorGeo.rotateX(-Math.PI / 2);
|
||||
geometries.push(floorGeo);
|
||||
// ДНО (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);
|
||||
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
const innerDepth = depth - (2 * thickness);
|
||||
|
||||
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);
|
||||
if (innerWidth > 0.1 && innerDepth > 0.1) {
|
||||
const innerHole = createRoundedRectShape(innerWidth, innerDepth, innerRadius);
|
||||
outerShape.holes.push(innerHole);
|
||||
}
|
||||
|
||||
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ (СТРОГО ПО ДАННЫМ, БЕЗ SOLVER)
|
||||
partitions.forEach(p => {
|
||||
// Берем данные напрямую. Если в 2D нарисовано от 0.2 до 0.8, тут будет 0.2 до 0.8.
|
||||
const pMin = p.min ?? 0;
|
||||
const pMax = p.max ?? 1;
|
||||
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 {
|
||||
// --- 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);
|
||||
|
||||
if (pMax - pMin < 0.01) return;
|
||||
const straightW = width - 2 * effRadius;
|
||||
const straightD = depth - 2 * effRadius;
|
||||
|
||||
const lengthRatio = pMax - pMin;
|
||||
const midRatio = pMin + (lengthRatio / 2);
|
||||
// 1. Corners (4 pcs)
|
||||
if (effRadius > 0) {
|
||||
const cornerGeoBase = createCornerProfile(effRadius, thickness, wallHeight);
|
||||
|
||||
let pWidth = 0, pDepth = 0, pX = 0, pY = 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);
|
||||
}
|
||||
// 1. Stand Up: Extrusion Z -> Y. Shape moves to X(+)/Z(+).
|
||||
cornerGeoBase.rotateX(-Math.PI / 2);
|
||||
|
||||
const partShape = createRoundedRectShape(pWidth, pDepth, 0.1);
|
||||
const partGeo = new THREE.ExtrudeGeometry(partShape, { depth: p.height, bevelEnabled: false });
|
||||
partGeo.rotateX(-Math.PI / 2);
|
||||
partGeo.translate(pX, thickness, pY);
|
||||
geometries.push(partGeo);
|
||||
const positions = [
|
||||
{ x: width / 2 - effRadius, z: depth / 2 - effRadius, rot: -Math.PI / 2 }, // Front Right (X+, Z+) -> Needs (X+, Z+). Base is (X+, Z-). Rot -90 -> (Z+, X+)
|
||||
{ x: -(width / 2 - effRadius), z: depth / 2 - effRadius, rot: Math.PI }, // Front Left (X-, Z+) -> Needs (X-, Z+). Rot 180 -> (X-, Z+)
|
||||
{ x: -(width / 2 - effRadius), z: -(depth / 2 - effRadius), rot: Math.PI / 2 }, // Back Left (X-, Z-) -> Needs (X-, Z-). Rot 90 -> (Z-, X-) which is X-, Z-? No wait.
|
||||
// Rot 90 on (X+, Z-): X->Z, Z->-X. (X+, Z-) -> (-Z, -X) = (X-, Z-). Correct.
|
||||
{ x: width / 2 - effRadius, z: -(depth / 2 - effRadius), rot: 0 } // Back Right (X+, Z-) -> Matches Base.
|
||||
];
|
||||
|
||||
// СКРУГЛЕНИЯ (Fillets)
|
||||
if (p.rounded && radius > 1) {
|
||||
const filletR = Math.min(radius, 5);
|
||||
const filletShape = createConcaveFilletShape(filletR);
|
||||
positions.forEach(pos => {
|
||||
const c = cornerGeoBase.clone();
|
||||
c.rotateY(pos.rot);
|
||||
c.translate(pos.x, thickness, pos.z);
|
||||
geometries.push(c);
|
||||
});
|
||||
}
|
||||
|
||||
// Функция проверки высоты соседа (простая проверка на пересечение)
|
||||
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;
|
||||
};
|
||||
// 2. Straight Walls (4 pcs) - Centered on edges
|
||||
// Front/Back
|
||||
if (straightW > 0.1) {
|
||||
const wGeo = createPerforatedPlate(straightW, wallHeight, thickness, perforation);
|
||||
// Plate: 0..W in X, 0..H in Y, 0..Th in Z.
|
||||
// Center Horizontally:
|
||||
wGeo.translate(-straightW / 2, 0, 0);
|
||||
|
||||
const hStart = Math.min(p.height, getNeighborHeight(pMin));
|
||||
const hEnd = Math.min(p.height, getNeighborHeight(pMax));
|
||||
// Wall 1 (Back / Top? +Z):
|
||||
// Needs to be at Z = Depth/2.
|
||||
// Plate thickness is along Z (positive).
|
||||
// If we put it at Z = D/2 - thickness, it occupies [D/2 - th, D/2].
|
||||
// Inner face at D/2 - th. Outer face at D/2. Correct.
|
||||
const w1 = wGeo.clone();
|
||||
w1.translate(0, thickness, depth / 2 - thickness);
|
||||
geometries.push(w1);
|
||||
|
||||
const addFillet = (x: number, y: number, rotY: number, h: number) => {
|
||||
if (h <= 1) return;
|
||||
const geo = new THREE.ExtrudeGeometry(filletShape, { depth: h, bevelEnabled: false });
|
||||
geo.rotateX(-Math.PI / 2);
|
||||
geo.rotateY(rotY);
|
||||
geo.translate(x, thickness, y);
|
||||
geometries.push(geo);
|
||||
};
|
||||
// Wall 2 (Front / Bottom? -Z):
|
||||
// Needs to be at Z = -Depth/2.
|
||||
// Occupies [-D/2, -D/2 + th].
|
||||
// RotateY(180)?
|
||||
// Plate (X, Z-thick). Rot180 -> (-X, -Z-thick).
|
||||
// If original in [-W/2, W/2]x[0,th].
|
||||
// Rot180 -> [W/2, -W/2]x[0,-th].
|
||||
// Translate to Z = -(Depth/2 - thickness). -> [-th - (D/2 - th)] = -D/2.
|
||||
// Wait. [-th - D/2 + th] = -D/2. Correct?
|
||||
// Let's just translate manually without rotation for robustness, assuming pattern symmetric or acceptable.
|
||||
|
||||
const t = thickness / 2;
|
||||
const w2 = wGeo.clone();
|
||||
// Rotate to face out?
|
||||
w2.rotateY(Math.PI);
|
||||
// After RotY(180): Z becomes negative. Range [-th, 0].
|
||||
// We want range [-D/2, -D/2 + th].
|
||||
// So translate Z by -D/2 + th.
|
||||
w2.translate(0, thickness, -(depth / 2 - thickness));
|
||||
geometries.push(w2);
|
||||
}
|
||||
|
||||
if (p.axis === 'x') {
|
||||
const topY = (-innerDepth / 2) + (innerDepth * pMin);
|
||||
const botY = (-innerDepth / 2) + (innerDepth * pMax);
|
||||
// Left/Right
|
||||
if (straightD > 0.1) {
|
||||
const dGeo = createPerforatedPlate(straightD, wallHeight, thickness, perforation);
|
||||
dGeo.translate(-straightD / 2, 0, 0);
|
||||
|
||||
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);
|
||||
// Wall 3 (Right? +X).
|
||||
// RotateY(-90). X -> Z, Z -> -X.
|
||||
// Plate Z[0, th] -> X[-th, 0].
|
||||
// We want X [W/2 - th, W/2].
|
||||
// So Translate X by W/2.
|
||||
const w3 = dGeo.clone();
|
||||
w3.rotateY(-Math.PI / 2);
|
||||
w3.translate(width / 2, thickness, 0);
|
||||
geometries.push(w3);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Wall 4 (Left? -X).
|
||||
// RotateY(90). X -> -Z, Z -> X.
|
||||
// Plate Z[0, th] -> X[0, th].
|
||||
// We want X [-W/2, -W/2 + th].
|
||||
// Translate X by -W/2.
|
||||
const w4 = dGeo.clone();
|
||||
w4.rotateY(Math.PI / 2);
|
||||
w4.translate(-(width / 2), thickness, 0);
|
||||
geometries.push(w4);
|
||||
}
|
||||
}
|
||||
|
||||
const merged = mergeBufferGeometries(geometries);
|
||||
if (merged) merged.computeVertexNormals();
|
||||
return merged || new THREE.BoxGeometry(1, 1, 1);
|
||||
|
||||
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ
|
||||
const innerWidth = width - (2 * thickness);
|
||||
const innerDepth = depth - (2 * thickness); // Approximate usable space logic
|
||||
|
||||
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);
|
||||
|
||||
// --- 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
|
||||
|
||||
if (usePerf) {
|
||||
// Calculate exact geometry
|
||||
let pLen = 0;
|
||||
|
||||
if (p.axis === 'x') {
|
||||
// Axis X -> Divider runs along Y (Depth)
|
||||
pLen = lengthRatio * innerDepth;
|
||||
pX = (-innerWidth / 2) + (innerWidth * p.offset);
|
||||
pY = (-innerDepth / 2) + (innerDepth * midRatio); // Center of partition
|
||||
|
||||
// Create Plate (Length, Height)
|
||||
const plate = createPerforatedPlate(pLen, p.height, thickness, perforation!);
|
||||
plate.translate(-pLen / 2, 0, 0); // Center X
|
||||
|
||||
// Rotate to align with Depth (along Z)
|
||||
// Plate X -> Z
|
||||
plate.rotateY(-Math.PI / 2);
|
||||
|
||||
// Position
|
||||
// Plate is now vertical Z-aligned. Thickness along X.
|
||||
plate.translate(pX + thickness / 2, thickness, pY);
|
||||
geometries.push(plate);
|
||||
|
||||
} else {
|
||||
// Axis Y -> Divider runs along X (Width)
|
||||
pLen = lengthRatio * innerWidth;
|
||||
pX = (-innerWidth / 2) + (innerWidth * midRatio);
|
||||
pY = (-innerDepth / 2) + (innerDepth * p.offset);
|
||||
|
||||
const plate = createPerforatedPlate(pLen, p.height, thickness, perforation!);
|
||||
plate.translate(-pLen / 2, 0, 0); // Center 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.
|
||||
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;
|
||||
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: p.height, bevelEnabled: false });
|
||||
partGeo.rotateX(-Math.PI / 2);
|
||||
partGeo.translate(pX, thickness, pY);
|
||||
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 => {
|
||||
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) => {
|
||||
if (h <= 1) return;
|
||||
const geo = new THREE.ExtrudeGeometry(filletShape, { depth: h, 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 => {
|
||||
@@ -210,7 +453,7 @@ export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => {
|
||||
|
||||
export const exportSTL = (mesh: THREE.Object3D, filename: string) => {
|
||||
const result = generateSTL(mesh);
|
||||
const blob = new Blob([result], { type: 'application/octet-stream' });
|
||||
const blob = new Blob([result as any], { type: 'application/octet-stream' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
|
||||
Reference in New Issue
Block a user