From 8010c172c71b3a4cb8aa2f6b265488b35c24890a 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: Sat, 27 Dec 2025 19:04:48 +0300 Subject: [PATCH 1/5] Add share --- src/App.tsx | 30 +++- src/components/PreviewStep.tsx | 299 +++++++++++++++------------------ src/utils/share.ts | 54 ++++++ 3 files changed, 214 insertions(+), 169 deletions(-) create mode 100644 src/utils/share.ts diff --git a/src/App.tsx b/src/App.tsx index 9dffe1b..93dc741 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,16 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { AppConfig, LayoutSplits, GeneratedPart } from './types'; -// Убедись, что этот файл существует по этому пути +// Импорт должен быть правильным, проверь путь import { calculateParts } from './services/geometryGenerator'; import { ConfigStep } from './components/ConfigStep'; import { LayoutStep } from './components/LayoutStep'; import { PreviewStep } from './components/PreviewStep'; +import { parseShareUrl } from './utils/share'; // <--- Новый импорт import { ChevronRight, ChevronLeft, Box } from 'lucide-react'; const App = () => { const [step, setStep] = useState(1); + const [isLoadedFromUrl, setIsLoadedFromUrl] = useState(false); // State const [config, setConfig] = useState({ @@ -22,7 +24,22 @@ const App = () => { y: [] }); - // Derived State: Parts (Мгновенный пересчет без API) + // --- ЛОГИКА ВОССТАНОВЛЕНИЯ ИЗ ССЫЛКИ --- + useEffect(() => { + const sharedData = parseShareUrl(); + if (sharedData) { + setConfig(sharedData.config); + setSplits(sharedData.splits); + setStep(3); // Сразу прыгаем на превью + setIsLoadedFromUrl(true); + + // Очищаем URL, чтобы он не мозолил глаза (опционально) + window.history.replaceState({}, '', window.location.pathname); + } + }, []); + // --------------------------------------- + + // Derived State: Parts const parts: GeneratedPart[] = useMemo(() => { return calculateParts(config, splits); }, [config, splits]); @@ -38,7 +55,7 @@ const App = () => {

PrintFit

-

Генератор без ИИ

+

Генератор органайзеров

