This commit is contained in:
Халимов Рустам
2026-01-11 15:40:07 +03:00
parent 272b2b6f77
commit 56183b3ada

View File

@@ -50,7 +50,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
const sortedX = useMemo(() => [0, ...safeX, 1].sort((a, b) => a - b), [safeX]);
const sortedY = useMemo(() => [0, ...safeY, 1].sort((a, b) => a - b), [safeY]);
// -- HELPERS --
// -- HELPER: Get Selected --
const getSelectedPartition = () => {
if (!selectedPartitionId) return null;
for (const key in safePartitions) {
@@ -61,12 +61,11 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
};
const selectedData = getSelectedPartition();
// --- SOLVER (Пересчет границ) ---
// --- SOLVER ---
const solveWallLimits = (parts: Partition[]): LimitMap => {
const limits: LimitMap = {};
parts.forEach(p => { limits[p.id] = { min: 0, max: 1 }; });
// 4 прохода для сложных вложений
for (let pass = 0; pass < 4; pass++) {
parts.forEach(target => {
let newMin = 0;
@@ -79,9 +78,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
const obsMin = limits[obstacle.id].min;
const obsMax = limits[obstacle.id].max;
// Допуск (epsilon) чуть больше, чтобы надежнее ловить пересечения
const EPS = 0.005;
if (target.offset >= obsMin - EPS && target.offset <= obsMax + EPS) {
if (target.offset >= obsMin - 0.001 && target.offset <= obsMax + 0.001) {
if (obstacle.offset < center) newMin = Math.max(newMin, obstacle.offset);
else if (obstacle.offset > center) newMax = Math.min(newMax, obstacle.offset);
}
@@ -92,7 +89,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
return limits;
};
// --- RAYCASTING (Поиск коробки под курсором) ---
// --- RAYCASTING ---
const getCursorBox = (lx: number, ly: number, parts: Partition[], limitMap: LimitMap) => {
let minX = 0, maxX = 1;
let minY = 0, maxY = 1;
@@ -101,17 +98,14 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
const { min: pMin, max: pMax } = limitMap[p.id];
if (pMax - pMin < 0.001) return;
// Используем небольшой допуск, чтобы мышь точно "видела" стенку
const EPS = 0.002;
if (p.axis === 'x') {
// Вертикальная преграда
if (ly >= pMin - EPS && ly <= pMax + EPS) {
if (p.offset < lx) minX = Math.max(minX, p.offset);
if (p.offset > lx) maxX = Math.min(maxX, p.offset);
}
} else {
// Горизонтальная преграда
if (lx >= pMin - EPS && lx <= pMax + EPS) {
if (p.offset < ly) minY = Math.max(minY, p.offset);
if (p.offset > ly) maxY = Math.min(maxY, p.offset);
@@ -220,7 +214,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
setPhantomPartition(null);
setHoveredCell(null);
// 1. Находим ячейку
// 1. Find Cell
let cellIdx = null;
for (let i = 0; i < sortedX.length - 1; i++) {
if (nx >= sortedX[i] && nx <= sortedX[i+1]) {
@@ -244,7 +238,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
const lx = (nx - cx1) / cw;
const ly = (ny - cy1) / ch;
// Реальные размеры ячейки в мм (для корректного расчета дистанции мыши)
// Реальные размеры в мм (для интуитивности)
const realCellW = cw * drawerW;
const realCellH = ch * drawerD;
@@ -262,34 +256,38 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
if (found) {
setHoveredPartition({ id: found.id, cellKey: key });
} else {
// Вычисляем бокс под курсором
// --- ЛОГИКА ФАНТОМА ---
const box = getCursorBox(lx, ly, parts, limitMap);
// --- ВАЖНО: Расчет дистанции с учетом РЕАЛЬНОГО размера ячейки (мм) ---
// Ранее считалось в относительных 0-1, что ломало логику в узких/широких ячейках
const distL = (lx - box.minX) * realCellW;
const distR = (box.maxX - lx) * realCellW;
const distT = (ly - box.minY) * realCellH;
const distB = (box.maxY - ly) * realCellH;
const distL = lx - box.minX; const distR = box.maxX - lx;
const distT = ly - box.minY; const distB = box.maxY - ly;
// Выбираем ось, к которой мы БЛИЖЕ ВИЗУАЛЬНО
const minD = Math.min(distL, distR, distT, distB);
// Если мы близко к бокам -> ставим вертикальную (x)
// Если близко к верху/низу -> ставим горизонтальную (y)
let newAxis: Axis = (minD === distL || minD === distR) ? 'x' : 'y';
// Но если мы в центре, выбираем ось, которая делит длинную сторону
// Размеры "комнаты" под курсором
const boxW = (box.maxX - box.minX) * realCellW;
const boxH = (box.maxY - box.minY) * realCellH;
// Если мы не "прилипли" к краю (находимся далеко от всего > 20мм), то делим длинную сторону
if (minD > 20) {
newAxis = boxW > boxH ? 'x' : 'y';
// 1. По умолчанию делим длинную сторону (чтобы ячейки стремились к квадрату)
let newAxis: Axis = boxW > boxH ? 'x' : 'y';
// 2. Но если мы близко к краю (менее 15% ширины/высоты),
// значит пользователь хочет отрезать узкую полоску параллельно этому краю.
const edgeThreshold = 0.15; // 15%
const relL = lx - box.minX;
const relT = ly - box.minY;
const relW = box.maxX - box.minX;
const relH = box.maxY - box.minY;
// Если близко к бокам -> ставим вертикальную (X)
if (relL / relW < edgeThreshold || (relW - relL) / relW < edgeThreshold) {
newAxis = 'x';
}
// Если близко к верху/низу -> ставим горизонтальную (Y)
else if (relT / relH < edgeThreshold || (relH - relT) / relH < edgeThreshold) {
newAxis = 'y';
}
// Финальная проверка, есть ли место для стенки (минимум 10% от размера коробки)
if ((newAxis === 'y' && (box.maxY - box.minY) > 0.1) || (newAxis === 'x' && (box.maxX - box.minX) > 0.1)) {
// Проверяем, есть ли место (>10% размера)
if ((newAxis === 'y' && relH > 0.1) || (newAxis === 'x' && relW > 0.1)) {
const offset = newAxis === 'x' ? lx : ly;
const min = newAxis === 'x' ? box.minY : box.minX;
const max = newAxis === 'x' ? box.maxY : box.maxX;
@@ -355,10 +353,8 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
const realW = (x2 - x1) * drawerW;
const realD = (y2 - y1) * drawerD;
// ВАЖНО: Используем solver для отрисовки
const limitMap = solveWallLimits(parts);
// Label
if (parts.length === 0) {
const labelX = cellX + cellW / 2;
const labelY = cellY + cellH / 2;
@@ -460,27 +456,22 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
};
return (
<div className="bg-slate-900 p-4 rounded-xl shadow-lg border border-slate-800 h-full flex flex-col relative overflow-hidden">
<div className="flex justify-between items-center mb-2 z-20 shrink-0">
<h2 className="text-lg font-bold flex items-center gap-2 text-primary"><Grid size={20} /> 2. Редактор макета</h2>
<div className="flex bg-slate-800 p-1 rounded-lg border border-slate-700">
<button onClick={() => { setMode('lines'); setSelectedPartitionId(null); }} className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${mode === 'lines' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200'}`}><Move size={14}/> Границы</button>
<button onClick={() => setMode('cells')} className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${mode === 'cells' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200'}`}><Grid size={14}/> Внутри ячеек</button>
</div>
<button onClick={() => onChange({ x: [], y: [], partitions: {} })} className="px-3 py-1.5 text-xs text-red-400 border border-slate-700 rounded hover:bg-slate-800 transition-colors flex items-center gap-1"><RotateCcw size={14} /> Сброс</button>
</div>
<div className="flex w-full h-full bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl">
{/* LEFT: WORKSPACE */}
<div className="flex-1 flex flex-col relative min-w-0">
{/* TOOLBAR */}
<div className="flex justify-between items-center p-3 border-b border-slate-800 bg-slate-900 z-10 shrink-0">
<h2 className="text-sm font-bold flex items-center gap-2 text-primary"><Grid size={18} /> Редактор</h2>
<div className="flex bg-slate-800 p-1 rounded-lg border border-slate-700">
<button onClick={() => { setMode('lines'); setSelectedPartitionId(null); }} className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${mode === 'lines' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200'}`}><Move size={14}/> Границы</button>
<button onClick={() => setMode('cells')} className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${mode === 'cells' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200'}`}><Grid size={14}/> Внутри</button>
</div>
<button onClick={() => onChange({ x: [], y: [], partitions: {} })} className="px-3 py-1.5 text-xs text-red-400 border border-slate-700 rounded hover:bg-slate-800 transition-colors flex items-center gap-1"><RotateCcw size={14} /> Сброс</button>
</div>
<div className="bg-slate-800/50 rounded-lg px-3 py-2 mb-2 flex items-center gap-3 text-[11px] text-gray-300 border border-slate-700/50 shrink-0">
<MousePointer2 size={14} className="text-primary" />
{mode === 'lines' ? (
<div className="flex gap-3"><span><b className="text-blue-400">ЛКМ:</b> Линия</span><span><b className="text-red-400">ПКМ:</b> Удалить</span></div>
) : (
<div className="flex gap-3"><span><b className="text-green-400">ЛКМ в ячейке:</b> Стенка</span><span><b className="text-purple-400">Драг:</b> Двигать</span><span><b className="text-red-400">2КМ:</b> Удалить</span></div>
)}
</div>
<div className="flex-1 bg-slate-800/30 rounded-lg flex flex-col items-center justify-center relative overflow-hidden border border-slate-700/50 min-h-0 w-full">
<div className="relative w-full h-full flex items-center justify-center p-4">
{/* CANVAS */}
<div className="flex-1 bg-slate-800/30 flex items-center justify-center p-4 overflow-hidden relative">
<div className="relative shadow-2xl bg-[#1e293b] border border-slate-600 rounded-sm overflow-hidden" style={{ width: aspectRatio > 1 ? 'auto' : '100%', height: aspectRatio > 1 ? '100%' : 'auto', aspectRatio: `${1/aspectRatio}`, maxHeight: '100%', maxWidth: '100%', cursor: mode === 'lines' ? (dragging ? 'grabbing' : hoveredMainSplit ? 'col-resize' : 'crosshair') : (dragging ? 'grabbing' : hoveredPartition ? 'grab' : hoveredCell ? 'crosshair' : 'default') }}>
<svg ref={svgRef} viewBox={`0 0 ${viewBoxW} ${viewBoxH}`} className="w-full h-full touch-none block" onMouseMove={handleMouseMove} onMouseDown={handleMouseDown} onMouseUp={() => setDragging(null)} onMouseLeave={() => setDragging(null)} 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>
@@ -503,19 +494,27 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
)}
</svg>
</div>
</div>
</div>
</div>
{mode === 'cells' && selectedData && (
<div className="absolute top-0 right-0 bottom-0 w-72 bg-slate-900 border-l border-slate-700 p-4 shadow-2xl flex flex-col z-30 animate-in slide-in-from-right duration-200">
<div className="flex justify-between items-center mb-6"><h3 className="text-sm font-bold text-white flex items-center gap-2">Настройки стенки</h3><button onClick={() => setSelectedPartitionId(null)} className="text-gray-400 hover:text-white"><X size={20}/></button></div>
<div className="bg-slate-800 p-4 rounded border border-slate-700 space-y-6">
<div><div className="flex justify-between text-xs text-gray-300 mb-2"><span>Высота</span> <span className="font-mono bg-slate-900 px-1.5 py-0.5 rounded text-xs">{selectedData.part.height} мм</span></div><input type="range" min="5" max={config.drawer.height || 100} step="1" value={selectedData.part.height} onChange={(e) => updatePartition(selectedData.key, selectedData.part.id, { height: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500"/></div>
<div className="flex items-center justify-between"><label htmlFor="rounded-check" className="text-xs text-gray-300 cursor-pointer select-none">Скруглить края</label><input type="checkbox" id="rounded-check" checked={selectedData.part.rounded} onChange={(e) => updatePartition(selectedData.key, selectedData.part.id, { rounded: e.target.checked })} className="w-4 h-4 rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0 cursor-pointer"/></div>
<button onClick={() => removePartition(selectedData.key, selectedData.part.id)} className="w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded text-xs flex items-center justify-center gap-2 transition-colors mt-4"><Trash2 size={14}/> Удалить</button>
</div>
</div>
)}
{/* RIGHT: SIDEBAR */}
{mode === 'cells' && selectedData ? (
<div className="w-72 bg-slate-900 border-l border-slate-800 p-4 flex flex-col shrink-0 z-20">
<div className="flex justify-between items-center mb-6"><h3 className="text-sm font-bold text-white flex items-center gap-2"><Settings2 size={16} className="text-purple-400"/>Настройки</h3><button onClick={() => setSelectedPartitionId(null)} className="text-gray-400 hover:text-white"><X size={20}/></button></div>
<div className="bg-slate-800 p-4 rounded border border-slate-700 space-y-6">
<div><div className="flex justify-between text-xs text-gray-300 mb-2"><span>Высота</span> <span className="font-mono bg-slate-900 px-1.5 py-0.5 rounded text-xs">{selectedData.part.height} мм</span></div><input type="range" min="5" max={config.drawer.height || 100} step="1" value={selectedData.part.height} onChange={(e) => updatePartition(selectedData.key, selectedData.part.id, { height: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500"/></div>
<div className="flex items-center justify-between"><label htmlFor="rounded-check" className="text-xs text-gray-300 cursor-pointer select-none">Скруглить края</label><input type="checkbox" id="rounded-check" checked={selectedData.part.rounded} onChange={(e) => updatePartition(selectedData.key, selectedData.part.id, { rounded: e.target.checked })} className="w-4 h-4 rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0 cursor-pointer"/></div>
<button onClick={() => removePartition(selectedData.key, selectedData.part.id)} className="w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded text-xs flex items-center justify-center gap-2 transition-colors mt-4"><Trash2 size={14}/> Удалить</button>
</div>
<div className="mt-auto text-[10px] text-gray-500 text-center leading-relaxed">Выделите стенку для настройки.<br/>Двойной клик удаляет её.</div>
</div>
) : (
<div className="w-72 bg-slate-900/50 border-l border-slate-800 p-8 flex flex-col items-center justify-center text-center text-gray-500 shrink-0">
<MousePointer2 size={32} className="mb-4 opacity-50"/>
<p className="text-sm">Выберите стенку для настройки</p>
<p className="text-xs mt-2 opacity-50">Кликните по любой перегородке внутри ячейки</p>
</div>
)}
</div>
);
};