add templates/index.html
This commit is contained in:
parent
2aa8bfe78d
commit
012c1b2bad
1 changed files with 635 additions and 0 deletions
635
templates/index.html
Normal file
635
templates/index.html
Normal file
|
|
@ -0,0 +1,635 @@
|
|||
<!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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue