Einstellungen -> Grundriss: Etagen, Raeume und Kacheln am Plan pflegen

Neuer Reiter mit Plan-Editor: Etagen anlegen, umbenennen, sortieren,
loeschen und als Standard markieren; Grundrissbild hochladen; Raeume mit
Name, Kuerzel und Thermostat-Zweig anlegen und bearbeiten; Kacheln im Plan
ziehen oder einen Raum ohne Kachel mit "Platzieren" und einem Klick setzen.
Loest tools/kachelpositionen.php ab - Raster, Geisterkachel und die
Umrechnung Mitte -> linke obere Ecke sind von dort uebernommen.

- restricted/grundriss.php: alle Pruefungen und Schreibzugriffe. Eine
  Etage mit Raeumen oder Automatiken laesst sich nicht loeschen, die
  Ablehnung nennt die Namen. Raeume loeschen laesst ihre Geraete ohne Raum.
- Grundrissbilder nach tiles/grundriss/ (beschreibbar, nicht im Git),
  nur PNG/JPEG/WebP, am Inhalt geprueft; SVG bewusst nicht. Prüfsumme im
  Dateinamen, alte hochgeladene Bilder werden weggeraeumt.
- ajax/settings.php: ?action=grundriss und sieben POST-Aktionen, jede
  antwortet mit dem ganzen neuen Stand.
- "Werte" springt in den Reiter Home-Kacheln und oeffnet die Kachel;
  Home-Kacheln und Geraete laden nach einer Grundriss-Aenderung nach.
- Startseite ohne Etage mit platziertem Raum: Hinweis auf die
  Einstellungen statt einer leeren Zeichenflaeche.
- README: Grundriss als Inhalt statt Code, neue Dateien und Endpunkte,
  Reihenfolge der Schema-Dateien fuer ein neues Haus.

