"#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 .= '' . "\n"; } return $svg; }