"#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 [ // Hier kommen die abgeklickten Flächen hinein. ]; } /** * 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) { // Satteldach mit First über der Mitte der beiden Schmalseiten. $e = $f["ecken"]; $first1 = aussenPunkt($einpassung, ($e[0][0] + $e[3][0]) / 2, ($e[0][1] + $e[3][1]) / 2, $z + $hoehe + $dach); $first2 = aussenPunkt($einpassung, ($e[1][0] + $e[2][0]) / 2, ($e[1][1] + $e[2][1]) / 2, $z + $hoehe + $dach); $polygone[] = ["punkte" => [$oben[0], $oben[1], $first2, $first1], "farbe" => $f["dachfarbe"] ?? "#b5563a"]; $polygone[] = ["punkte" => [$oben[3], $oben[2], $first2, $first1], "farbe" => aussenSchatten($f["dachfarbe"] ?? "#b5563a", 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; }