From 34543d07c548eea09223b57558253344ab18581c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Mon, 12 Jan 2026 23:21:07 +0300 Subject: [PATCH] Fix --- src/services/geometryGenerator.ts | 357 +++++++++++++++++++++++------- 1 file changed, 283 insertions(+), 74 deletions(-) diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index a0a29af..afafb14 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -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.