anycoder-990cb508 / index.html
XTeRX's picture
Upload folder using huggingface_hub
1d5a019 verified
Raw
History Blame Contribute Delete
43.5 kB
<!DOCTYPE html>
<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"></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