Spaces:
Running
Running
| <html lang="pl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Room Creator 2D/3D</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-dark: #0f172a; | |
| --bg-panel: #1e293b; | |
| --bg-canvas: #0b1120; | |
| --accent: #3b82f6; | |
| --accent-hover: #2563eb; | |
| --text: #f1f5f9; | |
| --text-muted: #94a3b8; | |
| --border: #334155; | |
| --danger: #ef4444; | |
| --success: #22c55e; | |
| --warning: #f59e0b; | |
| } | |
| body { | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| background: var(--bg-dark); | |
| color: var(--text); | |
| overflow: hidden; | |
| height: 100vh; | |
| } | |
| /* Layout */ | |
| .app-container { | |
| display: grid; | |
| grid-template-columns: 280px 1fr; | |
| grid-template-rows: 60px 1fr; | |
| height: 100vh; | |
| } | |
| /* Top Bar */ | |
| .toolbar { | |
| grid-column: 1 / -1; | |
| background: var(--bg-panel); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| padding: 0 20px; | |
| gap: 12px; | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-weight: 700; | |
| font-size: 1.2rem; | |
| color: var(--accent); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-right: 10px; | |
| } | |
| .logo a { | |
| color: var(--accent); | |
| text-decoration: none; | |
| transition: opacity 0.2s; | |
| } | |
| .logo a:hover { | |
| opacity: 0.8; | |
| } | |
| .tool-group { | |
| display: flex; | |
| gap: 8px; | |
| padding: 0 12px; | |
| border-right: 1px solid var(--border); | |
| } | |
| .tool-btn { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| padding: 8px 14px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 0.85rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| white-space: nowrap; | |
| } | |
| .tool-btn:hover { | |
| background: rgba(255,255,255,0.05); | |
| border-color: var(--accent); | |
| } | |
| .tool-btn.active { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: white; | |
| } | |
| .tool-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .view-toggle { | |
| margin-left: auto; | |
| display: flex; | |
| background: var(--bg-dark); | |
| border-radius: 6px; | |
| padding: 4px; | |
| border: 1px solid var(--border); | |
| gap: 4px; | |
| } | |
| .view-btn { | |
| padding: 6px 12px; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| border-radius: 4px; | |
| font-weight: 500; | |
| transition: all 0.2s; | |
| font-size: 0.85rem; | |
| white-space: nowrap; | |
| } | |
| .view-btn.active { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| /* Keyboard shortcuts popover */ | |
| .shortcuts-popover { | |
| position: relative; | |
| } | |
| .shortcuts-content { | |
| position: absolute; | |
| top: calc(100% + 10px); | |
| right: 0; | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 20px; | |
| width: 320px; | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.4); | |
| opacity: 0; | |
| visibility: hidden; | |
| transform: translateY(-10px); | |
| transition: all 0.2s ease; | |
| z-index: 1000; | |
| } | |
| .shortcuts-content.visible { | |
| opacity: 1; | |
| visibility: visible; | |
| transform: translateY(0); | |
| } | |
| .shortcuts-content::before { | |
| content: ''; | |
| position: absolute; | |
| top: -6px; | |
| right: 20px; | |
| width: 12px; | |
| height: 12px; | |
| background: var(--bg-panel); | |
| border-left: 1px solid var(--border); | |
| border-top: 1px solid var(--border); | |
| transform: rotate(45deg); | |
| } | |
| .shortcuts-title { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| margin-bottom: 16px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .shortcuts-grid { | |
| display: grid; | |
| gap: 12px; | |
| } | |
| .shortcut-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-size: 0.9rem; | |
| } | |
| .shortcut-key { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| kbd { | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 4px 8px; | |
| font-family: 'SF Mono', monospace; | |
| font-size: 0.75rem; | |
| color: var(--accent); | |
| min-width: 28px; | |
| text-align: center; | |
| } | |
| .shortcut-desc { | |
| color: var(--text-muted); | |
| } | |
| .shortcuts-footer { | |
| margin-top: 16px; | |
| padding-top: 16px; | |
| border-top: 1px solid var(--border); | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| line-height: 1.5; | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| background: var(--bg-panel); | |
| border-right: 1px solid var(--border); | |
| padding: 20px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| } | |
| .panel-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .panel-title { | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| } | |
| .items-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| } | |
| .item-card { | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 12px; | |
| cursor: grab; | |
| text-align: center; | |
| transition: all 0.2s; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .item-card:hover { | |
| border-color: var(--accent); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); | |
| } | |
| .item-card:active { | |
| cursor: grabbing; | |
| } | |
| .item-icon { | |
| font-size: 2rem; | |
| margin-bottom: 4px; | |
| } | |
| .item-name { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| } | |
| .lock-indicator { | |
| display: inline-block; | |
| margin-left: 8px; | |
| padding: 2px 8px; | |
| background: var(--danger); | |
| color: white; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| font-weight: bold; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| position: relative; | |
| background: var(--bg-canvas); | |
| overflow: hidden; | |
| } | |
| #canvas2d, #canvas3d { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| #canvas3d { | |
| display: none; | |
| } | |
| .viewport-label { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| background: rgba(15, 23, 42, 0.9); | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| border: 1px solid var(--border); | |
| backdrop-filter: blur(4px); | |
| z-index: 10; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| /* Properties Panel (floating) */ | |
| .properties-panel { | |
| position: absolute; | |
| right: 20px; | |
| top: 20px; | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 16px; | |
| width: 280px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| display: none; | |
| z-index: 50; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| } | |
| .properties-panel.visible { | |
| display: block; | |
| } | |
| .properties-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .properties-header .panel-title { | |
| margin: 0; | |
| } | |
| .close-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| padding: 4px; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s; | |
| } | |
| .close-btn:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: var(--text); | |
| } | |
| .prop-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| font-size: 0.9rem; | |
| } | |
| .prop-label { | |
| color: var(--text-muted); | |
| font-size: 0.85rem; | |
| } | |
| .prop-value { | |
| font-weight: 600; | |
| color: var(--accent); | |
| } | |
| .dimension-row { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| } | |
| .dimension-label { | |
| color: var(--text-muted); | |
| font-size: 0.85rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .dimension-input-group { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .dimension-input { | |
| flex: 1; | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| padding: 6px 10px; | |
| border-radius: 6px; | |
| font-size: 0.9rem; | |
| width: 100%; | |
| } | |
| .dimension-input:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| .unit-select { | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| padding: 6px 8px; | |
| border-radius: 6px; | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| min-width: 70px; | |
| } | |
| .unit-select:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| .dimensions-section { | |
| margin-top: 16px; | |
| padding-top: 16px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .section-subtitle { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| font-weight: 600; | |
| } | |
| .prop-actions { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| margin-top: 16px; | |
| padding-top: 16px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .prop-actions-row { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .prop-btn { | |
| flex: 1; | |
| padding: 8px; | |
| border: 1px solid var(--border); | |
| background: var(--bg-dark); | |
| color: var(--text); | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| transition: all 0.2s; | |
| } | |
| .prop-btn:hover { | |
| border-color: var(--accent); | |
| background: rgba(59, 130, 246, 0.1); | |
| } | |
| .prop-btn.delete { | |
| border-color: #ef4444; | |
| color: #ef4444; | |
| } | |
| .prop-btn.delete:hover { | |
| background: rgba(239, 68, 68, 0.1); | |
| } | |
| .prop-btn.secondary { | |
| border-color: var(--warning); | |
| color: var(--warning); | |
| } | |
| .prop-btn.secondary:hover { | |
| background: rgba(245, 158, 11, 0.1); | |
| } | |
| .prop-btn.success { | |
| border-color: var(--success); | |
| color: var(--success); | |
| } | |
| .prop-btn.success:hover { | |
| background: rgba(34, 197, 94, 0.1); | |
| } | |
| .lock-toggle { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px; | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| margin-bottom: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .lock-toggle:hover { | |
| border-color: var(--accent); | |
| } | |
| .lock-toggle.locked { | |
| border-color: var(--danger); | |
| background: rgba(239, 68, 68, 0.1); | |
| } | |
| .lock-toggle-label { | |
| font-size: 0.85rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .lock-status { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| } | |
| .lock-toggle.locked .lock-status { | |
| color: var(--danger); | |
| } | |
| .multi-select-info { | |
| background: rgba(59, 130, 246, 0.1); | |
| border: 1px solid rgba(59, 130, 246, 0.2); | |
| border-radius: 6px; | |
| padding: 8px; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| } | |
| /* Settings */ | |
| .settings-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 12px; | |
| } | |
| input[type="range"] { | |
| width: 100px; | |
| accent-color: var(--accent); | |
| } | |
| input[type="checkbox"] { | |
| accent-color: var(--accent); | |
| width: 18px; | |
| height: 18px; | |
| cursor: pointer; | |
| } | |
| /* Modal */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.7); | |
| z-index: 1000; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .modal.visible { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 24px; | |
| max-width: 400px; | |
| width: 90%; | |
| } | |
| .modal-title { | |
| font-size: 1.2rem; | |
| margin-bottom: 16px; | |
| color: var(--text); | |
| } | |
| .modal-text { | |
| color: var(--text-muted); | |
| margin-bottom: 20px; | |
| line-height: 1.5; | |
| } | |
| .modal-actions { | |
| display: flex; | |
| gap: 12px; | |
| justify-content: flex-end; | |
| } | |
| .btn { | |
| padding: 8px 16px; | |
| border-radius: 6px; | |
| border: 1px solid var(--border); | |
| background: var(--bg-dark); | |
| color: var(--text); | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| transition: all 0.2s; | |
| } | |
| .btn:hover { | |
| border-color: var(--accent); | |
| } | |
| .btn-primary { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background: var(--accent-hover); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 900px) { | |
| .toolbar { | |
| flex-wrap: wrap; | |
| height: auto; | |
| padding: 10px; | |
| } | |
| .view-toggle { | |
| margin-left: 0; | |
| width: 100%; | |
| justify-content: center; | |
| margin-top: 10px; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .app-container { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: 100px 1fr 80px; | |
| } | |
| .sidebar { | |
| position: fixed; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| height: 80px; | |
| flex-direction: row; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| z-index: 100; | |
| border-top: 1px solid var(--border); | |
| border-right: none; | |
| padding: 10px; | |
| gap: 10px; | |
| } | |
| .panel-section:not(.tools-section) { | |
| display: none; | |
| } | |
| .items-grid { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .item-card { | |
| min-width: 70px; | |
| padding: 8px; | |
| } | |
| .properties-panel { | |
| width: calc(100% - 40px); | |
| right: 20px; | |
| left: 20px; | |
| } | |
| .shortcuts-content { | |
| right: -60px; | |
| width: 280px; | |
| } | |
| } | |
| /* Custom scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-dark); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-muted); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Toolbar --> | |
| <header class="toolbar"> | |
| <div class="logo"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M3 21h18M5 21V7l8-4 8 4v14M8 21v-9a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v9"/> | |
| </svg> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a> | |
| </div> | |
| <div class="tool-group"> | |
| <button class="tool-btn active" data-tool="select" title="Wybierz (V)"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/> | |
| </svg> | |
| <span>Wybierz</span> | |
| </button> | |
| <button class="tool-btn" data-tool="wall" title="Ściana (W)"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M2 12h20M2 12v5m20-5v5M5 12v-3m14 3v-3"/> | |
| </svg> | |
| <span>Ściana</span> | |
| </button> | |
| </div> | |
| <div class="tool-group"> | |
| <button class="tool-btn" onclick="app.exportJSON()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m4-5l5 5 5-5m-5 5V3"/> | |
| </svg> | |
| <span>Zapisz</span> | |
| </button> | |
| <button class="tool-btn" onclick="document.getElementById('fileInput').click()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m4-5l5-5 5 5m-5-5v12"/> | |
| </svg> | |
| <span>Wczytaj</span> | |
| </button> | |
| <input type="file" id="fileInput" accept=".json" style="display: none" onchange="app.importJSON(this)"> | |
| <button class="tool-btn" onclick="app.exportImage()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/> | |
| <circle cx="8.5" cy="8.5" r="1.5"/> | |
| <polyline points="21 15 16 10 5 21"/> | |
| </svg> | |
| <span>Eksport</span> | |
| </button> | |
| <button class="tool-btn" onclick="app.reset()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="3 6 5 6 21 6"/> | |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/> | |
| </svg> | |
| <span>Reset</span> | |
| </button> | |
| </div> | |
| <!-- Keyboard Shortcuts Button --> | |
| <div class="shortcuts-popover"> | |
| <button class="tool-btn" onclick="app.toggleShortcuts()" title="Skróty klawiszowe"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="2" y="4" width="20" height="16" rx="2" ry="2"/> | |
| <path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/> | |
| </svg> | |
| <span>Skróty</span> | |
| </button> | |
| <div class="shortcuts-content" id="shortcutsContent"> | |
| <div class="shortcuts-title"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="2" y="4" width="20" height="16" rx="2" ry="2"/> | |
| <path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/> | |
| </svg> | |
| Skróty klawiszowe | |
| </div> | |
| <div class="shortcuts-grid"> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Wybierz narzędzie</span> | |
| <span class="shortcut-key"><kbd>V</kbd></span> | |
| </div> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Narzędzie ściany</span> | |
| <span class="shortcut-key"><kbd>W</kbd></span> | |
| </div> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Obrót obiektu (90°)</span> | |
| <span class="shortcut-key"><kbd>R</kbd></span> | |
| </div> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Usuń zaznaczone</span> | |
| <span class="shortcut-key"><kbd>Del</kbd></span> | |
| </div> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Przybliżenie</span> | |
| <span class="shortcut-key"><kbd>+</kbd></span> | |
| </div> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Oddalenie</span> | |
| <span class="shortcut-key"><kbd>-</kbd></span> | |
| </div> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Blokada kamery (3D)</span> | |
| <span class="shortcut-key"><kbd>L</kbd></span> | |
| </div> | |
| <div class="shortcut-row"> | |
| <span class="shortcut-desc">Zaznacz wiele</span> | |
| <span class="shortcut-key"><kbd>Ctrl</kbd><kbd>+</kbd><kbd>Klik</kbd></span> | |
| </div> | |
| </div> | |
| <div class="shortcuts-footer"> | |
| <strong style="color: var(--accent);">Wskazówka:</strong> Przeciągnij mebel z listy bezpośrednio na plan (działa w 2D i 3D). Użyj <kbd>Ctrl</kbd> + kliknięcie aby zaznaczyć wiele obiektów naraz. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="view-toggle"> | |
| <button class="view-btn active" data-view="2d">2D Plan</button> | |
| <button class="view-btn" data-view="3d">3D Widok</button> | |
| <button class="view-btn" data-view="3d-plan">3D Plan</button> | |
| </div> | |
| </header> | |
| <!-- Sidebar --> | |
| <aside class="sidebar"> | |
| <div class="panel-section tools-section"> | |
| <div class="panel-title">Ustawienia widoku</div> | |
| <div class="settings-row"> | |
| <label>Siatka (grid)</label> | |
| <input type="checkbox" id="gridToggle" checked onchange="app.toggleGrid(this.checked)"> | |
| </div> | |
| <div class="settings-row"> | |
| <label>Skala (px/m)</label> | |
| <input type="range" id="scaleSlider" min="20" max="100" value="50" oninput="app.setScale(this.value)"> | |
| <span id="scaleValue">50</span> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-title">Meble</div> | |
| <div class="items-grid" ondragstart="app.handleDragStart(event)" ondragend="app.handleDragEnd(event)"> | |
| <div class="item-card" draggable="true" data-type="sofa" data-width="200" data-height="90"> | |
| <div class="item-icon">🛋️</div> | |
| <div class="item-name">Sofa</div> | |
| </div> | |
| <div class="item-card" draggable="true" data-type="table" data-width="160" data-height="90"> | |
| <div class="item-icon">🍽️</div> | |
| <div class="item-name">Stół</div> | |
| </div> | |
| <div class="item-card" draggable="true" data-type="chair" data-width="50" data-height="50"> | |
| <div class="item-icon">🪑</div> | |
| <div class="item-name">Krzesło</div> | |
| </div> | |
| <div class="item-card" draggable="true" data-type="bed" data-width="180" data-height="200"> | |
| <div class="item-icon">🛏️</div> | |
| <div class="item-name">Łóżko</div> | |
| </div> | |
| <div class="item-card" draggable="true" data-type="cabinet" data-width="120" data-height="60"> | |
| <div class="item-icon">🗄️</div> | |
| <div class="item-name">Szafka</div> | |
| </div> | |
| <div class="item-card" draggable="true" data-type="plant" data-width="40" data-height="40"> | |
| <div class="item-icon">🪴</div> | |
| <div class="item-name">Roślina</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="panel-title">Drzwi i okna</div> | |
| <div class="items-grid"> | |
| <div class="item-card" draggable="true" data-type="door" data-width="90" data-height="10"> | |
| <div class="item-icon">🚪</div> | |
| <div class="item-name">Drzwi</div> | |
| </div> | |
| <div class="item-card" draggable="true" data-type="window" data-width="120" data-height="10"> | |
| <div class="item-icon">🪟</div> | |
| <div class="item-name">Okno</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="main-content" id="mainContainer"> | |
| <div class="viewport-label" id="viewportLabel">Widok 2D - Rzut z góry</div> | |
| <canvas id="canvas2d"></canvas> | |
| <div id="canvas3d"></div> | |
| <!-- Properties Panel --> | |
| <div class="properties-panel" id="propertiesPanel"> | |
| <div class="properties-header"> | |
| <div class="panel-title">Właściwości</div> | |
| <button class="close-btn" onclick="app.closePropertiesPanel()" title="Zamknij"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| <div id="singleSelection"> | |
| <div class="prop-row"> | |
| <span class="prop-label">Typ:</span> | |
| <span class="prop-value" id="propType">-</span> | |
| </div> | |
| <div class="prop-row"> | |
| <span class="prop-label">Pozycja X:</span> | |
| <span class="prop-value" id="propX">0m</span> | |
| </div> | |
| <div class="prop-row"> | |
| <span class="prop-label">Pozycja Y:</span> | |
| <span class="prop-value" id="propY">0m</span> | |
| </div> | |
| <div class="prop-row" id="rotationRow"> | |
| <span class="prop-label">Rotacja:</span> | |
| <span class="prop-value" id="propRot">0°</span> | |
| </div> | |
| <!-- Sekcja wymiarów --> | |
| <div class="dimensions-section" id="dimensionsSection"> | |
| <div class="section-subtitle">Wymiary</div> | |
| <!-- Dla mebli --> | |
| <div id="furnitureDimensions"> | |
| <div class="dimension-row"> | |
| <div class="dimension-label">Szerokość</div> | |
| <div class="dimension-input-group"> | |
| <input type="number" class="dimension-input" id="dimWidth" step="0.1" onchange="app.updateDimension('width')"> | |
| <select class="unit-select" id="unitWidth" onchange="app.updateDimension('width')"> | |
| <option value="mm">mm</option> | |
| <option value="cm" selected>cm</option> | |
| <option value="m">m</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="dimension-row"> | |
| <div class="dimension-label">Głębokość</div> | |
| <div class="dimension-input-group"> | |
| <input type="number" class="dimension-input" id="dimDepth" step="0.1" onchange="app.updateDimension('depth')"> | |
| <select class="unit-select" id="unitDepth" onchange="app.updateDimension('depth')"> | |
| <option value="mm">mm</option> | |
| <option value="cm" selected>cm</option> | |
| <option value="m">m</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="dimension-row"> | |
| <div class="dimension-label">Wysokość</div> | |
| <div class="dimension-input-group"> | |
| <input type="number" class="dimension-input" id="dimHeight" step="0.1" onchange="app.updateDimension('height')"> | |
| <select class="unit-select" id="unitHeight" onchange="app.updateDimension('height')"> | |
| <option value="mm">mm</option> | |
| <option value="cm" selected>cm</option> | |
| <option value="m">m</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Dla ścian --> | |
| <div id="wallDimensions" style="display: none;"> | |
| <div class="dimension-row"> | |
| <div class="dimension-label">Długość</div> | |
| <div class="dimension-input-group"> | |
| <input type="number" class="dimension-input" id="dimLength" step="0.1" onchange="app.updateDimension('length')"> | |
| <select class="unit-select" id="unitLength" onchange="app.updateDimension('length')"> | |
| <option value="mm">mm</option> | |
| <option value="cm" selected>cm</option> | |
| <option value="m">m</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="dimension-row"> | |
| <div class="dimension-label">Grubość</div> | |
| <div class="dimension-input-group"> | |
| <input type="number" class="dimension-input" id="dimThickness" step="0.1" onchange="app.updateDimension('thickness')"> | |
| <select class="unit-select" id="unitThickness" onchange="app.updateDimension('thickness')"> | |
| <option value="mm">mm</option> | |
| <option value="cm" selected>cm</option> | |
| <option value="m">m</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="dimension-row"> | |
| <div class="dimension-label">Wysokość</div> | |
| <div class="dimension-input-group"> | |
| <input type="number" class="dimension-input" id="dimWallHeight" step="0.1" onchange="app.updateDimension('wallHeight')"> | |
| <select class="unit-select" id="unitWallHeight" onchange="app.updateDimension('wallHeight')"> | |
| <option value="mm">mm</option> | |
| <option value="cm" selected>cm</option> | |
| <option value="m">m</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Przełącznik blokady --> | |
| <div class="lock-toggle" id="lockToggle" onclick="app.toggleWallLock()"> | |
| <div class="lock-toggle-label"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" id="lockIcon"> | |
| <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | |
| <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> | |
| </svg> | |
| <span>Blokada pozycji</span> | |
| </div> | |
| <span class="lock-status" id="lockStatus">Odblokowana</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="prop-actions"> | |
| <div class="prop-actions-row"> | |
| <button class="prop-btn" onclick="app.rotateSelected()" id="rotateBtn">Obrót 90°</button> | |
| <button class="prop-btn delete" onclick="app.deleteSelected()">Usuń</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Multi-selection view --> | |
| <div id="multiSelection" style="display: none;"> | |
| <div class="multi-select-info"> | |
| Zaznaczono <span id="selectedCount">0</span> obiektów | |
| </div> | |
| <div class="prop-actions"> | |
| <button class="prop-btn success" onclick="app.joinSelectedWalls()"> | |
| Połącz węzły ścian | |
| </button> | |
| <button class="prop-btn delete" onclick="app.deleteSelected()"> | |
| Usuń wszystkie | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Modal --> | |
| <div class="modal" id="modal"> | |
| <div class="modal-content"> | |
| <div class="modal-title" id="modalTitle">Tytuł</div> | |
| <div class="modal-text" id="modalText">Treść</div> | |
| <div class="modal-actions"> | |
| <button class="btn" onclick="app.closeModal()">Anuluj</button> | |
| <button class="btn btn-primary" id="modalConfirm">OK</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * Room Creator Application | |
| */ | |
| class Vector2 { | |
| constructor(x = 0, y = 0) { | |
| this.x = x; | |
| this.y = y; | |
| } | |
| } | |
| class Wall { | |
| constructor(x1, y1, x2, y2) { | |
| this.id = Date.now() + Math.random(); | |
| this.x1 = x1; | |
| this.y1 = y1; | |
| this.x2 = x2; | |
| this.y2 = y2; | |
| this.thickness = 10; // cm | |
| this.height = 280; // cm | |
| this.locked = false; | |
| } | |
| length() { | |
| return Math.sqrt((this.x2 - this.x1) ** 2 + (this.y2 - this.y1) ** 2); | |
| } | |
| angle() { | |
| return Math.atan2(this.y2 - this.y1, this.x2 - this.x1); | |
| } | |
| center() { | |
| return { | |
| x: (this.x1 + this.x2) / 2, | |
| y: (this.y1 + this.y2) / 2 | |
| }; | |
| } | |
| setLength(newLength) { | |
| const cx = (this.x1 + this.x2) / 2; | |
| const cy = (this.y1 + this.y2) / 2; | |
| const angle = this.angle(); | |
| const halfLen = newLength / 2; | |
| this.x1 = cx - Math.cos(angle) * halfLen; | |
| this.y1 = cy - Math.sin(angle) * halfLen; | |
| this.x2 = cx + Math.cos(angle) * halfLen; | |
| this.y2 = cy + Math.sin(angle) * halfLen; | |
| } | |
| move(dx, dy) { | |
| if (this.locked) return; | |
| this.x1 += dx; | |
| this.y1 += dy; | |
| this.x2 += dx; | |
| this.y2 += dy; | |
| } | |
| } | |
| class Furniture { | |
| constructor(type, x, y, width, height) { | |
| this.id = Date.now() + Math.random(); | |
| this.type = type; | |
| this.x = x; | |
| this.y = y; | |
| this.width = width; | |
| this.height = height; | |
| this.depth = 50; | |
| this.rotation = 0; | |
| this.color = this.getDefaultColor(); | |
| this.setDefaultDepth(); | |
| } | |
| setDefaultDepth() { | |
| const heights = { | |
| sofa: 80, | |
| table: 75, | |
| chair: 90, | |
| bed: 60, | |
| cabinet: 120, | |
| plant: 60, | |
| door: 210, | |
| window: 120 | |
| }; | |
| this.depth = heights[this.type] || 80; | |
| } | |
| getDefaultColor() { | |
| const colors = { | |
| sofa: '#8B4513', | |
| table: '#D2691E', | |
| chair: '#A0522D', | |
| bed: '#4682B4', | |
| cabinet: '#CD853F', | |
| plant: '#228B22', | |
| door: '#8B0000', | |
| window: '#87CEEB' | |
| }; | |
| return colors[this.type] || '#808080'; | |
| } | |
| getBounds() { | |
| const rad = this.rotation * Math.PI / 180; | |
| const cos = Math.cos(rad); | |
| const sin = Math.sin(rad); | |
| const cx = this.x + this.width / 2; | |
| const cy = this.y + this.height / 2; | |
| const corners = [ | |
| {x: -this.width/2, y: -this.height/2}, | |
| {x: this.width/2, y: -this.height/2}, | |
| {x: this.width/2, y: this.height/2}, | |
| {x: -this.width/2, y: this.height/2} | |
| ]; | |
| return corners.map(p => ({ | |
| x: cx + p.x * cos - p.y * sin, | |
| y: cy + p.x * sin + p.y * cos | |
| })); | |
| } | |
| containsPoint(px, py) { | |
| const bounds = this.getBounds(); | |
| let inside = false; | |
| for (let i = 0, j = bounds.length - 1; i < bounds.length; j = i++) { | |
| const xi = bounds[i].x, yi = bounds[i].y; | |
| const xj = bounds[j].x, yj = bounds[j].y; | |
| const intersect = ((yi > py |