grundriss/templates/index.html
2026-07-27 20:38:38 +02:00

635 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grundriss Planer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
header { background: #16213e; padding: 16px 24px; display: flex; align-items: center; gap: 16px; border-bottom: 2px solid #0f3460; }
header h1 { font-size: 1.3rem; color: #e94560; flex: 1; }
header h1 span { color: #888; font-weight: normal; font-size: 0.9rem; }
.btn { padding: 8px 18px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all .2s; }
.btn-primary { background: #e94560; color: #fff; }
.btn-primary:hover { background: #d63851; }
.btn-secondary { background: #0f3460; color: #e0e0e0; }
.btn-secondary:hover { background: #1a4a8a; }
.btn-danger { background: #c0392b; color: #fff; }
.btn-danger:hover { background: #a93226; }
.btn-sm { padding: 4px 10px; font-size: 0.8rem; }
.btn-success { background: #27ae60; color: #fff; }
.btn-success:hover { background: #219a52; }
.container { display: flex; height: calc(100vh - 60px); }
/* Sidebar */
.sidebar { width: 320px; min-width: 320px; background: #16213e; padding: 16px; overflow-y: auto; border-right: 2px solid #0f3460; }
.sidebar h2 { font-size: 1rem; color: #e94560; margin-bottom: 12px; }
.sidebar h3 { font-size: 0.9rem; color: #aaa; margin: 16px 0 8px; }
.project-list { display: flex; flex-direction: column; gap: 6px; }
.project-item { background: #1a1a2e; padding: 10px 12px; border-radius: 6px; cursor: pointer; border: 1px solid transparent; transition: all .2s; }
.project-item:hover { border-color: #0f3460; }
.project-item.active { border-color: #e94560; background: #1a1a3e; }
.project-item .name { font-weight: 600; }
.project-item .meta { font-size: 0.75rem; color: #888; margin-top: 2px; }
.room-list { display: flex; flex-direction: column; gap: 4px; }
.room-item { background: #1a1a2e; padding: 8px 10px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
.room-item .rname { font-weight: 500; }
.room-item .rdim { font-size: 0.8rem; color: #888; }
.room-item .rcolor { width: 14px; height: 14px; border-radius: 3px; display: inline-block; margin-right: 6px; }
.wall-input { display: flex; gap: 6px; margin-bottom: 6px; align-items: center; }
.wall-input input { flex: 1; background: #1a1a2e; border: 1px solid #333; color: #e0e0e0; padding: 6px 8px; border-radius: 4px; font-size: 0.85rem; }
.wall-input input:focus { outline: none; border-color: #e94560; }
.wall-input .unit { color: #888; font-size: 0.8rem; width: 20px; }
.measurements { margin-top: 12px; }
.measurement { display: flex; justify-content: space-between; padding: 4px 0; font-size: 0.85rem; border-bottom: 1px solid #1a1a2e; }
.measurement .label { color: #aaa; }
.measurement .value { font-weight: 600; color: #27ae60; }
.measurement .value.unknown { color: #e67e22; }
/* Canvas area */
.canvas-area { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden; background: #0d0d1a; }
.canvas-area canvas { cursor: crosshair; }
.canvas-toolbar { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; background: #16213e; padding: 8px 16px; border-radius: 8px; border: 1px solid #0f3460; z-index: 10; }
.canvas-toolbar label { display: flex; align-items: center; gap: 6px; font-size: 0.85rem; color: #aaa; }
.canvas-toolbar select { background: #1a1a2e; border: 1px solid #333; color: #e0e0e0; padding: 4px 8px; border-radius: 4px; }
.canvas-info { position: absolute; bottom: 12px; left: 12px; background: #16213ee0; padding: 8px 12px; border-radius: 6px; font-size: 0.8rem; color: #888; z-index: 10; }
/* Modal */
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.show { display: flex; }
.modal { background: #16213e; border-radius: 12px; padding: 24px; min-width: 400px; max-width: 500px; border: 1px solid #0f3460; }
.modal h2 { margin-bottom: 16px; color: #e94560; }
.modal label { display: block; margin-bottom: 4px; color: #aaa; font-size: 0.85rem; }
.modal input, .modal select { width: 100%; background: #1a1a2e; border: 1px solid #333; color: #e0e0e0; padding: 8px 10px; border-radius: 4px; margin-bottom: 12px; }
.modal .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
/* Empty state */
.empty-state { text-align: center; padding: 40px 20px; color: #666; }
.empty-state .icon { font-size: 3rem; margin-bottom: 12px; }
.empty-state p { margin-bottom: 8px; }
/* Dimension labels on canvas */
.dim-label { position: absolute; font-size: 11px; color: #e94560; font-weight: 600; pointer-events: none; z-index: 5; }
</style>
</head>
<body>
<header>
<h1>🏠 Grundriss Planer <span id="projectTitle"> Kein Projekt geladen</span></h1>
<button class="btn btn-primary" onclick="showNewProjectModal()">+ Neues Projekt</button>
<button class="btn btn-secondary" onclick="showNewRoomModal()" id="btnAddRoom" disabled>+ Raum</button>
</header>
<div class="container">
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<h2>📁 Projekte</h2>
<div class="project-list" id="projectList"></div>
<div id="roomSection" style="display:none">
<h2>🏠 Räume</h2>
<div class="room-list" id="roomList"></div>
<h3>📏 Bekannte Maße</h3>
<div id="wallInputs"></div>
<button class="btn btn-secondary btn-sm" onclick="addWallInput()">+ Wandmaß hinzufügen</button>
<h3>📐 Berechnete Maße</h3>
<div class="measurements" id="measurements"></div>
</div>
</div>
<!-- Canvas -->
<div class="canvas-area" id="canvasArea">
<div class="canvas-toolbar" id="canvasToolbar" style="display:none">
<label>Werkzeug:
<select id="toolSelect">
<option value="select">Auswählen</option>
<option value="wall">Wand zeichnen</option>
<option value="move">Verschieben</option>
</select>
</label>
<label>Raster: <input type="checkbox" id="gridToggle" checked></label>
<button class="btn btn-secondary btn-sm" onclick="fitCanvas()">🔲 Passend</button>
</div>
<canvas id="floorplan"></canvas>
<div class="canvas-info" id="canvasInfo">Kein Raum ausgewählt</div>
</div>
</div>
<!-- New Project Modal -->
<div class="modal-overlay" id="newProjectModal">
<div class="modal">
<h2>Neues Projekt</h2>
<label>Projektname</label>
<input type="text" id="newProjectName" placeholder="z.B. Meine Wohnung" value="Meine Wohnung">
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal('newProjectModal')">Abbrechen</button>
<button class="btn btn-primary" onclick="createProject()">Erstellen</button>
</div>
</div>
</div>
<!-- New Room Modal -->
<div class="modal-overlay" id="newRoomModal">
<div class="modal">
<h2>Neuen Raum hinzufügen</h2>
<label>Raumname</label>
<input type="text" id="newRoomName" placeholder="z.B. Wohnzimmer">
<label>Farbe</label>
<input type="color" id="newRoomColor" value="#e94560">
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal('newRoomModal')">Abbrechen</button>
<button class="btn btn-primary" onclick="addRoom()">Hinzufügen</button>
</div>
</div>
</div>
<script>
// ===== STATE =====
const STATE = {
projects: [],
currentProject: null,
currentRoomIdx: -1,
rooms: [],
walls: [], // known wall measurements per room
// Canvas state
scale: 10, // pixels per cm
offsetX: 0,
offsetY: 0,
isDragging: false,
dragStart: null,
selectedRoom: -1,
// Wall drawing
drawingWall: false,
wallStart: null,
};
const COLORS = ['#e94560','#0f3460','#27ae60','#e67e22','#9b59b6','#3498db','#1abc9c','#f39c12','#e74c3c','#2ecc71'];
// ===== API =====
async function api(method, path, body) {
const opts = { method, headers: {'Content-Type':'application/json'} };
if (body) opts.body = JSON.stringify(body);
const r = await fetch('/api' + path, opts);
return r.json();
}
// ===== PROJECTS =====
async function loadProjects() {
STATE.projects = await api('GET', '/projects');
renderProjectList();
}
function renderProjectList() {
const el = document.getElementById('projectList');
if (STATE.projects.length === 0) {
el.innerHTML = '<div class="empty-state"><div class="icon">📐</div><p>Noch keine Projekte</p><p style="font-size:0.8rem">Erstelle ein neues Projekt um zu starten</p></div>';
return;
}
el.innerHTML = STATE.projects.map(p => `
<div class="project-item ${STATE.currentProject && STATE.currentProject.id === p.id ? 'active' : ''}"
onclick="loadProject('${p.id}')">
<div class="name">${p.name}</div>
<div class="meta">${p.rooms} Räume · ${p.updated ? new Date(p.updated).toLocaleDateString('de-DE') : ''}</div>
</div>
`).join('');
}
async function createProject() {
const name = document.getElementById('newProjectName').value || 'Meine Wohnung';
const result = await api('POST', '/projects', { name });
closeModal('newProjectModal');
await loadProjects();
await loadProject(result.id);
}
async function loadProject(pid) {
const data = await api('GET', '/projects/' + pid);
if (data.error) return;
STATE.currentProject = data;
STATE.rooms = data.rooms || [];
STATE.currentRoomIdx = -1;
STATE.selectedRoom = -1;
document.getElementById('projectTitle').textContent = ' ' + data.name;
document.getElementById('btnAddRoom').disabled = false;
document.getElementById('roomSection').style.display = 'block';
document.getElementById('canvasToolbar').style.display = 'flex';
renderRoomList();
renderCanvas();
renderProjectList();
}
// ===== ROOMS =====
function showNewRoomModal() {
document.getElementById('newRoomName').value = '';
document.getElementById('newRoomColor').value = COLORS[STATE.rooms.length % COLORS.length];
document.getElementById('newRoomModal').classList.add('show');
}
async function addRoom() {
const name = document.getElementById('newRoomName').value || 'Raum ' + (STATE.rooms.length + 1);
const color = document.getElementById('newRoomColor').value;
const room = {
id: 'room_' + Date.now(),
name,
color,
walls: [], // {id, length_cm, label, x1,y1,x2,y2} - placed on canvas
known_measurements: [], // {id, label, length_cm}
x: 50, y: 50, // position on canvas
};
STATE.rooms.push(room);
STATE.currentRoomIdx = STATE.rooms.length - 1;
STATE.selectedRoom = STATE.currentRoomIdx;
await saveCurrentProject();
renderRoomList();
renderCanvas();
closeModal('newRoomModal');
}
function renderRoomList() {
const el = document.getElementById('roomList');
if (STATE.rooms.length === 0) {
el.innerHTML = '<div style="color:#666;font-size:0.85rem;padding:8px">Noch keine Räume. Klick auf "+ Raum"</div>';
return;
}
el.innerHTML = STATE.rooms.map((r, i) => `
<div class="room-item ${i === STATE.currentRoomIdx ? 'active' : ''}" onclick="selectRoom(${i})">
<div>
<span class="rcolor" style="background:${r.color}"></span>
<span class="rname">${r.name}</span>
<span class="rdim">${r.known_measurements ? r.known_measurements.length : 0} Maße</span>
</div>
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteRoom(${i})">✕</button>
</div>
`).join('');
}
function selectRoom(idx) {
STATE.currentRoomIdx = idx;
STATE.selectedRoom = idx;
renderRoomList();
renderWallInputs();
renderMeasurements();
renderCanvas();
}
async function deleteRoom(idx) {
if (!confirm(`"${STATE.rooms[idx].name}" wirklich löschen?`)) return;
STATE.rooms.splice(idx, 1);
if (STATE.currentRoomIdx >= STATE.rooms.length) STATE.currentRoomIdx = STATE.rooms.length - 1;
if (STATE.selectedRoom >= STATE.rooms.length) STATE.selectedRoom = STATE.rooms.length - 1;
await saveCurrentProject();
renderRoomList();
renderWallInputs();
renderMeasurements();
renderCanvas();
}
// ===== WALL INPUTS =====
function renderWallInputs() {
const el = document.getElementById('wallInputs');
const room = STATE.rooms[STATE.currentRoomIdx];
if (!room) { el.innerHTML = ''; return; }
const ms = room.known_measurements || [];
if (ms.length === 0) {
el.innerHTML = '<div style="color:#666;font-size:0.85rem;padding:8px">Noch keine Maße. Füge Wandmaße hinzu!</div>';
return;
}
el.innerHTML = ms.map((m, i) => `
<div class="wall-input">
<input type="text" value="${m.label}" placeholder="z.B. Wand A" onchange="updateWallLabel(${i}, this.value)" style="width:80px">
<input type="number" value="${m.length_cm}" placeholder="cm" min="1" onchange="updateWallLength(${i}, this.value)" style="width:80px">
<span class="unit">cm</span>
<button class="btn btn-danger btn-sm" onclick="deleteWall(${i})">✕</button>
</div>
`).join('');
}
function addWallInput() {
const room = STATE.rooms[STATE.currentRoomIdx];
if (!room) return;
if (!room.known_measurements) room.known_measurements = [];
const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const label = labels[room.known_measurements.length % labels.length];
room.known_measurements.push({ id: 'm_' + Date.now(), label: 'Wand ' + label, length_cm: 0 });
renderWallInputs();
renderMeasurements();
saveCurrentProject();
}
function updateWallLabel(idx, val) {
const room = STATE.rooms[STATE.currentRoomIdx];
if (!room) return;
room.known_measurements[idx].label = val;
renderMeasurements();
saveCurrentProject();
}
function updateWallLength(idx, val) {
const room = STATE.rooms[STATE.currentRoomIdx];
if (!room) return;
room.known_measurements[idx].length_cm = parseInt(val) || 0;
renderMeasurements();
renderCanvas();
saveCurrentProject();
}
function deleteWall(idx) {
const room = STATE.rooms[STATE.currentRoomIdx];
if (!room) return;
room.known_measurements.splice(idx, 1);
renderWallInputs();
renderMeasurements();
renderCanvas();
saveCurrentProject();
}
// ===== MEASUREMENTS =====
function renderMeasurements() {
const el = document.getElementById('measurements');
const room = STATE.rooms[STATE.currentRoomIdx];
if (!room) { el.innerHTML = ''; return; }
const ms = room.known_measurements || [];
if (ms.length === 0) {
el.innerHTML = '<div style="color:#666;font-size:0.85rem;padding:8px">Füge Maße hinzu um Berechnungen zu sehen</div>';
return;
}
const known = ms.filter(m => m.length_cm > 0);
const unknown = ms.filter(m => !m.length_cm || m.length_cm === 0);
let html = '';
known.forEach(m => {
html += `<div class="measurement"><span class="label">${m.label}</span><span class="value">${m.length_cm} cm</span></div>`;
});
unknown.forEach(m => {
html += `<div class="measurement"><span class="label">${m.label}</span><span class="value unknown">? cm (unbekannt)</span></div>`;
});
// Try to calculate unknown from known
if (known.length > 0 && unknown.length > 0) {
const totalKnown = known.reduce((s, m) => s + m.length_cm, 0);
const avg = Math.round(totalKnown / known.length);
html += `<div class="measurement" style="border-top:1px solid #333;margin-top:4px;padding-top:6px">
<span class="label">📐 Geschätzt (∅ ${avg} cm)</span>
<span class="value unknown">pro Wand</span>
</div>`;
}
el.innerHTML = html;
}
// ===== CANVAS =====
const canvas = document.getElementById('floorplan');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
const area = document.getElementById('canvasArea');
canvas.width = area.clientWidth - 4;
canvas.height = area.clientHeight - 4;
renderCanvas();
}
function fitCanvas() {
if (STATE.rooms.length === 0) return;
// Find bounding box
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
STATE.rooms.forEach(r => {
if (r.x < minX) minX = r.x;
if (r.y < minY) minY = r.y;
if (r.x + 400 > maxX) maxX = r.x + 400;
if (r.y + 400 > maxY) maxY = r.y + 400;
});
if (minX === Infinity) return;
const margin = 100;
const bw = maxX - minX + margin * 2;
const bh = maxY - minY + margin * 2;
const scaleX = (canvas.width - 40) / bw;
const scaleY = (canvas.height - 40) / bh;
STATE.scale = Math.min(scaleX, scaleY, 15);
STATE.offsetX = -minX * STATE.scale + margin * STATE.scale / 2 + 20;
STATE.offsetY = -minY * STATE.scale + margin * STATE.scale / 2 + 20;
renderCanvas();
}
function renderCanvas() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Grid
const gridToggle = document.getElementById('gridToggle');
if (gridToggle && gridToggle.checked) {
const gridStep = 50 * STATE.scale; // 50cm grid
ctx.strokeStyle = '#1a1a3e';
ctx.lineWidth = 0.5;
const startX = STATE.offsetX % gridStep;
const startY = STATE.offsetY % gridStep;
for (let x = startX; x < W; x += gridStep) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = startY; y < H; y += gridStep) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
}
// Draw rooms
STATE.rooms.forEach((room, idx) => {
const x = room.x * STATE.scale + STATE.offsetX;
const y = room.y * STATE.scale + STATE.offsetY;
const w = 360 * STATE.scale; // default 360cm
const h = 360 * STATE.scale;
// Room background
ctx.fillStyle = room.color + '22';
ctx.fillRect(x, y, w, h);
// Room border
ctx.strokeStyle = idx === STATE.selectedRoom ? '#e94560' : room.color;
ctx.lineWidth = idx === STATE.selectedRoom ? 3 : 2;
ctx.strokeRect(x, y, w, h);
// Room label
ctx.fillStyle = '#e0e0e0';
ctx.font = '14px -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(room.name, x + w/2, y - 8);
// Known measurements as dimension lines
const ms = room.known_measurements || [];
ms.forEach((m, mi) => {
if (!m.length_cm) return;
const wallPos = mi % 4; // 0=top, 1=right, 2=bottom, 3=left
let lx1, ly1, lx2, ly2;
const offset = 20 + mi * 14;
switch(wallPos) {
case 0: // top
lx1 = x; ly1 = y - offset; lx2 = x + w; ly2 = y - offset;
break;
case 1: // right
lx1 = x + w + offset; ly1 = y; lx2 = x + w + offset; ly2 = y + h;
break;
case 2: // bottom
lx1 = x; ly1 = y + h + offset; lx2 = x + w; ly2 = y + h + offset;
break;
case 3: // left
lx1 = x - offset; ly1 = y; lx2 = x - offset; ly2 = y + h;
break;
}
ctx.strokeStyle = '#27ae60';
ctx.lineWidth = 1;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(lx1, ly1); ctx.lineTo(lx2, ly2); ctx.stroke();
ctx.setLineDash([]);
// Arrow tips
const angle = wallPos <= 1 ? 0 : Math.PI/2;
const ax = wallPos <= 1 ? lx1 : lx1;
const ay = wallPos <= 1 ? ly1 : ly1;
ctx.fillStyle = '#27ae60';
// Label
ctx.font = '11px -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${m.label}: ${m.length_cm}cm`, (lx1+lx2)/2, ly1 - 4);
});
// Unknown measurements
ms.forEach((m, mi) => {
if (m.length_cm) return;
const wallPos = mi % 4;
let lx1, ly1, lx2, ly2;
const offset = 20 + mi * 14;
switch(wallPos) {
case 0: lx1 = x; ly1 = y - offset; lx2 = x + w; ly2 = y - offset; break;
case 1: lx1 = x + w + offset; ly1 = y; lx2 = x + w + offset; ly2 = y + h; break;
case 2: lx1 = x; ly1 = y + h + offset; lx2 = x + w; ly2 = y + h + offset; break;
case 3: lx1 = x - offset; ly1 = y; lx2 = x - offset; ly2 = y + h; break;
}
ctx.strokeStyle = '#e67e22';
ctx.lineWidth = 1;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(lx1, ly1); ctx.lineTo(lx2, ly2); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#e67e22';
ctx.font = '11px -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${m.label}: ? cm`, (lx1+lx2)/2, ly1 - 4);
});
});
// Info
const room = STATE.rooms[STATE.selectedRoom];
document.getElementById('canvasInfo').textContent = room
? `${room.name} · ${(room.known_measurements||[]).filter(m=>m.length_cm>0).length} bekannte Maße`
: 'Kein Raum ausgewählt';
}
// ===== CANVAS EVENTS =====
let isPanning = false, panStart = null;
canvas.addEventListener('mousedown', e => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
// Check if clicking on a room
for (let i = STATE.rooms.length - 1; i >= 0; i--) {
const r = STATE.rooms[i];
const x = r.x * STATE.scale + STATE.offsetX;
const y = r.y * STATE.scale + STATE.offsetY;
const w = 360 * STATE.scale;
const h = 360 * STATE.scale;
if (mx >= x && mx <= x + w && my >= y && my <= y + h) {
STATE.selectedRoom = i;
STATE.currentRoomIdx = i;
renderRoomList();
renderWallInputs();
renderMeasurements();
renderCanvas();
return;
}
}
// Pan canvas
isPanning = true;
panStart = { x: e.clientX, y: e.clientY, ox: STATE.offsetX, oy: STATE.offsetY };
});
canvas.addEventListener('mousemove', e => {
if (isPanning) {
STATE.offsetX = panStart.ox + (e.clientX - panStart.x);
STATE.offsetY = panStart.oy + (e.clientY - panStart.y);
renderCanvas();
}
});
canvas.addEventListener('mouseup', () => { isPanning = false; });
canvas.addEventListener('mouseleave', () => { isPanning = false; });
// Touch support
canvas.addEventListener('touchstart', e => {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const mx = touch.clientX - rect.left;
const my = touch.clientY - rect.top;
for (let i = STATE.rooms.length - 1; i >= 0; i--) {
const r = STATE.rooms[i];
const x = r.x * STATE.scale + STATE.offsetX;
const y = r.y * STATE.scale + STATE.offsetY;
const w = 360 * STATE.scale;
const h = 360 * STATE.scale;
if (mx >= x && mx <= x + w && my >= y && my <= y + h) {
STATE.selectedRoom = i;
STATE.currentRoomIdx = i;
renderRoomList();
renderWallInputs();
renderMeasurements();
renderCanvas();
return;
}
}
isPanning = true;
panStart = { x: touch.clientX, y: touch.clientY, ox: STATE.offsetX, oy: STATE.offsetY };
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
if (isPanning && e.touches.length === 1) {
const touch = e.touches[0];
STATE.offsetX = panStart.ox + (touch.clientX - panStart.x);
STATE.offsetY = panStart.oy + (touch.clientY - panStart.y);
renderCanvas();
}
}, { passive: false });
canvas.addEventListener('touchend', () => { isPanning = false; });
// ===== MODALS =====
function showNewProjectModal() { document.getElementById('newProjectModal').classList.add('show'); }
function closeModal(id) { document.getElementById(id).classList.remove('show'); }
// ===== SAVE =====
async function saveCurrentProject() {
if (!STATE.currentProject) return;
STATE.currentProject.rooms = STATE.rooms;
await api('PUT', '/projects/' + STATE.currentProject.id, { rooms: STATE.rooms });
}
// ===== INIT =====
window.addEventListener('resize', resizeCanvas);
document.addEventListener('DOMContentLoaded', () => {
resizeCanvas();
loadProjects();
});
</script>
</body>
</html>