Backend mit einer Probe-Etage durchgespielt (auch Fehlerfaelle, Upload,
Aufraeumen); Oberflaeche auf einer Probeseite mit echten Daten und
Zeiger-Ereignissen geprueft: Ziehen, Antippen, Platzieren, neuer Raum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 11:11:10 +02:00
co-authored by Claude Opus 5
parent 62a8af3fc6
commit 8d626abc6d
12 changed files with 1365 additions and 255 deletions
-214
View File
@@ -1,214 +0,0 @@
<?php
/**
* Messblatt für die Kachelpositionen im Grundriss.
*
* Die Kacheln der Home-Ansicht stehen in restricted/rooms.php als x/y in
* einer 400 × 300 großen Zeichenfläche. Wo eine neue hingehört, lässt sich
* an einem Bild nur schätzen - und der Bezugspunkt ist auch noch die linke
* obere Ecke, nicht die Mitte, die man eigentlich im Auge hat.
*
* Diese Seite nimmt beides ab: sie zeigt die Grundrisse mit Raster und den
* vorhandenen Kacheln in Originalgröße, hängt eine Geisterkachel an den
* Zeiger und schreibt darunter die Zeile, die in die Tabelle gehört.
*
* Aufruf: http://<host>/smart/tools/kachelpositionen.php
*
* Bewusst eine eigene Seite und kein Eintrag im Menü: sie wird gebraucht,
* wenn ein Raum dazukommt, und das ist selten.
*/
require_once(__DIR__ . "/../helper.php");
require_once(__DIR__ . "/../restricted/rooms.php");
if (!checkLogin()) {
http_response_code(403);
exit;
}
/** So groß ist eine Kachel im SVG - siehe restricted/home.php. */
const KACHEL_BREIT = 24;
const KACHEL_HOCH = 19;
/** So groß ist die Zeichenfläche. */
const PLAN_BREIT = 400;
const PLAN_HOCH = 300;
?><!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kachelpositionen</title>
<style>
:root {
--grund: #fbfbfa; --schrift: #1f2328; --leise: #6b7280;
--linie: #d8d8d4; --karte: #ffffff; --akzent: #00788f;
}
@media (prefers-color-scheme: dark) {
:root {
--grund: #16181c; --schrift: #e8eaed; --leise: #9aa0a6;
--linie: #2f333a; --karte: #1e2126; --akzent: #35b6cf;
}
}
body {
margin: 0; padding: 1.5rem 1rem 3rem;
background: var(--grund); color: var(--schrift);
font: 15px/1.55 -apple-system, "Segoe UI", Roboto, sans-serif;
}
.huelle { max-width: 900px; margin: 0 auto; }
h1 { font-size: 1.35rem; margin: 0 0 .3rem; }
.vorwort { color: var(--leise); margin: 0 0 1.5rem; max-width: 62ch; }
.vorwort code {
background: var(--karte); border: 1px solid var(--linie);
border-radius: .25rem; padding: .05rem .3rem; font-size: .88em;
}
.etage {
background: var(--karte); border: 1px solid var(--linie);
border-radius: .6rem; padding: .8rem; margin-bottom: 1.2rem;
}
h2 { font-size: .95rem; margin: 0 0 .5rem; font-weight: 600; }
h2 span { font-weight: 400; color: var(--leise); }
.plan {
display: block; width: 100%; height: auto; border-radius: .4rem;
cursor: crosshair; touch-action: none; background: #0b0d10;
}
.kachel { fill: rgba(0, 0, 0, .4); stroke: var(--akzent); stroke-width: .7; }
.anker { fill: var(--akzent); }
.kachelname { fill: #fff; font: 4px sans-serif; text-anchor: middle; opacity: .85; }
.achsen text { fill: #ffffffaa; font: 7px sans-serif; }
.geist { fill: rgba(255, 190, 60, .28); stroke: #ffbe3c; stroke-width: .8; }
.kreuz { stroke: #ffbe3c; stroke-width: .4; stroke-dasharray: 3 3; }
.ablesung {
margin: .6rem 0 0; font-family: ui-monospace, Consolas, monospace;
font-size: .88rem; color: var(--leise); min-height: 1.5em;
}
.ablesung b { color: var(--schrift); }
.ablesung .fest { color: var(--akzent); }
</style>
</head>
<body>
<div class="huelle">
<h1>Kachelpositionen im Grundriss</h1>
<p class="vorwort">
Die Zeichenfläche ist <?= PLAN_BREIT ?> &times; <?= PLAN_HOCH ?>. Eine Kachel ist
<?= KACHEL_BREIT ?> &times; <?= KACHEL_HOCH ?> groß, und <code>x</code>/<code>y</code>
in <code>restricted/rooms.php</code> ist ihre <b>linke obere Ecke</b> &ndash; die
Punkte an den vorhandenen Kacheln zeigen, wo dieser Bezugspunkt sitzt.
Zeig auf die Stelle, an der die <b>Mitte</b> der neuen Kachel liegen soll; das gelbe
Rechteck zeigt sie in Originalgröße, darunter stehen die Werte für die Tabelle.
Ein Klick friert die Ablesung ein.
</p>
<?php foreach ($floors as $floor): ?>
<?php
$mitKachel = roomsOnFloor($floor);
$ohneKachel = array_values(array_filter(allRooms(), function ($r) use ($floor) {
return $r["floor"] === $floor && !isset($r["x"]);
}));
?>
<section class="etage">
<h2><?= htmlspecialchars($floor) ?><?php if ($ohneKachel): ?>
<span>&middot; noch ohne Kachel:
<?= htmlspecialchars(implode(", ", array_column($ohneKachel, "mqtt"))) ?></span>
<?php endif; ?></h2>
<svg viewBox="0 0 <?= PLAN_BREIT ?> <?= PLAN_HOCH ?>" class="plan">
<defs>
<!-- Eigenes Raster je Etage: eine Kennung darf im Dokument nur
einmal vorkommen, sonst zeigt url(#...) immer auf dasselbe. -->
<pattern id="raster<?= $floor ?>" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M10 0H0V10" fill="none" stroke="#ffffff22" stroke-width=".4"/>
</pattern>
</defs>
<image href="../<?= htmlspecialchars((string)etageBild($floor)) ?>" x="0" y="0"
width="<?= PLAN_BREIT ?>" height="<?= PLAN_HOCH ?>"/>
<rect x="0" y="0" width="<?= PLAN_BREIT ?>" height="<?= PLAN_HOCH ?>"
fill="url(#raster<?= $floor ?>)"/>
<g class="achsen">
<?php for ($v = 50; $v < PLAN_BREIT; $v += 50): ?>
<text x="<?= $v ?>" y="9" text-anchor="middle"><?= $v ?></text>
<?php endfor; ?>
<?php for ($v = 50; $v < PLAN_HOCH; $v += 50): ?>
<text x="4" y="<?= $v + 3 ?>"><?= $v ?></text>
<?php endfor; ?>
</g>
<?php foreach ($mitKachel as $room): ?>
<g transform="translate(<?= $room["x"] ?>,<?= $room["y"] ?>)">
<rect class="kachel" width="<?= KACHEL_BREIT ?>" height="<?= KACHEL_HOCH ?>" rx="5"/>
<circle class="anker" r="1.6"/>
<text class="kachelname" x="<?= KACHEL_BREIT / 2 ?>" y="11">
<?= htmlspecialchars(mb_substr($room["mqtt"], 0, 8)) ?></text>
</g>
<?php endforeach; ?>
<g class="zeiger" style="display:none">
<rect class="geist" width="<?= KACHEL_BREIT ?>" height="<?= KACHEL_HOCH ?>" rx="5"/>
<line class="kreuz" x1="<?= -PLAN_BREIT ?>" x2="<?= PLAN_BREIT ?>"
y1="<?= KACHEL_HOCH / 2 ?>" y2="<?= KACHEL_HOCH / 2 ?>"/>
<line class="kreuz" y1="<?= -PLAN_HOCH ?>" y2="<?= PLAN_HOCH ?>"
x1="<?= KACHEL_BREIT / 2 ?>" x2="<?= KACHEL_BREIT / 2 ?>"/>
</g>
</svg>
<p class="ablesung">Zeiger über den Grundriss bewegen …</p>
</section>
<?php endforeach; ?>
</div>
<script>
// Muss zu den Konstanten oben passen - der Server schreibt sie hier hinein.
const BREIT = <?= KACHEL_BREIT ?>, HOCH = <?= KACHEL_HOCH ?>;
const PLAN_B = <?= PLAN_BREIT ?>, PLAN_H = <?= PLAN_HOCH ?>;
document.querySelectorAll(".etage").forEach(function (etage) {
const plan = etage.querySelector(".plan");
const zeiger = etage.querySelector(".zeiger");
const text = etage.querySelector(".ablesung");
let fest = false;
/**
* Vom Zeiger zur Kachel: gezeigt wird auf die Mitte, gebraucht wird die
* linke obere Ecke. Die Umrechnung gehoert hierher und nicht in den Kopf
* dessen, der misst.
*/
function stelle(ereignis) {
const r = plan.getBoundingClientRect();
const mx = (ereignis.clientX - r.left) / r.width * PLAN_B;
const my = (ereignis.clientY - r.top) / r.height * PLAN_H;
const x = Math.round(mx - BREIT / 2);
const y = Math.round(my - HOCH / 2);
zeiger.style.display = "";
zeiger.setAttribute("transform", "translate(" + x + "," + y + ")");
return { x: x, y: y, mx: Math.round(mx), my: Math.round(my) };
}
function zeile(p) {
return 'Mitte ' + p.mx + ',' + p.my + ' &nbsp;&rarr;&nbsp; <b>"x" =&gt; ' +
p.x + ', "y" =&gt; ' + p.y + '</b>';
}
plan.addEventListener("pointermove", function (e) {
if (fest) return;
text.innerHTML = zeile(stelle(e));
});
plan.addEventListener("pointerleave", function () {
if (fest) return;
zeiger.style.display = "none";
text.textContent = "Zeiger über den Grundriss bewegen …";
});
plan.addEventListener("click", function (e) {
const p = stelle(e);
fest = !fest;
text.innerHTML = fest
? '<span class="fest">festgehalten:</span> "x" =&gt; <b>' + p.x +
'</b>, "y" =&gt; <b>' + p.y + '</b> &nbsp;(Mitte ' + p.mx + ',' + p.my +
') &ndash; nochmal klicken zum Lösen'
: zeile(p);
});
});
</script>
</body>
</html>
+6 -2
View File
@@ -34,8 +34,12 @@
* Aufruf: http://<host>/smart/tools/lageplan.php
*
* Bewusst eine eigene Seite und kein Eintrag im Menü: sie wird gebraucht,
* wenn sich draußen etwas ändert, und das ist selten - genau wie
* kachelpositionen.php, mit der sie sich die Bauart teilt.
* wenn sich draußen etwas ändert, und das ist selten.
*
* Seit September 2026 ist sie nur noch ein Weg unter mehreren: das
* Außengelände ist eine Etage wie jede andere, und ihr Grundriss darf ein
* beliebiges hochgeladenes Bild sein (Einstellungen -> Grundriss). Diese Seite
* hilft nur, eines in Schrägsicht zu erzeugen.
*/
require_once(__DIR__ . "/../helper.php");