This commit is contained in:
Халимов Рустам
2026-01-12 23:21:07 +03:00
parent aaff0d06df
commit 34543d07c5

View File

@@ -84,7 +84,15 @@ const createConcaveFilletShape = (radius: number): THREE.Shape => {
};
// New Helper: Create Perforated Plate (Vertical Wall)
const createPerforatedPlate = (width: number, height: number, thickness: number, perf: PerforationConfig): THREE.BufferGeometry => {
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);
@@ -96,9 +104,10 @@ const createPerforatedPlate = (width: number, height: number, thickness: number,
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
// 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
@@ -111,8 +120,22 @@ const createPerforatedPlate = (width: number, height: number, thickness: number,
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;
// 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.
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)
return true;
});
if (inExclusion) continue;
if (shapeType === 'circle') {
hole.absarc(x, y, r, 0, Math.PI * 2, true);
@@ -143,7 +166,7 @@ const createPerforatedPlate = (width: number, height: number, thickness: number,
}
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
// Extruded along Z. Wall is flat on XY.
// Extruded along Z. Wall is flat on XY.
// We want "thickness" to be Z depth.
return geo;
};
@@ -152,23 +175,29 @@ const createPerforatedPlate = (width: number, height: number, thickness: number,
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)
// 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
// 1. Start at Outer Start
shape.moveTo(radius, 0);
// 2. Outer Arc (CCW) -> To (0, radius)
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);
// 3. Line to Inner End (0, innerRadius)
shape.lineTo(0, innerRadius);
// 4. Inner Arc (CW) -> To (innerRadius, 0)
shape.absarc(0, 0, innerRadius, Math.PI / 2, 0, true);
// Close
// 5. Close loop
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.
// Extrude
// curveSegments 32 for smoothness
const geo = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: false, curveSegments: 32 });
return geo;
};
@@ -223,83 +252,193 @@ export const createBinGeometry = (
// 1. Stand Up: Extrusion Z -> Y. Shape moves to 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).
// Q4 (0) -> Q1 (-90) -> Q2 (-180/180) -> Q3 (-270/+90).
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.
// Back Right (+X, +Z). Q1.
// Rot -90 (-PI/2).
{ x: width / 2 - effRadius, z: depth / 2 - effRadius, rot: -Math.PI / 2 },
// Back Left (-X, +Z). Q2.
// Rot 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: -(width / 2 - effRadius), z: -(depth / 2 - effRadius), rot: Math.PI / 2 },
// Front Right (+X, -Z). Q4.
// Rot 0.
{ x: width / 2 - effRadius, z: -(depth / 2 - effRadius), rot: 0 }
];
positions.forEach(pos => {
const c = cornerGeoBase.clone();
c.rotateY(pos.rot);
c.translate(pos.x, thickness, pos.z);
geometries.push(c);
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
geometries.push(corner);
});
}
// 2. Straight Walls (4 pcs) - Centered on edges
// 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
if (p.axis === 'x') {
// Runs Depth-wise (Y axis in Layout).
// Intersects Front and Back walls (which are Width-wise plates).
// X pos on Front/Back walls:
// Partition X global: (-effectiveInnerW / 2) + (effectiveInnerW * p.offset)
// Wall plate local X: 0 to straightW.
// Wall Global X range: [-straightW/2, straightW/2].
// Map partition global X to wall local X:
// localX = partGlobalX - (-straightW/2) = partGlobalX + straightW/2.
// Note: straightW = width - 2*effRadius.
// effectiveInnerW = width - 2*thickness.
// These coordinate spaces are slightly different if radius > thickness.
// We should use the ACTUAL spatial intersection.
const partGlobalX = (-effectiveInnerW / 2) + (effectiveInnerW * p.offset);
const sW = width - 2 * effRadius;
const localXOnWall = partGlobalX + sW / 2;
// Check if partition intersects the wall's X range
if (localXOnWall + thickness / 2 > 0 && localXOnWall - thickness / 2 < sW) {
// Check if it touches Back (min approx 0)
// Mapping check:
// If p.min (0) is at -Depth/2. And p.max (1) is at +Depth/2.
// Wall 1 is at +Depth/2. So Wall 1 contacts p.max.
// Wall 2 is at -Depth/2. So Wall 2 contacts p.min.
// Let's store EXCLUSIONS per contact point.
// p.min touches Wall 2. So add to 'exclusionsWall2'.
// p.max touches Wall 1. So add to 'exclusionsWall1'.
// I used names 'exclusionsBack' and 'exclusionsFront'.
// Let's adhere to names:
// 'Back' typically means +Z (User side) or -Z?
// In 3D graphics: Camera at +Z looking -Z.
// Objects at +Z are "Front" (Near). Objects at -Z are "Background" (Far).
// So Wall 1 (+Z) is Front. Wall 2 (-Z) is Back.
// 'p.min' (0) -> -Z -> Back/Far Wall (Wall 2).
// 'p.max' (1) -> +Z -> Front/Near Wall (Wall 1).
// So:
// p.min contacts Wall 2 (Back).
// p.max contacts Wall 1 (Front).
// Array 'exclusionsBack' should store contacts for Wall 2.
// Array 'exclusionsFront' should store contacts for Wall 1.
if ((p.min ?? 0) < 0.01) {
exclusionsBack.push({ start: localXOnWall - thickness / 2, end: localXOnWall + thickness / 2, yMax: hEff });
}
// Check if it touches Front (max approx 1)
if ((p.max ?? 1) > 0.99) {
exclusionsFront.push({ start: localXOnWall - thickness / 2, end: localXOnWall + thickness / 2, yMax: hEff });
}
}
} else { // p.axis === 'y'
// Runs Width-wise (X axis in Layout).
// Intersects Left and Right walls (Length-wise plates).
const partGlobalZ = (-effectiveInnerD / 2) + (effectiveInnerD * p.offset);
const sD = depth - 2 * effRadius;
const localXOnWall = partGlobalZ + sD / 2;
// Check if partition intersects the wall's X range (which maps to depth)
if (localXOnWall + thickness / 2 > 0 && localXOnWall - thickness / 2 < sD) {
// Check if it touches Left (min approx 0)
if ((p.min ?? 0) < 0.01) {
exclusionsLeft.push({ start: localXOnWall - thickness / 2, end: localXOnWall + thickness / 2, yMax: hEff });
}
// Check if it touches Right (max approx 1)
if ((p.max ?? 1) > 0.99) {
exclusionsRight.push({ start: localXOnWall - thickness / 2, end: localXOnWall + thickness / 2, yMax: hEff });
}
}
}
});
// 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);
// 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();
// Wall 1 (at +Z). This is "Front" (Near). Contacts p.max.
// Uses 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 (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.
// 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.
const exclusionsBackMapped = exclusionsBack.map(e => ({
start: straightW - e.end,
end: straightW - e.start,
yMax: e.yMax
}));
const w2 = wGeo.clone();
// Rotate to face out?
const wGeoBack = createPerforatedPlate(straightW, wallHeight, thickness, perforation, 0, 0, exclusionsBackMapped);
wGeoBack.translate(-straightW / 2, 0, 0);
const w2 = wGeoBack.clone();
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);
}
// Left/Right
if (straightD > 0.1) {
const dGeo = createPerforatedPlate(straightD, wallHeight, thickness, perforation);
dGeo.translate(-straightD / 2, 0, 0);
// 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);
// 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.
const wGeoRight = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsRight);
wGeoRight.translate(-straightD / 2, 0, 0); // Center X of plate
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
geometries.push(w3);
// 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();
// 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.
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); // Center X of plate
const w4 = wGeoLeft.clone();
w4.rotateY(Math.PI / 2);
w4.translate(-(width / 2), thickness, 0);
w4.translate(-width / 2, thickness, 0); // Position at X=-width/2
geometries.push(w4);
}
}
@@ -336,8 +475,52 @@ export const createBinGeometry = (
pX = (-innerWidth / 2) + (innerWidth * p.offset);
pY = (-innerDepth / 2) + (innerDepth * midRatio); // Center of partition
// Create Plate (Length, Height)
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!);
// 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).
partitions.forEach(n => {
if (n.axis !== 'y') return; // Only orthogonal partitions intersect
// Check if n intersects p
// Intersection Y location:
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.
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.
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
// Rotate to align with Depth (along Z)
@@ -355,7 +538,33 @@ export const createBinGeometry = (
pX = (-innerWidth / 2) + (innerWidth * midRatio);
pY = (-innerDepth / 2) + (innerDepth * p.offset);
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!);
// 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).
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 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) {
// Intersection confirmed
// local = global - pX + pLen/2
const nHeight = Math.max(0.1, n.height - thickness); // INTERSECTING PARTITION HEIGHT
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); // Center X
// Already aligned with X. Thickness along Z.