Vier Korrekturen am Aussenplan: Die Waende sind Kaesten und keine Flaechen mehr. An einer Oeffnung stand sonst ein Blatt Papier im Raum - Innenseite, Oberkante und die beiden Schnittkanten fehlten, und genau das sah seltsam aus. Die Dicke wird nur gerechnet, wo ein Koerper ueberhaupt offen ist; bei einer geschlossenen Wand sieht man sie nie. Der First der Garage lief vorher schon richtig, Nord-Sued. Die Drehung um 90 Grad war ein Missverstaendnis und ist zurueckgenommen - jetzt steht die Achse ausgeschrieben da, damit bei diesem fast quadratischen Grundriss nicht wieder die zweite Nachkommastelle entscheidet. Die halb offenen Nord- und Suedwaende des EG-Carports sind wieder zu. Die Faehigkeit bleibt im Zeichner, sie wird hier nur nicht mehr gebraucht. Beide Pultdaecher sind auf ein Drittel abgeflacht: 0,27 m statt 0,8 beim OG-Carport, 0,23 statt 0,7 beim EG-Carport - rund vier Prozent Neigung. Beim OG-Carport ist die Wandhoehe dafuer auf 2,23 m gesetzt, damit die hohe Seite bei 2,50 m herauskommt und dort an die Traufe des Garagendachs anschliesst. Nachgerechnet: beide bei 2,50 m. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
691 lines
29 KiB
PHP
691 lines
29 KiB
PHP
<?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" => "Garten", "z" => 0, "hoehe" => 0, "farbe" => "#4a6b4e",
|
||
"ecken" => [[8.6, 40.6], [7.9, 45.0], [49.0, 38.4], [50.7, 12.9], [23.6, 16.7],
|
||
[24.3, 19.5], [36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [22.7, 24.2],
|
||
[19.9, 25.6], [20.2, 28.7], [19.0, 29.8], [19.3, 39.1]]],
|
||
|
||
["raum" => null, "z" => 0, "hoehe" => 0, "farbe" => "#6e7278", // Hof
|
||
"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]]],
|
||
|
||
// Das Haus. First ueber die laengere Achse, also Ost-West - so liegt
|
||
// er auch auf dem Luftbild.
|
||
["raum" => null, "z" => 0, "hoehe" => 6, "farbe" => "#d5d9df",
|
||
"dachfarbe" => "#9aa3ad", "dach" => ["art" => "sattel", "hoehe" => 3],
|
||
"ecken" => [[36.8, 17.2], [39.2, 31.2], [24.2, 33.8], [21.8, 19.8]]],
|
||
|
||
// Wiederkehr auf der Suedseite: ein Kreuzgiebel quer zum Hauptfirst.
|
||
// Als eigener Koerper auf derselben Grundflaeche gezeichnet - er liegt
|
||
// suedlich und damit weiter vorn, deckt das Hauptdach also von selbst
|
||
// richtig ab. Die Firsthoehe folgt der Dachneigung des Hauses: 3 m auf
|
||
// 7,1 m halbe Spannweite sind knapp 23 Grad, bei 3 m halber
|
||
// Spannweite also rund 1,3 m.
|
||
//
|
||
// Breite und Tiefe sind vom Luftbild geschaetzt - wenn es genauer sein
|
||
// soll, klick die Flaeche in tools/lageplan.php ab.
|
||
["raum" => null, "z" => 6, "hoehe" => 0, "farbe" => "#d5d9df",
|
||
"dachfarbe" => "#9aa3ad",
|
||
"dach" => ["art" => "sattel", "hoehe" => 1.3, "achse" => "NS"],
|
||
"ecken" => [[34.7, 32.0], [28.7, 33.0], [27.5, 25.6], [33.4, 24.6]]],
|
||
|
||
// Beide Carports sind Holzkonstruktionen mit Pultdach, die Module
|
||
// liegen plan darauf. Der OG-Carport faellt zur Strasse ab, also nach
|
||
// Westen, und ist nach Sueden offen.
|
||
// Die hohe Seite liegt im Osten und schliesst dort an die Traufe des
|
||
// Garagendachs an - deshalb 2,23 + 0,27 und nicht glatt 2,5: die
|
||
// Oberkante der hohen Seite soll die 2,5 treffen, nicht die Wand.
|
||
["raum" => "Carport OG", "z" => 0, "hoehe" => 2.23, "farbe" => "#b8b3aa",
|
||
"dachfarbe" => "#a8a29a", "pv" => true,
|
||
"dach" => ["art" => "pult", "hoehe" => 0.27, "tief" => "W"],
|
||
"offen" => ["S"],
|
||
"ecken" => [[12.2, 19.2], [13.3, 25.4], [9.3, 26.1], [8.2, 19.9]]],
|
||
|
||
// Der EG-Carport faellt nach Sueden ab und ist nach Westen offen.
|
||
["raum" => "Carport EG", "z" => 0, "hoehe" => 2.2, "farbe" => "#b8b3aa",
|
||
"dachfarbe" => "#a8a29a", "pv" => true,
|
||
"dach" => ["art" => "pult", "hoehe" => 0.23, "tief" => "S"],
|
||
"offen" => ["W"],
|
||
"ecken" => [[19.0, 34.6], [19.3, 39.1], [10.7, 40.3], [10.4, 35.8]]],
|
||
|
||
// Die Garage hat ein begruentes Dach und ihr Tor nach Sueden. Der
|
||
// First laeuft Nord-Sued, die Traufen liegen also im Osten und Westen;
|
||
// an der westlichen setzt der OG-Carport an. Ausgeschrieben, weil bei
|
||
// einem fast quadratischen Grundriss sonst die zweite Nachkommastelle
|
||
// entscheidet, welche Seite die laengere ist.
|
||
["raum" => "Garage", "z" => 0, "hoehe" => 2.5, "farbe" => "#c9ced5",
|
||
"dachfarbe" => "#7d9470", "tor" => "S",
|
||
"dach" => ["art" => "sattel", "hoehe" => 1.5, "achse" => "NS"],
|
||
"ecken" => [[12.2, 19.2], [13.3, 25.4], [19.6, 24.3], [18.5, 18.0]]],
|
||
|
||
["raum" => "Veranda EG", "z" => 0, "hoehe" => 0, "farbe" => "#aeb8c4",
|
||
"ecken" => [[24.3, 19.5], [26.2, 30.8], [23.8, 31.1], [22.0, 19.9]]],
|
||
["raum" => "Veranda OG", "z" => 3, "hoehe" => 0, "farbe" => "#c3cbd5",
|
||
"ecken" => [[24.3, 19.5], [26.2, 30.8], [23.8, 31.1], [22.0, 19.9]]],
|
||
|
||
["raum" => "Terrasse EG", "z" => 0, "hoehe" => 0, "farbe" => "#aeb8c4",
|
||
"ecken" => [[23.8, 31.1], [28.3, 30.4], [29.0, 33.0], [24.2, 33.8]]],
|
||
["raum" => "Terrasse OG", "z" => 3, "hoehe" => 0, "farbe" => "#c3cbd5",
|
||
"ecken" => [[23.8, 31.1], [28.3, 30.4], [29.0, 33.0], [24.2, 33.8]]],
|
||
|
||
["raum" => "Pergola", "z" => 0, "hoehe" => 0, "farbe" => "#9aa3ad",
|
||
"ecken" => [[48.0, 36.5], [43.8, 36.4], [46.0, 31.5], [49.4, 32.4]]],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 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) {
|
||
// "dach" ist eine Beschreibung, keine Zahl - die Hoehe steckt darin.
|
||
$oben = ($f["z"] ?? 0) + ($f["hoehe"] ?? 0) + ($f["dach"]["hoehe"] ?? 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));
|
||
}
|
||
|
||
/**
|
||
* Himmelsrichtung, in die eine Aussennormale zeigt.
|
||
*
|
||
* Das Grundstueck steht rund zehn Grad schief im Raster; die groessere der
|
||
* beiden Komponenten ist deshalb immer eindeutig.
|
||
*/
|
||
function aussenRichtung($nx, $ny)
|
||
{
|
||
if (abs($nx) > abs($ny)) {
|
||
return $nx > 0 ? "O" : "W";
|
||
}
|
||
return $ny > 0 ? "S" : "N";
|
||
}
|
||
|
||
/**
|
||
* Ein Vieleck mit positivem Umlaufsinn.
|
||
*
|
||
* Daran haengt zweierlei: die Aussennormale einer Kante ist nur bei bekanntem
|
||
* Umlauf berechenbar, und das Wegwerfen der Rueckseiten prueft das Vorzeichen
|
||
* der projizierten Flaeche. Beides ginge schief, wenn eine Flaeche andersherum
|
||
* abgeklickt wurde als die naechste.
|
||
*/
|
||
function aussenUmlauf($ecken)
|
||
{
|
||
$summe = 0.0;
|
||
$n = count($ecken);
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$j = ($i + 1) % $n;
|
||
$summe += $ecken[$i][0] * $ecken[$j][1] - $ecken[$j][0] * $ecken[$i][1];
|
||
}
|
||
return $summe < 0 ? array_reverse($ecken) : $ecken;
|
||
}
|
||
|
||
/** Je Kante ihre Himmelsrichtung, Aussennormale und Laenge. */
|
||
function aussenKanten($ecken)
|
||
{
|
||
$kanten = [];
|
||
$n = count($ecken);
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$a = $ecken[$i];
|
||
$b = $ecken[($i + 1) % $n];
|
||
$dx = $b[0] - $a[0];
|
||
$dy = $b[1] - $a[1];
|
||
$laenge = hypot($dx, $dy);
|
||
// Aussennormale einer positiv umlaufenden Flaeche in Bildkoordinaten.
|
||
$nx = $laenge > 0 ? $dy / $laenge : 0;
|
||
$ny = $laenge > 0 ? -$dx / $laenge : 0;
|
||
$kanten[] = ["i" => $i, "a" => $a, "b" => $b, "laenge" => $laenge,
|
||
"nx" => $nx, "ny" => $ny,
|
||
"richtung" => aussenRichtung($nx, $ny),
|
||
"mitte" => [($a[0] + $b[0]) / 2, ($a[1] + $b[1]) / 2]];
|
||
}
|
||
return $kanten;
|
||
}
|
||
|
||
/** Die Kante, deren Aussennormale in diese Richtung zeigt. */
|
||
function aussenKante($kanten, $richtung)
|
||
{
|
||
foreach ($kanten as $k) {
|
||
if ($k["richtung"] === $richtung) {
|
||
return $k;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Zeigt eine projizierte Flaeche zum Betrachter?
|
||
*
|
||
* Bei positivem Umlauf der Grundflaeche haben die zugewandten Seiten eine
|
||
* positive projizierte Flaeche, die abgewandten eine negative. Damit fallen
|
||
* die Rueckseiten weg, ohne sie einzeln benennen zu muessen - und mit ihnen
|
||
* die Kanten, die man durch einen Koerper hindurch gesehen haette.
|
||
*/
|
||
function aussenSichtbar($punkte)
|
||
{
|
||
$summe = 0.0;
|
||
$n = count($punkte);
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$j = ($i + 1) % $n;
|
||
$summe += $punkte[$i][0] * $punkte[$j][1] - $punkte[$j][0] * $punkte[$i][1];
|
||
}
|
||
return $summe > 0;
|
||
}
|
||
|
||
/** Ein Vieleck zu seinem Schwerpunkt hin einziehen, um `abstand` Meter. */
|
||
function aussenEinzug($ecken, $abstand)
|
||
{
|
||
$m = aussenMitte($ecken);
|
||
$neu = [];
|
||
foreach ($ecken as $p) {
|
||
$dx = $p[0] - $m[0];
|
||
$dy = $p[1] - $m[1];
|
||
$d = hypot($dx, $dy);
|
||
$f = $d > $abstand ? ($d - $abstand) / $d : 0.0;
|
||
$neu[] = [$m[0] + $dx * $f, $m[1] + $dy * $f];
|
||
}
|
||
return $neu;
|
||
}
|
||
|
||
/**
|
||
* Die Zeichnung als Liste von Polygonen, von hinten nach vorn sortiert.
|
||
*
|
||
* Sortiert wird nach dem hintersten Punkt der Grundflaeche (Malerreihenfolge).
|
||
* Das reicht hier: die Koerper stehen nebeneinander auf dem Boden. Die
|
||
* Wiederkehr durchdringt zwar das Hausdach, liegt aber suedlich davon und
|
||
* damit ohnehin weiter vorn - genau so faelscht man einen Kreuzgiebel, ohne
|
||
* Flaechen verschneiden zu muessen.
|
||
*
|
||
* Returns:
|
||
* ["polygone" => [["punkte" => [[x,y],...], "farbe" => "#rrggbb"], ...],
|
||
* "anker" => [Raumname => [x, y], ...]] Mitte der Deckflaeche
|
||
*/
|
||
function aussenZeichnung()
|
||
{
|
||
$flaechen = aussenFlaechen();
|
||
$einpassung = aussenEinpassung($flaechen);
|
||
if (!$einpassung) {
|
||
return ["polygone" => [], "anker" => []];
|
||
}
|
||
|
||
$stuecke = [];
|
||
foreach ($flaechen as $f) {
|
||
$ecken = aussenUmlauf($f["ecken"]);
|
||
$kanten = aussenKanten($ecken);
|
||
$z = $f["z"] ?? 0;
|
||
$wand = $f["hoehe"] ?? 0;
|
||
$dach = $f["dach"] ?? null;
|
||
$farbe = $f["farbe"] ?? "#8a9099";
|
||
$offen = $f["offen"] ?? [];
|
||
$halb = $f["halb"] ?? [];
|
||
// Wandstaerke gibt es nur, wo sie zu sehen ist - an einer Oeffnung.
|
||
// Bei einem geschlossenen Koerper waere sie Rechenarbeit fuer nichts.
|
||
$dicke = ($offen || $halb) ? ($f["dicke"] ?? 0.2) : 0.0;
|
||
|
||
// --- Hoehe der Oberkante je Ecke ---------------------------------
|
||
//
|
||
// Beim Pultdach faellt sie zu einer Seite ab. Die Fallrichtung kommt
|
||
// aus der Aussennormalen der genannten Kante und nicht aus einem
|
||
// festen Nord-Sued-Vektor: das Grundstueck steht schief, und ein
|
||
// First soll parallel zur Traufe laufen und nicht fast parallel.
|
||
$zOben = array_fill(0, count($ecken), $z + $wand);
|
||
if ($dach && $dach["art"] === "pult") {
|
||
$tief = aussenKante($kanten, $dach["tief"]);
|
||
if ($tief) {
|
||
$werte = [];
|
||
foreach ($ecken as $p) {
|
||
$werte[] = $p[0] * $tief["nx"] + $p[1] * $tief["ny"];
|
||
}
|
||
$lo = min($werte);
|
||
$hi = max($werte);
|
||
foreach ($ecken as $i => $p) {
|
||
$anteil = $hi > $lo ? ($hi - $werte[$i]) / ($hi - $lo) : 0;
|
||
$zOben[$i] = $z + $wand + $dach["hoehe"] * $anteil;
|
||
}
|
||
}
|
||
}
|
||
|
||
$unten = [];
|
||
$oben = [];
|
||
foreach ($ecken as $i => $p) {
|
||
$unten[] = aussenPunkt($einpassung, $p[0], $p[1], $z);
|
||
$oben[] = aussenPunkt($einpassung, $p[0], $p[1], $zOben[$i]);
|
||
}
|
||
|
||
$polygone = [];
|
||
$tiefe = INF;
|
||
foreach ($ecken as $p) {
|
||
$tiefe = min($tiefe, aussenPunkt($einpassung, $p[0], $p[1], 0)[1]);
|
||
}
|
||
|
||
// --- Boden -------------------------------------------------------
|
||
//
|
||
// Nur wo eine Seite offen ist. Bei einem geschlossenen Koerper sieht
|
||
// man ihn nie, bei einem offenen dagegen sofort - ohne ihn schaut man
|
||
// durch den Carport auf den schwarzen Hintergrund, und das liest sich
|
||
// als Loch und nicht als Unterstand.
|
||
if ($wand > 0 && ($offen || $halb)) {
|
||
$boden = [];
|
||
foreach ($ecken as $pkt) {
|
||
$boden[] = aussenPunkt($einpassung, $pkt[0], $pkt[1], $z);
|
||
}
|
||
$polygone[] = ["punkte" => $boden, "farbe" => aussenSchatten($farbe, 0.55)];
|
||
}
|
||
|
||
// --- Waende ------------------------------------------------------
|
||
if ($wand > 0) {
|
||
foreach ($kanten as $k) {
|
||
if (in_array($k["richtung"], $offen, true)) {
|
||
continue;
|
||
}
|
||
$i = $k["i"];
|
||
$j = ($i + 1) % count($ecken);
|
||
$von = $ecken[$i];
|
||
$bis = $ecken[$j];
|
||
$zVon = $zOben[$i];
|
||
$zBis = $zOben[$j];
|
||
|
||
if (in_array($k["richtung"], $halb, true)) {
|
||
// Halb offene Seite: stehen bleibt die Haelfte, die von
|
||
// der offenen Seite weiter weg liegt - vorn ist offen.
|
||
$offeneKante = $offen ? aussenKante($kanten, $offen[0]) : null;
|
||
$mitteKante = [($von[0] + $bis[0]) / 2, ($von[1] + $bis[1]) / 2];
|
||
$zMitte = ($zVon + $zBis) / 2;
|
||
if ($offeneKante) {
|
||
$wertVon = $von[0] * $offeneKante["nx"] + $von[1] * $offeneKante["ny"];
|
||
$wertBis = $bis[0] * $offeneKante["nx"] + $bis[1] * $offeneKante["ny"];
|
||
if ($wertVon > $wertBis) {
|
||
$bis = $mitteKante;
|
||
$zBis = $zMitte;
|
||
} else {
|
||
$von = $mitteKante;
|
||
$zVon = $zMitte;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Eine Wand ist ein Kasten und keine Flaeche. Sichtbar wird
|
||
// die Dicke nur an einer Oeffnung - dort steht sonst ein
|
||
// Blatt Papier im Raum, und genau das sah seltsam aus.
|
||
// Innenseite, Oberkante und die beiden Schnittkanten kommen
|
||
// deshalb nur dazu, wo der Koerper ueberhaupt offen ist.
|
||
$innenVon = [$von[0] - $k["nx"] * $dicke, $von[1] - $k["ny"] * $dicke];
|
||
$innenBis = [$bis[0] - $k["nx"] * $dicke, $bis[1] - $k["ny"] * $dicke];
|
||
|
||
$seiten = [];
|
||
// Aussenseite
|
||
$seiten[] = [[
|
||
aussenPunkt($einpassung, $von[0], $von[1], $z),
|
||
aussenPunkt($einpassung, $bis[0], $bis[1], $z),
|
||
aussenPunkt($einpassung, $bis[0], $bis[1], $zBis),
|
||
aussenPunkt($einpassung, $von[0], $von[1], $zVon),
|
||
], 0.72];
|
||
if ($dicke > 0) {
|
||
// Innenseite - umgekehrter Umlauf, damit sie zaehlt, wenn
|
||
// man durch die Oeffnung hineinsieht.
|
||
$seiten[] = [[
|
||
aussenPunkt($einpassung, $innenBis[0], $innenBis[1], $z),
|
||
aussenPunkt($einpassung, $innenVon[0], $innenVon[1], $z),
|
||
aussenPunkt($einpassung, $innenVon[0], $innenVon[1], $zVon),
|
||
aussenPunkt($einpassung, $innenBis[0], $innenBis[1], $zBis),
|
||
], 0.60];
|
||
// Oberkante
|
||
$seiten[] = [[
|
||
aussenPunkt($einpassung, $von[0], $von[1], $zVon),
|
||
aussenPunkt($einpassung, $bis[0], $bis[1], $zBis),
|
||
aussenPunkt($einpassung, $innenBis[0], $innenBis[1], $zBis),
|
||
aussenPunkt($einpassung, $innenVon[0], $innenVon[1], $zVon),
|
||
], 0.86];
|
||
// Die beiden Schnittkanten an den Enden der Wand
|
||
$seiten[] = [[
|
||
aussenPunkt($einpassung, $innenVon[0], $innenVon[1], $z),
|
||
aussenPunkt($einpassung, $von[0], $von[1], $z),
|
||
aussenPunkt($einpassung, $von[0], $von[1], $zVon),
|
||
aussenPunkt($einpassung, $innenVon[0], $innenVon[1], $zVon),
|
||
], 0.84];
|
||
$seiten[] = [[
|
||
aussenPunkt($einpassung, $bis[0], $bis[1], $z),
|
||
aussenPunkt($einpassung, $innenBis[0], $innenBis[1], $z),
|
||
aussenPunkt($einpassung, $innenBis[0], $innenBis[1], $zBis),
|
||
aussenPunkt($einpassung, $bis[0], $bis[1], $zBis),
|
||
], 0.84];
|
||
}
|
||
$etwasSichtbar = false;
|
||
foreach ($seiten as $seite) {
|
||
if (aussenSichtbar($seite[0])) {
|
||
$polygone[] = ["punkte" => $seite[0],
|
||
"farbe" => aussenSchatten($farbe, $seite[1])];
|
||
$etwasSichtbar = true;
|
||
}
|
||
}
|
||
if (!$etwasSichtbar) {
|
||
continue;
|
||
}
|
||
|
||
// Das Tor sitzt mittig in seiner Wand und reicht nicht ganz
|
||
// bis unter die Traufe.
|
||
if (($f["tor"] ?? null) === $k["richtung"]) {
|
||
$torHoch = min(2.2, $z + min($zVon, $zBis) - $z);
|
||
$a = 0.2;
|
||
$b = 0.8;
|
||
$pa = [$von[0] + ($bis[0] - $von[0]) * $a, $von[1] + ($bis[1] - $von[1]) * $a];
|
||
$pb = [$von[0] + ($bis[0] - $von[0]) * $b, $von[1] + ($bis[1] - $von[1]) * $b];
|
||
$polygone[] = ["punkte" => [
|
||
aussenPunkt($einpassung, $pa[0], $pa[1], $z),
|
||
aussenPunkt($einpassung, $pb[0], $pb[1], $z),
|
||
aussenPunkt($einpassung, $pb[0], $pb[1], $z + $torHoch),
|
||
aussenPunkt($einpassung, $pa[0], $pa[1], $z + $torHoch),
|
||
], "farbe" => aussenSchatten($farbe, 0.52)];
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Dach --------------------------------------------------------
|
||
if ($dach && $dach["art"] === "sattel" && count($ecken) === 4) {
|
||
// Der First laeuft entlang der laengeren Gebaeudeachse, wenn nichts
|
||
// anderes dasteht. Fest an Eckennummern haengt das nicht mehr: die
|
||
// Kanten sagen selbst, wo Norden ist.
|
||
$achse = $dach["achse"] ?? null;
|
||
if (!$achse) {
|
||
$nord = aussenKante($kanten, "N");
|
||
$ost = aussenKante($kanten, "O");
|
||
$achse = ($nord && $ost && $nord["laenge"] >= $ost["laenge"]) ? "OW" : "NS";
|
||
}
|
||
$enden = $achse === "OW" ? ["O", "W"] : ["N", "S"];
|
||
$seiten = $achse === "OW" ? ["N", "S"] : ["O", "W"];
|
||
$firstHoch = $z + $wand + $dach["hoehe"];
|
||
|
||
// Die Firstenden erst in Weltkoordinaten: gebraucht werden sie
|
||
// gleich zum Vergleichen von Abstaenden, und das geht nur vor der
|
||
// Projektion sinnvoll.
|
||
$firstWelt = [];
|
||
$first = [];
|
||
foreach ($enden as $r) {
|
||
$k = aussenKante($kanten, $r);
|
||
$firstWelt[$r] = $k ? $k["mitte"] : null;
|
||
$first[$r] = $k
|
||
? aussenPunkt($einpassung, $k["mitte"][0], $k["mitte"][1], $firstHoch)
|
||
: null;
|
||
}
|
||
// Giebeldreiecke schliessen die Schmalseiten. Ohne sie schiene an
|
||
// den Enden die Wand durch das Dach.
|
||
foreach ($enden as $r) {
|
||
$k = aussenKante($kanten, $r);
|
||
if (!$k || !$first[$r]) {
|
||
continue;
|
||
}
|
||
$giebel = [
|
||
aussenPunkt($einpassung, $k["a"][0], $k["a"][1], $z + $wand),
|
||
aussenPunkt($einpassung, $k["b"][0], $k["b"][1], $z + $wand),
|
||
$first[$r],
|
||
];
|
||
if (aussenSichtbar($giebel)) {
|
||
$polygone[] = ["punkte" => $giebel, "farbe" => aussenSchatten($farbe, 0.86)];
|
||
}
|
||
}
|
||
foreach ($seiten as $r) {
|
||
$k = aussenKante($kanten, $r);
|
||
if (!$k || !$firstWelt[$enden[0]] || !$firstWelt[$enden[1]]) {
|
||
continue;
|
||
}
|
||
// Jedes Traufende bekommt den Firstpunkt, der bei ihm liegt.
|
||
// In fester Reihenfolge genommen wird eine der beiden
|
||
// Dachflaechen zur Schleife und zeichnet einen Zacken quer
|
||
// ueber das Haus - genau das war zu sehen.
|
||
$nahB = $enden[0];
|
||
$nahA = $enden[1];
|
||
if (hypot($k["b"][0] - $firstWelt[$enden[0]][0], $k["b"][1] - $firstWelt[$enden[0]][1])
|
||
> hypot($k["b"][0] - $firstWelt[$enden[1]][0], $k["b"][1] - $firstWelt[$enden[1]][1])) {
|
||
$nahB = $enden[1];
|
||
$nahA = $enden[0];
|
||
}
|
||
$schraege = [
|
||
aussenPunkt($einpassung, $k["a"][0], $k["a"][1], $z + $wand),
|
||
aussenPunkt($einpassung, $k["b"][0], $k["b"][1], $z + $wand),
|
||
$first[$nahB],
|
||
$first[$nahA],
|
||
];
|
||
if (aussenSichtbar($schraege)) {
|
||
$polygone[] = ["punkte" => $schraege,
|
||
"farbe" => aussenSchatten($f["dachfarbe"] ?? "#9aa3ad",
|
||
$r === "S" || $r === "W" ? 1.0 : 0.82)];
|
||
}
|
||
}
|
||
$deck = null;
|
||
} else {
|
||
// Flach, Pult oder gar kein Dach: die Oberseite ist die Deckflaeche.
|
||
$polygone[] = ["punkte" => $oben, "farbe" => $f["dachfarbe"] ?? $farbe];
|
||
$deck = $oben;
|
||
}
|
||
|
||
// Die Module liegen plan auf der Dachflaeche - ein Stueck eingezogen,
|
||
// damit der Rand des Daches sichtbar bleibt.
|
||
if (!empty($f["pv"]) && $deck) {
|
||
$modulEcken = aussenEinzug($ecken, 0.4);
|
||
$modul = [];
|
||
foreach ($modulEcken as $i => $p) {
|
||
$modul[] = aussenPunkt($einpassung, $p[0], $p[1], $zOben[$i] + 0.05);
|
||
}
|
||
$polygone[] = ["punkte" => $modul, "farbe" => "#2f3a49"];
|
||
}
|
||
|
||
$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;
|
||
}
|