Merge pull request 'Скругления только для примыканий к стенкам' (#9) from test into main

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2026-01-11 23:50:32 +03:00
2 changed files with 63 additions and 27 deletions

View File

@@ -59,17 +59,17 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
};
const selectedData = getSelectedPartition();
// --- RAYCASTING (Надежный поиск коробки) ---
// Находит ближайшие стенки во всех 4 направлениях
// --- RAYCASTING (Поиск коробки для НОВОЙ стенки) ---
// Мы ищем ближайшие препятствия, чтобы определить границы новой стенки
const getCursorBox = (lx: number, ly: number, parts: Partition[]) => {
let minX = 0, maxX = 1;
let minY = 0, maxY = 1;
parts.forEach(p => {
// Используем сохраненные границы (они теперь достоверны)
// Используем сохраненные границы (честные)
const pMin = p.min ?? 0;
const pMax = p.max ?? 1;
const EPS = 0.005; // Допуск на попадание
const EPS = 0.005; // Допуск
if (p.axis === 'x') {
// Вертикальная стенка. Перекрывает ли она наш Y?
@@ -90,7 +90,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
return { minX, maxX, minY, maxY };
};
// Поиск соседей для размеров (Та же логика, что Raycasting)
// Поиск соседей для размеров (Используем сохраненные данные)
const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[]) => {
let min = 0;
let max = 1;
@@ -232,7 +232,7 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
if (found) {
setHoveredPartition({ id: found.id, cellKey: key });
} else {
// --- FIND BOX ---
// --- FIND BOX USING RAYCASTING ---
const box = getCursorBox(lx, ly, parts);
const boxW = (box.maxX - box.minX) * realCellW;
@@ -244,11 +244,12 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
// Override if near edges
const relL = (lx - box.minX) / (box.maxX - box.minX);
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
else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; // Near horiz edge -> horizontal wall
if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x';
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);
if (valid) {
@@ -298,6 +299,16 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
}
};
// --- HANDLER: Clear Phantom on Leave ---
const handleMouseLeave = () => {
setDragging(null);
setPhantomMainAxis(null);
setHoveredMainSplit(null);
setHoveredCell(null);
setHoveredPartition(null);
setPhantomPartition(null);
};
// --- RENDER ---
const renderCellsAndPartitions = () => {
const elements = [];
@@ -430,7 +441,13 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
<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()}>
<svg ref={svgRef} viewBox={`0 0 ${viewBoxW} ${viewBoxH}`} className="w-full h-full touch-none block"
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={() => setDragging(null)}
onMouseLeave={handleMouseLeave} // ДОБАВЛЕНО СЮДА
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>
<rect width="100%" height="100%" fill="url(#grid)" />
{renderCellsAndPartitions()}

View File

@@ -110,9 +110,9 @@ export const createBinGeometry = (
wallGeo.translate(0, thickness, 0);
geometries.push(wallGeo);
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ
// Мы просто верим сохраненным данным (p.min/p.max). Они должны быть корректны при создании.
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ (СТРОГО ПО ДАННЫМ, БЕЗ SOLVER)
partitions.forEach(p => {
// Берем данные напрямую. Если в 2D нарисовано от 0.2 до 0.8, тут будет 0.2 до 0.8.
const pMin = p.min ?? 0;
const pMax = p.max ?? 1;
@@ -141,38 +141,57 @@ export const createBinGeometry = (
partGeo.translate(pX, thickness, pY);
geometries.push(partGeo);
// СКРУГЛЕНИЯ
// СКРУГЛЕНИЯ (Fillets)
if (p.rounded && radius > 1) {
const filletR = Math.min(radius, 5);
const filletShape = createConcaveFilletShape(filletR);
const filletExtrude = { depth: p.height, bevelEnabled: false };
const addFillet = (x: number, y: number, rotY: number) => {
const geo = new THREE.ExtrudeGeometry(filletShape, filletExtrude);
// Функция проверки высоты соседа (простая проверка на пересечение)
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 h = thickness / 2;
const t = thickness / 2;
if (p.axis === 'x') {
const topY = (-innerDepth / 2) + (innerDepth * pMin);
addFillet(pX - h, topY, Math.PI);
addFillet(pX + h, topY, -Math.PI / 2);
const botY = (-innerDepth / 2) + (innerDepth * pMax);
addFillet(pX - h, botY, Math.PI / 2);
addFillet(pX + h, botY, 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);
addFillet(leftX, pY - h, 0);
addFillet(leftX, pY + h, -Math.PI / 2);
const rightX = (-innerWidth / 2) + (innerWidth * pMax);
addFillet(rightX, pY - h, Math.PI / 2);
addFillet(rightX, pY + h, Math.PI);
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);
}
}
});