This commit is contained in:
Халимов Рустам
2026-01-11 23:40:10 +03:00
parent 43a54c99a6
commit 96bf976b64
2 changed files with 31 additions and 69 deletions
+14 -12
View File
@@ -59,17 +59,17 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
}; };
const selectedData = getSelectedPartition(); const selectedData = getSelectedPartition();
// --- RAYCASTING (Надежный поиск коробки) --- // --- RAYCASTING (Поиск коробки для НОВОЙ стенки) ---
// Находит ближайшие стенки во всех 4 направлениях // Мы ищем ближайшие препятствия, чтобы определить границы новой стенки
const getCursorBox = (lx: number, ly: number, parts: Partition[]) => { const getCursorBox = (lx: number, ly: number, parts: Partition[]) => {
let minX = 0, maxX = 1; let minX = 0, maxX = 1;
let minY = 0, maxY = 1; let minY = 0, maxY = 1;
parts.forEach(p => { parts.forEach(p => {
// Используем сохраненные границы (они теперь достоверны) // Используем сохраненные границы (честные)
const pMin = p.min ?? 0; const pMin = p.min ?? 0;
const pMax = p.max ?? 1; const pMax = p.max ?? 1;
const EPS = 0.005; // Допуск на попадание const EPS = 0.005; // Допуск
if (p.axis === 'x') { if (p.axis === 'x') {
// Вертикальная стенка. Перекрывает ли она наш Y? // Вертикальная стенка. Перекрывает ли она наш Y?
@@ -90,7 +90,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
return { minX, maxX, minY, maxY }; return { minX, maxX, minY, maxY };
}; };
// Поиск соседей для размеров (Та же логика, что Raycasting) // Поиск соседей для размеров (Используем сохраненные данные)
const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[]) => { const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[]) => {
let min = 0; let min = 0;
let max = 1; let max = 1;
@@ -232,7 +232,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
if (found) { if (found) {
setHoveredPartition({ id: found.id, cellKey: key }); setHoveredPartition({ id: found.id, cellKey: key });
} else { } else {
// --- FIND BOX --- // --- FIND BOX USING RAYCASTING ---
const box = getCursorBox(lx, ly, parts); const box = getCursorBox(lx, ly, parts);
const boxW = (box.maxX - box.minX) * realCellW; const boxW = (box.maxX - box.minX) * realCellW;
@@ -244,11 +244,12 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
// Override if near edges // Override if near edges
const relL = (lx - box.minX) / (box.maxX - box.minX); const relL = (lx - box.minX) / (box.maxX - box.minX);
const relT = (ly - box.minY) / (box.maxY - box.minY); const relT = (ly - box.minY) / (box.maxY - box.minY);
const THRESHOLD = 0.2; const THRESHOLD = 0.2; // 20% zone near edges
if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x'; // Near vertical edge -> vertical wall if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x';
else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; // Near horiz edge -> horizontal wall else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y';
// Validate space
const valid = (newAxis === 'x' && (box.maxX - box.minX) > 0.05) || (newAxis === 'y' && (box.maxY - box.minY) > 0.05); const valid = (newAxis === 'x' && (box.maxX - box.minX) > 0.05) || (newAxis === 'y' && (box.maxY - box.minY) > 0.05);
if (valid) { if (valid) {
@@ -298,7 +299,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
} }
}; };
// --- ДОБАВЛЕНО: Очистка при выходе мыши --- // --- HANDLER: Clear Phantom on Leave ---
const handleMouseLeave = () => { const handleMouseLeave = () => {
setDragging(null); setDragging(null);
setPhantomMainAxis(null); setPhantomMainAxis(null);
@@ -444,8 +445,9 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseUp={() => setDragging(null)} onMouseUp={() => setDragging(null)}
onMouseLeave={handleMouseLeave} // ДОБАВЛЕН ОБРАБОТЧИК onMouseLeave={handleMouseLeave} // ДОБАВЛЕНО СЮДА
onContextMenu={(e) => e.preventDefault()}> onContextMenu={(e) => e.preventDefault()}
>
<defs><pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse"><path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(255,255,255,0.03)" strokeWidth="1"/></pattern></defs> <defs><pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse"><path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(255,255,255,0.03)" strokeWidth="1"/></pattern></defs>
<rect width="100%" height="100%" fill="url(#grid)" /> <rect width="100%" height="100%" fill="url(#grid)" />
{renderCellsAndPartitions()} {renderCellsAndPartitions()}
+14 -54
View File
@@ -2,45 +2,6 @@ import * as THREE from 'three';
import { STLExporter, mergeBufferGeometries } from 'three-stdlib'; import { STLExporter, mergeBufferGeometries } from 'three-stdlib';
import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types';
type Limits = { min: number; max: number };
type LimitMap = Record<string, Limits>;
// --- SOLVER: Идентичен тому, что в LayoutStep.tsx ---
// Гарантирует, что 3D модель будет выглядеть точно так же, как 2D макет
const solveWallLimits = (partitions: Partition[]): LimitMap => {
const limits: LimitMap = {};
// 1. Сброс границ
partitions.forEach(p => { limits[p.id] = { min: 0, max: 1 }; });
// 2. Итеративное решение коллизий (4 прохода для надежности)
for (let pass = 0; pass < 4; pass++) {
partitions.forEach(target => {
let newMin = 0;
let newMax = 1;
const center = target.offset;
partitions.forEach(obstacle => {
if (target.id === obstacle.id || target.axis === obstacle.axis) return;
// Берем актуальные границы препятствия
const obsMin = limits[obstacle.id].min;
const obsMax = limits[obstacle.id].max;
// ВАЖНО: Используем тот же допуск (EPSILON), что и в 2D редакторе
const EPS = 0.002;
// Проверяем пересечение
if (target.offset >= obsMin - EPS && target.offset <= obsMax + EPS) {
if (obstacle.offset < center) newMin = Math.max(newMin, obstacle.offset);
else if (obstacle.offset > center) newMax = Math.min(newMax, obstacle.offset);
}
});
limits[target.id] = { min: newMin, max: newMax };
});
}
return limits;
};
export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => { export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => {
const parts: GeneratedPart[] = []; const parts: GeneratedPart[] = [];
const safeX = Array.isArray(splits?.x) ? splits.x : []; const safeX = Array.isArray(splits?.x) ? splits.x : [];
@@ -61,7 +22,6 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat
const rawX = xPoints[i] * config.drawer.width; const rawX = xPoints[i] * config.drawer.width;
const rawY = yPoints[j] * config.drawer.depth; const rawY = yPoints[j] * config.drawer.depth;
const internalPartitions = safeParts[`${i}-${j}`] || []; const internalPartitions = safeParts[`${i}-${j}`] || [];
const realWidth = rawW - config.printerTolerance; const realWidth = rawW - config.printerTolerance;
@@ -150,12 +110,11 @@ export const createBinGeometry = (
wallGeo.translate(0, thickness, 0); wallGeo.translate(0, thickness, 0);
geometries.push(wallGeo); geometries.push(wallGeo);
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ (С SOLVER'ом) // ВНУТРЕННИЕ ПЕРЕГОРОДКИ (СТРОГО ПО ДАННЫМ, БЕЗ SOLVER)
const limitMap = solveWallLimits(partitions);
partitions.forEach(p => { partitions.forEach(p => {
// Используем рассчитанные границы, а не старые сохраненные // Берем данные напрямую. Если в 2D нарисовано от 0.2 до 0.8, тут будет 0.2 до 0.8.
const { min: pMin, max: pMax } = limitMap[p.id]; const pMin = p.min ?? 0;
const pMax = p.max ?? 1;
if (pMax - pMin < 0.01) return; if (pMax - pMin < 0.01) return;
@@ -182,20 +141,23 @@ export const createBinGeometry = (
partGeo.translate(pX, thickness, pY); partGeo.translate(pX, thickness, pY);
geometries.push(partGeo); geometries.push(partGeo);
// СКРУГЛЕНИЯ // СКРУГЛЕНИЯ (Fillets)
if (p.rounded && radius > 1) { if (p.rounded && radius > 1) {
const filletR = Math.min(radius, 5); const filletR = Math.min(radius, 5);
const filletShape = createConcaveFilletShape(filletR); const filletShape = createConcaveFilletShape(filletR);
// Функция высоты соседа (чтобы скругление не висело в воздухе) // Функция проверки высоты соседа (простая проверка на пересечение)
const getNeighborHeight = (pos: number) => { const getNeighborHeight = (pos: number) => {
if (pos < 0.001 || pos > 0.999) return height; // Край ящика = полная высота if (pos < 0.001 || pos > 0.999) return height; // Край ящика
const neighbor = partitions.find(n => { const neighbor = partitions.find(n => {
if (n.axis === p.axis) return false; if (n.axis === p.axis) return false; // Перпендикуляр
// Проверка на пересечение координат const nMin = n.min ?? 0;
const nLims = limitMap[n.id]; // Берем актуальные границы соседа const nMax = n.max ?? 1;
return Math.abs(n.offset - pos) < 0.002 && p.offset >= nLims.min && p.offset <= nLims.max; // Совпадает ли позиция?
if (Math.abs(n.offset - pos) > 0.002) return false;
// Перекрывает ли?
return p.offset > nMin && p.offset < nMax;
}); });
return neighbor ? neighbor.height : 0; return neighbor ? neighbor.height : 0;
}; };
@@ -220,7 +182,6 @@ export const createBinGeometry = (
addFillet(pX - t, topY, Math.PI, hStart); addFillet(pX - t, topY, Math.PI, hStart);
addFillet(pX + t, topY, -Math.PI / 2, hStart); addFillet(pX + t, topY, -Math.PI / 2, hStart);
addFillet(pX - t, botY, Math.PI / 2, hEnd); addFillet(pX - t, botY, Math.PI / 2, hEnd);
addFillet(pX + t, botY, 0, hEnd); addFillet(pX + t, botY, 0, hEnd);
} else { } else {
@@ -229,7 +190,6 @@ export const createBinGeometry = (
addFillet(leftX, pY - t, 0, hStart); addFillet(leftX, pY - t, 0, hStart);
addFillet(leftX, pY + t, -Math.PI / 2, hStart); addFillet(leftX, pY + t, -Math.PI / 2, hStart);
addFillet(rightX, pY - t, Math.PI / 2, hEnd); addFillet(rightX, pY - t, Math.PI / 2, hEnd);
addFillet(rightX, pY + t, Math.PI, hEnd); addFillet(rightX, pY + t, Math.PI, hEnd);
} }