Files
Smart-Dashboard/restricted/aussenplan.php
T
adminandClaude Opus 5 ff74ec3c68 Der Lageplan des Aussengelaendes ist gezeichnet
Die elf Flaechen sind am Luftbild abgeklickt und danach gerichtet worden:
von Hand trifft man keine rechten Winkel, und Ecken, die derselbe Punkt sein
sollten, lagen ein paar Dezimeter auseinander.

Gerichtet wurde im Bezugssystem des Grundstuecks. Dessen Ausrichtung steckt
in den Hauskanten - laengengewichtet gemittelt ueber den vierfachen Winkel,
weil vier Kanten in vier Richtungen desselben Rasters zeigen und erst der
vierfache Winkel sie zusammenfallen laesst. Ergebnis: -9,77 Grad.

In diesem System sind die Gebaeude achsparallel. Sie durch ihr umschliessendes
Rechteck zu ersetzen macht sie damit genau rechtwinklig, und das anschliessende
Zusammenschnappen der Koordinaten - u und v getrennt - laesst sie Rechtecke
bleiben. Andersherum waere das nicht gegangen: schnappt man vorher, stehen die
Winkel wieder schief.

Hof, Garten und Pergola bleiben, wie sie abgeklickt wurden. Die ersten beiden
sind gewachsene Formen, und am dritten haengt ein Sonnensegel - die sind
selten rechteckig. Ihre Ecken schnappen trotzdem mit, sonst klafften Luecken
zu den Gebaeuden.

Ergebnis, alle acht Gebaeude auf 90,0 Grad:

  Haus         15,3 x 14,2 m      Veranda EG/OG   2,4 x 11,5 m
  Garage        6,5 x  6,4 m      Terrasse EG/OG  2,6 x  5,0 m
  Carport OG    4,0 x  6,4 m      Carport EG      9,0 x  4,3 m

Die groesste Verschiebung einer Ecke betrug 0,92 m, die meisten lagen unter
einem halben Meter.

Dazu zwei Fehler am Dach, die erst die Zeichnung zeigte: die Giebel standen
offen, sodass an den Schmalseiten die Wand durchschien, und der First lag
fest an den Ecken 0/3 und 1/2 - bei einem quer stehenden Gebaeude waere er
damit ueber die kurze Achse gelaufen. Jetzt schliessen zwei Giebeldreiecke
das Dach, und der First richtet sich nach der laengeren Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 20:24:07 +02:00