@@ -77,7 +94,8 @@ const App = () => { {step === 3 && (
- + {/* Передаем splits, чтобы кнопка Share могла их использовать */} +
)} @@ -109,6 +127,8 @@ const App = () => { onClick={() => { setStep(1); setSplits({x: [], y: []}); + // Сбрасываем URL если он был + window.history.replaceState({}, '', window.location.pathname); }} className="flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-gray-400 hover:text-white transition-colors border border-transparent hover:border-slate-700" > diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index 8a91087..030e255 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -1,19 +1,17 @@ -import React, { useMemo, Suspense, useEffect, useRef, useState } from 'react'; +import React, { Suspense, useEffect, useRef, useState } from 'react'; import { Canvas } from '@react-three/fiber'; import { OrbitControls, Center, Environment } from '@react-three/drei'; import * as THREE from 'three'; import JSZip from 'jszip'; -import { AppConfig, GeneratedPart } from '../types'; -import { createBinGeometry, exportSTL, generateSTL } from '../services/geometryGenerator'; -import { Download, Package, Info, Loader2 } from 'lucide-react'; +import { AppConfig, GeneratedPart, LayoutSplits } from '../types'; +import { createBinGeometry, generateSTL, exportSTL } from '../services/geometryGenerator'; +import { Download, Package, Info, Loader2, Share2, Check, Ruler } from 'lucide-react'; +import { generateShareUrl } from '../utils/share'; // <--- Импорт утилиты -// --- 3D Helper Components --- - -// Каркас ящика (только ребра, без диагоналей) +// ... (DrawerFrame и BinMesh остаются без изменений) ... const DrawerFrame = ({ config }: { config: AppConfig }) => { const { width, depth, height } = config.drawer; - const offset = 0.5; // Небольшой отступ наружу - + const offset = 0.5; return ( @@ -24,8 +22,6 @@ const DrawerFrame = ({ config }: { config: AppConfig }) => { ) } -// --- Bin Component --- - interface BinMeshProps { part: GeneratedPart; thickness: number; @@ -34,29 +30,15 @@ interface BinMeshProps { } const BinMesh: React.FC = ({ part, thickness, isSelected, onClick }) => { - // REMOVED ARTIFICIAL GAP: The part dimensions now include the printer tolerance physically. - // The gap will be visible naturally because part.width is smaller than grid size. - - // Генерируем реальную геометрию, как для STL - const geometry = useMemo(() => { + const geometry = React.useMemo(() => { return createBinGeometry(part.width, part.depth, part.height, thickness); }, [part, thickness]); return ( - {/* Основной меш ячейки */} - { e.stopPropagation(); onClick(); }} - > - + { e.stopPropagation(); onClick(); }}> + - - {/* Подсветка выделения (Bounding Box) */} {isSelected && ( @@ -67,23 +49,23 @@ const BinMesh: React.FC = ({ part, thickness, isSelected, onClick ); }; +// --- Основной компонент --- + interface Props { parts: GeneratedPart[]; config: AppConfig; + splits: LayoutSplits; // <--- Добавили splits в пропсы } -export const PreviewStep: React.FC = ({ parts, config }) => { +export const PreviewStep: React.FC = ({ parts, config, splits }) => { const [selectedId, setSelectedId] = useState(null); const [isZipping, setIsZipping] = useState(false); + const [shareUrlCopied, setShareUrlCopied] = useState(false); const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - // Автопрокрутка к выбранному элементу useEffect(() => { if (selectedId && itemRefs.current[selectedId]) { - itemRefs.current[selectedId]?.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); + itemRefs.current[selectedId]?.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, [selectedId]); @@ -96,162 +78,151 @@ export const PreviewStep: React.FC = ({ parts, config }) => { const handleDownloadAll = async () => { if (isZipping) return; setIsZipping(true); - try { - console.log("Starting ZIP generation..."); - // Ensure JSZip is available - if (typeof JSZip === 'undefined' && !JSZip) { - throw new Error("Библиотека JSZip не загружена."); - } - const zip = new JSZip(); - - // Генерация STL для каждой части и добавление в архив parts.forEach(part => { const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness); const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial()); const stlData = generateSTL(mesh); - // stlData is Uint8Array or string here, which is supported zip.file(`${part.name.replace(/\s+/g, '_')}.stl`, stlData); }); - - console.log("Files added to ZIP. Generating blob..."); - - // Генерация самого ZIP файла const content = await zip.generateAsync({ type: "blob" }); - - console.log("ZIP blob generated. Size:", content.size); - - // Скачивание const link = document.createElement('a'); link.href = URL.createObjectURL(content); link.download = "PrintFit_Project.zip"; document.body.appendChild(link); link.click(); document.body.removeChild(link); - } catch (e: any) { - console.error("Failed to create zip archive", e); - alert(`Ошибка при создании архива: ${e.message || 'Неизвестная ошибка'}`); + alert(`Ошибка архивации: ${e.message}`); } finally { setIsZipping(false); } }; - return ( -
- {/* 3D Viewer */} -
-
-
- Управление -
-
    -
  • • ЛКМ: Вращение
  • -
  • • ПКМ: Перемещение
  • -
  • • Скролл: Масштаб
  • -
  • • Клик по детали для выбора
  • -
-
- - - - - - - - - + // Логика кнопки "Поделиться" + const handleShare = () => { + const url = generateShareUrl(config, splits); + navigator.clipboard.writeText(url).then(() => { + setShareUrlCopied(true); + setTimeout(() => setShareUrlCopied(false), 2000); + }); + }; -
- - - {parts.map(part => ( - setSelectedId(part.id)} - /> - ))} - -
- - -
-
+ return ( +
+ {/* 1. ВЕРХНЯЯ ПАНЕЛЬ С РАЗМЕРАМИ И КНОПКОЙ SHARE */} +
+ +
+
+ + Размеры ящика: +
+
+
+ W: {config.drawer.width} +
+
+ D: {config.drawer.depth} +
+
+ H: {config.drawer.height} +
+ мм +
+
+ +
- {/* Sidebar List */} -
-
-

- Детали ({parts.length}) -

- +
+ {/* 3D Viewer */} +
+
+
+ Управление +
+
    +
  • • ЛКМ: Вращение
  • +
  • • ПКМ: Перемещение
  • +
  • • Скролл: Масштаб
  • +
+
+ + + + + + + +
+ + + {parts.map(part => ( + setSelectedId(part.id)} + /> + ))} + +
+ +
+
-
- {parts.map(part => ( -
{ itemRefs.current[part.id] = el }} - className={`p-4 rounded-lg border transition-all cursor-pointer group ${selectedId === part.id ? 'bg-slate-800 border-accent shadow-md shadow-accent/10 ring-1 ring-accent' : 'bg-slate-800/50 border-slate-700 hover:border-slate-500 hover:bg-slate-800'}`} - onClick={() => setSelectedId(part.id)} - > -
- {part.name} -
-
-
-
- Ширина - {part.width.toFixed(1)} -
-
- Глубина - {part.depth.toFixed(1)} -
-
- Высота - {part.height.toFixed(1)} -
-
- -
- ))} + {/* Sidebar List */} +
+
+

+ Детали ({parts.length}) +

+ +
+ +
+ {parts.map(part => ( +
{ itemRefs.current[part.id] = el }} + className={`p-4 rounded-lg border transition-all cursor-pointer group ${selectedId === part.id ? 'bg-slate-800 border-accent shadow-md shadow-accent/10 ring-1 ring-accent' : 'bg-slate-800/50 border-slate-700 hover:border-slate-500 hover:bg-slate-800'}`} + onClick={() => setSelectedId(part.id)} + > +
+ {part.name} +
+
+ + +
+ ))} +
diff --git a/src/utils/share.ts b/src/utils/share.ts new file mode 100644 index 0000000..4bce9d4 --- /dev/null +++ b/src/utils/share.ts @@ -0,0 +1,54 @@ +import { AppConfig, LayoutSplits } from '../types'; + +interface ShareData { + c: AppConfig; // config + s: LayoutSplits; // splits +} + +/** + * Генерирует ссылку на текущее состояние + */ +export const generateShareUrl = (config: AppConfig, splits: LayoutSplits): string => { + try { + // 1. Собираем объект данных + const data: ShareData = { c: config, s: splits }; + + // 2. Превращаем в JSON строку + const jsonString = JSON.stringify(data); + + // 3. Кодируем в Base64 (чтобы URL был чище) + // btoa работает только с ASCII, поэтому для надежности кодируем через URI + const base64 = btoa(encodeURIComponent(jsonString)); + + // 4. Формируем полный URL + return `${window.location.origin}?share=${base64}`; + } catch (e) { + console.error('Ошибка генерации ссылки:', e); + return ''; + } +}; + +/** + * Пытается восстановить состояние из URL + */ +export const parseShareUrl = (): { config: AppConfig, splits: LayoutSplits } | null => { + try { + const params = new URLSearchParams(window.location.search); + const shareParam = params.get('share'); + + if (!shareParam) return null; + + // Декодируем обратно + const jsonString = decodeURIComponent(atob(shareParam)); + const data: ShareData = JSON.parse(jsonString); + + // Простая валидация, что данные похожи на правду + if (data.c && data.s && Array.isArray(data.s.x)) { + return { config: data.c, splits: data.s }; + } + return null; + } catch (e) { + console.error('Ошибка чтения ссылки:', e); + return null; + } +}; \ No newline at end of file From f296db8297c7636266cb9b5374285eca4a79de0a 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: Sat, 27 Dec 2025 19:15:22 +0300 Subject: [PATCH 2/5] Fix copy link --- src/components/PreviewStep.tsx | 71 ++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index 030e255..ce131f4 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -5,10 +5,10 @@ import * as THREE from 'three'; import JSZip from 'jszip'; import { AppConfig, GeneratedPart, LayoutSplits } from '../types'; import { createBinGeometry, generateSTL, exportSTL } from '../services/geometryGenerator'; -import { Download, Package, Info, Loader2, Share2, Check, Ruler } from 'lucide-react'; -import { generateShareUrl } from '../utils/share'; // <--- Импорт утилиты +import { Download, Package, Info, Loader2, Share2, Check, Ruler, Copy } from 'lucide-react'; +import { generateShareUrl } from '../utils/share'; -// ... (DrawerFrame и BinMesh остаются без изменений) ... +// --- DrawerFrame (Каркас) --- const DrawerFrame = ({ config }: { config: AppConfig }) => { const { width, depth, height } = config.drawer; const offset = 0.5; @@ -22,6 +22,7 @@ const DrawerFrame = ({ config }: { config: AppConfig }) => { ) } +// --- BinMesh (Ячейка) --- interface BinMeshProps { part: GeneratedPart; thickness: number; @@ -49,12 +50,11 @@ const BinMesh: React.FC = ({ part, thickness, isSelected, onClick ); }; -// --- Основной компонент --- - +// --- PreviewStep (Основной) --- interface Props { parts: GeneratedPart[]; config: AppConfig; - splits: LayoutSplits; // <--- Добавили splits в пропсы + splits: LayoutSplits; } export const PreviewStep: React.FC = ({ parts, config, splits }) => { @@ -100,18 +100,55 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { } }; - // Логика кнопки "Поделиться" - const handleShare = () => { + // --- Улучшенная функция копирования --- + const handleShare = async () => { const url = generateShareUrl(config, splits); - navigator.clipboard.writeText(url).then(() => { + let success = false; + + try { + // 1. Пробуем современный API (работает только на HTTPS или localhost) + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(url); + success = true; + } else { + throw new Error('Clipboard API unavailable'); + } + } catch (err) { + // 2. Если не вышло (например, HTTP), используем Fallback метод + try { + const textArea = document.createElement("textarea"); + textArea.value = url; + + // Делаем элемент невидимым, но частью DOM + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + textArea.style.top = "0"; + document.body.appendChild(textArea); + + textArea.focus(); + textArea.select(); + + const result = document.execCommand('copy'); + document.body.removeChild(textArea); + + if (result) success = true; + } catch (e) { + console.error("Copy failed", e); + } + } + + if (success) { setShareUrlCopied(true); - setTimeout(() => setShareUrlCopied(false), 2000); - }); + setTimeout(() => setShareUrlCopied(false), 3000); + } else { + // 3. Если совсем ничего не помогло, показываем промпт + prompt("Не удалось скопировать автоматически. Скопируйте ссылку вручную:", url); + } }; return (
- {/* 1. ВЕРХНЯЯ ПАНЕЛЬ С РАЗМЕРАМИ И КНОПКОЙ SHARE */} + {/* Верхняя панель: Размеры + Поделиться */}
@@ -136,15 +173,15 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => {
From a81e4be8a2fb9a4cfdfa9398d22bd84e06c91963 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: Sat, 27 Dec 2025 19:25:23 +0300 Subject: [PATCH 3/5] Renames --- src/components/PreviewStep.tsx | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index ce131f4..9bf8975 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -5,7 +5,7 @@ import * as THREE from 'three'; import JSZip from 'jszip'; import { AppConfig, GeneratedPart, LayoutSplits } from '../types'; import { createBinGeometry, generateSTL, exportSTL } from '../services/geometryGenerator'; -import { Download, Package, Info, Loader2, Share2, Check, Ruler, Copy } from 'lucide-react'; +import { Download, Package, Info, Loader2, Share2, Check, Ruler } from 'lucide-react'; import { generateShareUrl } from '../utils/share'; // --- DrawerFrame (Каркас) --- @@ -106,7 +106,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { let success = false; try { - // 1. Пробуем современный API (работает только на HTTPS или localhost) + // 1. Пробуем современный API if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(url); success = true; @@ -114,23 +114,18 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { throw new Error('Clipboard API unavailable'); } } catch (err) { - // 2. Если не вышло (например, HTTP), используем Fallback метод + // 2. Fallback для HTTP try { const textArea = document.createElement("textarea"); textArea.value = url; - - // Делаем элемент невидимым, но частью DOM textArea.style.position = "fixed"; textArea.style.left = "-9999px"; textArea.style.top = "0"; document.body.appendChild(textArea); - textArea.focus(); textArea.select(); - const result = document.execCommand('copy'); document.body.removeChild(textArea); - if (result) success = true; } catch (e) { console.error("Copy failed", e); @@ -141,8 +136,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { setShareUrlCopied(true); setTimeout(() => setShareUrlCopied(false), 3000); } else { - // 3. Если совсем ничего не помогло, показываем промпт - prompt("Не удалось скопировать автоматически. Скопируйте ссылку вручную:", url); + prompt("Скопируйте ссылку вручную:", url); } }; @@ -156,15 +150,17 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { Размеры ящика:
+ + {/* ЗДЕСЬ ИЗМЕНЕНИЯ: Ш, Г, В */}
- W: {config.drawer.width} + Ш: {config.drawer.width}
- D: {config.drawer.depth} + Г: {config.drawer.depth}
- H: {config.drawer.height} + В: {config.drawer.height}
мм
From e91eea8c689ccecd9887fb9a4f59bb59b57a101a 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: Sat, 27 Dec 2025 19:39:21 +0300 Subject: [PATCH 4/5] Try fix 3d view --- src/components/PreviewStep.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index 9bf8975..0193735 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -100,13 +100,12 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { } }; - // --- Улучшенная функция копирования --- + // --- Функция копирования --- const handleShare = async () => { const url = generateShareUrl(config, splits); let success = false; try { - // 1. Пробуем современный API if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(url); success = true; @@ -114,7 +113,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { throw new Error('Clipboard API unavailable'); } } catch (err) { - // 2. Fallback для HTTP try { const textArea = document.createElement("textarea"); textArea.value = url; @@ -142,7 +140,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { return (
- {/* Верхняя панель: Размеры + Поделиться */} + {/* Верхняя панель */}
@@ -150,8 +148,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { Размеры ящика:
- - {/* ЗДЕСЬ ИЗМЕНЕНИЯ: Ш, Г, В */}
Ш: {config.drawer.width} @@ -195,7 +191,16 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => {
- + From 3263645c3bc29b518b5ebad6eee30e1348ad2a29 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: Sat, 27 Dec 2025 20:02:20 +0300 Subject: [PATCH 5/5] Fix --- src/components/PreviewStep.tsx | 37 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index 0193735..132e489 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -140,32 +140,37 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { return (
- {/* Верхняя панель */} -
+ {/* Верхняя панель: Размеры + Поделиться */} +
-
-
+
+
Размеры ящика:
-
-
- Ш: {config.drawer.width} + + {/* --- ОБНОВЛЕННЫЙ БЛОК РАЗМЕРОВ --- */} +
+
+ Ширина: + {config.drawer.width}
-
- Г: {config.drawer.depth} +
+ Глубина: + {config.drawer.depth}
-
- В: {config.drawer.height} +
+ Высота: + {config.drawer.height}
- мм + мм
@@ -197,8 +202,8 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { camera={{ position: [config.drawer.width * 1.5, config.drawer.height * 3, config.drawer.depth * 1.5], fov: 45, - near: 1, // Исправлено: не режем вблизи - far: 20000 // Исправлено: видим очень далеко (для больших размеров в мм) + near: 1, + far: 20000 }} >