360 lines
14 KiB
PHP
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.
<?php
/**
* Der Lageplan des Außengeländes und die Projektion, die ihn zeichnet.
*
* OG, EG und UG haben je ein Rendering in assets/img. Für das Außengelände
* gibt es keins - die Planungssoftware kennt nur das einzelne Gebäude, nicht
* das Grundstück -, und eine schlichte Draufsicht taugt hier ohnehin nicht:
* Veranda und Terrasse gibt es zweimal, einmal für jede Wohnpartei, und in
* der Draufsicht liegt das obere Paar genau auf dem unteren. Man könnte die
* beiden nicht auseinanderhalten und schon gar nicht getrennt anklicken.
*
* Deshalb eine Schrägsicht von Südwesten. Von dort sieht man den Hof, auf den
* die Veranden zeigen; die Ostseite des Hauses trägt nichts, was hier
* vorkommen müsste.
*
* Gezeichnet wird in Militärprojektion (Planoblique), nicht in echter
* Perspektive:
*
* X = x·cos φ y·sin φ
* Y = (x·sin φ + y·cos φ)·STAUCHUNG z·UEBERHOEHUNG
*
* Der Grundriss bleibt dabei maßhaltig - Geraden bleiben gerade, Parallelen
* parallel, Rechtecke Rechtecke. Genau deshalb diese Projektion und keine
* perspektivische: die Flächen lassen sich am Luftbild von oben abklicken
* (tools/lageplan.php), und dieselben Zahlen ergeben ohne Umweg die
* Schrägsicht. Eine echte Perspektive müsste man neu vermessen.
*
* Das Vorzeichen des Winkels bestimmt die Himmelsrichtung: bei +35° zeigt der
* Y-Gradient nach Osten und Süden, die nächste Ecke wäre die südöstliche.
* Für Südwesten muss er negativ sein.
*
* Was die Paare trennt, ist nicht die Drehung, sondern Stauchung und
* Überhöhung: auch ohne jede Drehung stünde Terrasse OG über Terrasse EG.
* Die Drehung macht nur die Verwandtschaft zu den anderen Geschossen.
*/
require_once(__DIR__ . "/rooms.php");
/**
* Vorgabefarben je Flaeche. Sie stehen hier und nicht in der Geometrie,
* damit die Messseite und die Anzeige dasselbe Gruen fuer den Garten
* nehmen - und damit man eine Farbe an einer Stelle aendert.
*/
function aussenFarbe($name)
{
$farben = [
"Haus" => "#c8cdd6",
"Hof" => "#57585a",
"Garten" => "#2e5c33",
"Garage" => "#8a9099",
"Carport EG" => "#3f4a63", // nur Fundament, mit PV darauf
"Carport OG" => "#6f7680",
"Veranda EG" => "#9a7b52",
"Veranda OG" => "#c9a978",
"Terrasse EG" => "#9a7b52",
"Terrasse OG" => "#c9a978",
"Pergola" => "#7a6a8f",
];
return $farben[$name] ?? "#8a9099";
}
/** Blickrichtung: negativ heißt von Westen her. -35° ist Südwesten. */
const PLAN_WINKEL = -35.0;
/** Wie stark die Tiefe zusammengedrückt wird. 1 wäre die reine Draufsicht. */
const PLAN_STAUCHUNG = 0.55;
/**
* Wie hoch Höhen erscheinen. 1,4 heißt: ein Meter Höhe sieht aus wie 1,4 m
* Grundfläche. Überhöht, damit die drei Meter zwischen Terrasse EG und
* Terrasse OG deutlich werden - bei maßstäblicher Höhe klebten die beiden
* aufeinander, und genau das soll die Schrägsicht ja auflösen.
*/
const PLAN_UEBERHOEHUNG = 1.4;
/** So groß ist die Zeichenfläche, in die alles hineinpassen muss. */
const PLAN_BREITE = 400;
const PLAN_HOEHE = 300;
/** Luft zwischen Zeichnung und Rand, in Einheiten der Zeichenfläche. */
const PLAN_RAND = 12;
/**
* Die Flächen des Außengeländes, in Metern.
*
* Herkunft: tools/lageplan.php - dort werden sie am amtlichen Luftbild
* abgeklickt, die Seite gibt diese Zeilen fertig aus. Von Hand zu ändern
* lohnt nur für Kleinigkeiten; wer etwas verschiebt, misst besser neu.
*
* raum Raumname aus rooms.php, oder null für Flächen, die kein Raum sind
* (Haus, Hof, Garten - sie tragen keine Kachel)
* ecken Grundriss als [x, y] in Metern, x nach Osten, y nach Süden
* z Höhe der Fläche über Gelände (0 = ebenerdig, ~3 = OG-Ebene)
* hoehe Bauhöhe darüber (0 = flache Fläche, sonst ein Körper)
* dach Satteldach über der Bauhöhe, 0 = flach
* farbe Deckfläche; die Seiten werden daraus abgedunkelt
*
* Solange die Liste leer ist, hat AG keinen Grundriss - die Etage taucht dann
* im Menü nicht auf (siehe floorsWithPlan() in rooms.php), Zuordnung und
* Automatiken funktionieren trotzdem.
*/
function aussenFlaechen()
{
return [
["raum" => null, "z" => 0, "hoehe" => 6, "dach" => 3,
"farbe" => "#c8cdd6",
"ecken" => [[36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [21.8, 19.8]]],
["raum" => null, "z" => 0, "hoehe" => 0, "dach" => 0,
"farbe" => "#57585a",
"ecken" => [[9.3, 26.1], [8.9, 36.4], [11.0, 36.0], [18.9, 34.7], [19.0, 29.8], [20.4, 28.7], [19.8, 25.6], [19.6, 24.3]]],
["raum" => "Carport EG", "z" => 0, "hoehe" => 2.2, "dach" => 0,
"farbe" => "#3f4a63",
"ecken" => [[18.9, 34.7], [19.6, 38.9], [10.7, 40.5], [10.0, 36.2]]],
["raum" => "Carport OG", "z" => 0, "hoehe" => 2.5, "dach" => 0,
"farbe" => "#6f7680",
"ecken" => [[12.2, 19.1], [13.2, 25.4], [9.3, 26.1], [8.2, 19.8]]],
["raum" => "Veranda EG", "z" => 0, "hoehe" => 0, "dach" => 0,
"farbe" => "#9a7b52",
"ecken" => [[24.1, 19.4], [26.1, 30.8], [23.7, 31.2], [21.8, 19.8]]],
["raum" => "Veranda OG", "z" => 3, "hoehe" => 0, "dach" => 0,
"farbe" => "#c9a978",
"ecken" => [[24.1, 19.4], [26.1, 30.8], [23.7, 31.2], [21.8, 19.8]]],
["raum" => "Terrasse EG", "z" => 0, "hoehe" => 0, "dach" => 0,
"farbe" => "#9a7b52",
"ecken" => [[23.7, 31.2], [28.6, 30.3], [29.1, 32.9], [24.2, 33.8]]],
["raum" => "Terrasse OG", "z" => 3, "hoehe" => 0, "dach" => 0,
"farbe" => "#c9a978",
"ecken" => [[23.7, 31.2], [28.6, 30.3], [29.1, 32.9], [24.2, 33.8]]],
["raum" => "Pergola", "z" => 0, "hoehe" => 0, "dach" => 0,
"farbe" => "#7a6a8f",
"ecken" => [[48.0, 36.5], [43.8, 36.4], [46.0, 31.5], [49.4, 32.4]]],
["raum" => "Garage", "z" => 0, "hoehe" => 2.5, "dach" => 1.5,
"farbe" => "#8a9099",
"ecken" => [[12.2, 19.1], [13.2, 25.4], [19.6, 24.3], [18.5, 18.0]]],
["raum" => "Garten", "z" => 0, "hoehe" => 0, "dach" => 0,
"farbe" => "#2e5c33",
"ecken" => [[8.6, 40.8], [7.9, 45.2], [49.0, 38.2], [50.6, 12.5], [23.7, 17.1], [24.1, 19.4], [36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [22.5, 23.8], [19.8, 25.6], [20.4, 28.7], [19.0, 29.8], [19.6, 38.9]]],
];
}
/**
* Die Projektion selbst. Liefert eine Funktion, die (x, y, z) in Metern auf
* (X, Y) in projizierten Metern abbildet - noch ohne Bezug zur Zeichenfläche.
*/
function aussenProjektor()
{
$w = deg2rad(PLAN_WINKEL);
$c = cos($w);
$s = sin($w);
return function ($x, $y, $z) use ($c, $s) {
return [$x * $c - $y * $s,
($x * $s + $y * $c) * PLAN_STAUCHUNG - $z * PLAN_UEBERHOEHUNG];
};
}
/**
* Alles Gezeichnete in die Zeichenfläche einpassen.
*
* Erst wird projiziert, dann ein Maßstab gesucht, in den die ganze Zeichnung
* passt - nicht umgekehrt. Die Zeichenfläche ist 400 × 300 und hat mit Metern
* nichts zu tun; wer den Maßstab vorher festlegt, schneidet entweder etwas ab
* oder lässt die Hälfte leer.
*
* Wichtig: Messseite und Anzeige müssen dieselbe Einpassung benutzen, sonst
* sitzen die Kacheln woanders als die Flächen, auf denen sie liegen sollen.
* Deshalb steht sie hier und nicht zweimal.
*/
function aussenEinpassung($flaechen)
{
$P = aussenProjektor();
$xs = [];
$ys = [];
foreach ($flaechen as $f) {
$oben = ($f["z"] ?? 0) + ($f["hoehe"] ?? 0) + ($f["dach"] ?? 0);
foreach ($f["ecken"] as $e) {
foreach ([$f["z"] ?? 0, $oben] as $z) {
list($X, $Y) = $P($e[0], $e[1], $z);
$xs[] = $X;
$ys[] = $Y;
}
}
}
if (!$xs) {
return null;
}
$breite = max($xs) - min($xs);
$hoehe = max($ys) - min($ys);
// Der kleinere der beiden Maßstäbe passt in beide Richtungen.
$massstab = min(($breite > 0 ? (PLAN_BREITE - 2 * PLAN_RAND) / $breite : INF),
($hoehe > 0 ? (PLAN_HOEHE - 2 * PLAN_RAND) / $hoehe : INF));
if (!is_finite($massstab)) {
$massstab = 1.0;
}
return [
"massstab" => $massstab,
// Zusätzlich zentrieren: was schmaler ist als die Fläche, soll nicht
// in der Ecke kleben.
"dx" => (PLAN_BREITE - $breite * $massstab) / 2 - min($xs) * $massstab,
"dy" => (PLAN_HOEHE - $hoehe * $massstab) / 2 - min($ys) * $massstab,
];
}
/** Ein Punkt in Metern zu einem Punkt auf der Zeichenfläche. */
function aussenPunkt($einpassung, $x, $y, $z)
{
$P = aussenProjektor();
list($X, $Y) = $P($x, $y, $z);
return [$X * $einpassung["massstab"] + $einpassung["dx"],
$Y * $einpassung["massstab"] + $einpassung["dy"]];
}
/** Mittelwert einer Punktliste - gut genug als Ankerpunkt einer Fläche. */
function aussenMitte($punkte)
{
$sx = 0.0;
$sy = 0.0;
foreach ($punkte as $p) {
$sx += $p[0];
$sy += $p[1];
}
$n = max(1, count($punkte));
return [$sx / $n, $sy / $n];
}
/** Eine Farbe abdunkeln - die Seitenflächen liegen im Schatten. */
function aussenSchatten($hex, $faktor)
{
$n = hexdec(ltrim($hex, "#"));
return sprintf("#%02x%02x%02x",
(int) round(($n >> 16) * $faktor),
(int) round((($n >> 8) & 255) * $faktor),
(int) round(($n & 255) * $faktor));
}
/**
* Die Zeichnung als Liste von Polygonen, von hinten nach vorn sortiert.
*
* Sortiert wird nach dem hintersten Punkt der Grundfläche (Malerreihenfolge).
* Das reicht hier: die Körper stehen nebeneinander auf dem Boden und
* durchdringen sich nicht. Bei ineinander verschachtelten Körpern bräuchte es
* mehr, aber ein Grundstück ist keine Baugruppe.
*
* Returns:
* ["polygone" => [["punkte" => [[x,y],...], "farbe" => "#rrggbb"], ...],
* "anker" => [Raumname => [x, y], ...]] Mitte der Deckfläche
*/
function aussenZeichnung()
{
$flaechen = aussenFlaechen();
$einpassung = aussenEinpassung($flaechen);
if (!$einpassung) {
return ["polygone" => [], "anker" => []];
}
$stuecke = [];
foreach ($flaechen as $f) {
$z = $f["z"] ?? 0;
$hoehe = $f["hoehe"] ?? 0;
$dach = $f["dach"] ?? 0;
$farbe = $f["farbe"] ?? "#8a9099";
$tiefe = INF;
foreach ($f["ecken"] as $e) {
$p = aussenPunkt($einpassung, $e[0], $e[1], 0);
$tiefe = min($tiefe, $p[1]);
}
$unten = [];
$oben = [];
foreach ($f["ecken"] as $e) {
$unten[] = aussenPunkt($einpassung, $e[0], $e[1], $z);
$oben[] = aussenPunkt($einpassung, $e[0], $e[1], $z + $hoehe);
}
$polygone = [];
if ($hoehe > 0) {
$anzahl = count($f["ecken"]);
for ($i = 0; $i < $anzahl; $i++) {
$j = ($i + 1) % $anzahl;
$polygone[] = ["punkte" => [$unten[$i], $unten[$j], $oben[$j], $oben[$i]],
"farbe" => aussenSchatten($farbe, 0.62)];
}
}
$polygone[] = ["punkte" => $oben, "farbe" => $farbe];
if ($dach > 0 && count($f["ecken"]) === 4) {
$e = $f["ecken"];
// Der First laeuft ueber die laengere Achse - er verbindet die
// Mitten der beiden kuerzeren Seiten. Fest verdrahtet waere das
// bei einem quer stehenden Gebaeude falsch herum.
$laenge = function ($a, $b) {
return hypot($a[0] - $b[0], $a[1] - $b[1]);
};
$langsSeite01 = $laenge($e[0], $e[1]) >= $laenge($e[1], $e[2]);
$paar = $langsSeite01 ? [[0, 3], [1, 2]] : [[0, 1], [2, 3]];
$first = [];
foreach ($paar as $pp) {
$first[] = aussenPunkt($einpassung,
($e[$pp[0]][0] + $e[$pp[1]][0]) / 2,
($e[$pp[0]][1] + $e[$pp[1]][1]) / 2,
$z + $hoehe + $dach);
}
$dachfarbe = $f["dachfarbe"] ?? "#b5563a";
// Erst die beiden Giebeldreiecke, dann die Dachflaechen darueber:
// ohne sie klaffte an den Schmalseiten ein Loch, durch das die
// Wand darunter zu sehen war.
foreach ($paar as $i => $pp) {
$polygone[] = ["punkte" => [$oben[$pp[0]], $oben[$pp[1]], $first[$i]],
"farbe" => aussenSchatten($farbe, 0.88)];
}
if ($langsSeite01) {
$polygone[] = ["punkte" => [$oben[0], $oben[1], $first[1], $first[0]],
"farbe" => $dachfarbe];
$polygone[] = ["punkte" => [$oben[3], $oben[2], $first[1], $first[0]],
"farbe" => aussenSchatten($dachfarbe, 0.78)];
} else {
$polygone[] = ["punkte" => [$oben[1], $oben[2], $first[1], $first[0]],
"farbe" => $dachfarbe];
$polygone[] = ["punkte" => [$oben[0], $oben[3], $first[1], $first[0]],
"farbe" => aussenSchatten($dachfarbe, 0.78)];
}
}
$stuecke[] = ["tiefe" => $tiefe, "polygone" => $polygone,
"raum" => $f["raum"] ?? null, "mitte" => aussenMitte($oben)];
}
usort($stuecke, function ($a, $b) {
return $a["tiefe"] <=> $b["tiefe"];
});
$polygone = [];
$anker = [];
foreach ($stuecke as $s) {
foreach ($s["polygone"] as $p) {
$polygone[] = $p;
}
if ($s["raum"] !== null) {
$anker[$s["raum"]] = $s["mitte"];
}
}
return ["polygone" => $polygone, "anker" => $anker];
}
/** Die Zeichnung als fertige SVG-Zeilen. */
function aussenSvg()
{
$zeichnung = aussenZeichnung();
$svg = "";
foreach ($zeichnung["polygone"] as $p) {
$punkte = [];
foreach ($p["punkte"] as $pt) {
$punkte[] = round($pt[0], 1) . "," . round($pt[1], 1);
}
$svg .= '<polygon points="' . implode(" ", $punkte) . '" fill="' . $p["farbe"]
. '" stroke="#ffffff30" stroke-width=".4"/>' . "\n";
}
return $svg;
}