Files
dxfmakros/lib/export_planquadrat.py
T

170 lines
7.2 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
export_planquadrat.py - Berechnet das Planquadrat eines Blocks aus x/y-Koordinate.
Liest die aktive Planquadrat-Konfiguration aus cfg/export.cfg
([Planquadrate] -> section=<Name der Sub-Sektion>, z.B. Planquadrate_AN) und
rundet die x/y-Koordinate eines Blocks (mm, wie im Roh-JSON von export.lsp)
je Achse auf die konfigurierte Schrittweite (in Metern) herunter.
x_origin/y_origin ([Planquadrate], in m) legen die DXF-Koordinate der
Planquadrat-Ecke A/1 fest. Sind sie leer/nicht gesetzt, ermittelt
resolve_origins() sie automatisch je Exportlauf aus den zu exportierenden
Bloecken (am weitesten links/unten liegendes Element = Ursprung) - siehe dort.
Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py.
"""
from export_blockpatterns import cfg_path_from_env, load_export_cfg
def load_planquadrat_config(cfg_path=None):
"""Liest die aktive Planquadrat-Konfiguration aus export.cfg.
Rueckgabe: dict mit x_step_mm, x_alpha, y_step_mm, y_alpha,
x_origin_mm/y_origin_mm (None, falls in [Planquadrate] nicht gesetzt -
siehe resolve_origins()) und format, oder None, wenn [Planquadrate]
bzw. die referenzierte Sub-Sektion fehlt.
"""
if cfg_path is None:
cfg_path = cfg_path_from_env()
parser = load_export_cfg(cfg_path)
if not parser.has_section("Planquadrate"):
return None
section_name = parser.get("Planquadrate", "section", fallback=None)
if not section_name or not parser.has_section(section_name):
return None
sec = parser[section_name]
def _origin_mm(key):
raw = parser.get("Planquadrate", key, fallback="").strip()
return float(raw) * 1000.0 if raw else None
return {
"x_step_mm": float(sec.get("x_step", "1")) * 1000.0,
"x_alpha": sec.get("x_description", "numerisch").strip().lower().startswith("alpha"),
"y_step_mm": float(sec.get("y_step", "1")) * 1000.0,
"y_alpha": sec.get("y_description", "numerisch").strip().lower().startswith("alpha"),
"x_origin_mm": _origin_mm("x_origin"),
"y_origin_mm": _origin_mm("y_origin"),
"format": sec.get("format", "{x}/{y}").strip(),
}
def _bbox_min(block, axis):
"""Untere Bounding-Box-Kante eines Blocks auf einer Achse ("x"/"y"), oder
dessen Einfuegepunkt, falls kein bbox-Feld vorhanden ist (z.B.
EXPORTSIVAS, das keine Bounding-Box mitschickt - siehe include-bbox in
csv:block-to-json, Lisp/export.lsp).
MUSS dieselbe Referenz (BBox statt nur Einfuegepunkt) wie
compute_planquadrat/planquadrat_column verwenden, sonst driften Ursprung
(Index 1) und Zellzuordnung auseinander: ein Element, dessen Bounding-Box-
Mitte weit von seinem Einfuegepunkt abweicht (z.B. ein um 90 Grad
gedrehter Kreisel, dessen Insertpunkt an einem Rand statt der Mitte
liegt), wuerde sonst in eine andere Spalte/Zeile einsortiert als der
Ursprung erwarten laesst.
"""
bbox = block.get("bbox")
if bbox:
c = bbox.get("c" + axis)
d = bbox.get("d" + axis)
if c is not None and d is not None:
return float(c) - float(d) / 2.0
v = block.get(axis)
return float(v) if v is not None else None
def resolve_origins(cfg, blocks):
"""Loest x_origin_mm/y_origin_mm auf, falls in export.cfg nicht gesetzt.
cfg = Rueckgabe von load_planquadrat_config() (kann None sein).
blocks = alle Bloecke des aktuellen Exportlaufs (Liste von dicts, wie im
Roh-JSON von export.lsp - "x"/"y" und optional "bbox").
Automatik: die am weitesten links liegende Bounding-Box-Kante (min x)
setzt x_origin, die am weitesten unten liegende (min y) setzt y_origin -
beide landen damit exakt in Planquadrat-Index 1 auf ihrer Achse. Fehlt
einem Block die Bounding-Box, faellt _bbox_min auf dessen Einfuegepunkt
zurueck. Explizit in export.cfg gesetzte Werte werden nicht ueberschrieben.
Rueckgabe: neues dict (cfg bleibt unveraendert) oder None, wenn cfg None ist.
"""
if cfg is None:
return None
resolved = dict(cfg)
if resolved.get("x_origin_mm") is None:
xs = [_bbox_min(b, "x") for b in blocks]
xs = [v for v in xs if v is not None]
resolved["x_origin_mm"] = min(xs) if xs else 0.0
if resolved.get("y_origin_mm") is None:
ys = [_bbox_min(b, "y") for b in blocks]
ys = [v for v in ys if v is not None]
resolved["y_origin_mm"] = min(ys) if ys else 0.0
return resolved
def _index_to_alpha(n):
"""Wandelt einen 1-basierten Index in eine Buchstaben-Bezeichnung um.
1=A, 26=Z, 27=AA, 28=AB, ... (wie Excel-Spaltennamen).
"""
result = ""
while n > 0:
n, remainder = divmod(n - 1, 26)
result = chr(65 + remainder) + result
return result
def _index_for(coord_mm, step_mm, origin_mm=0.0):
"""1-basierter Planquadrat-Index einer Koordinate fuer eine Schrittweite.
origin_mm ist die Koordinate der Planquadrat-Ecke A/1 (x_origin/y_origin
aus cfg/export.cfg) - Koordinaten werden vor der Index-Berechnung um
diesen Nullpunkt verschoben, damit reale Layouts mit negativen
DXF-Koordinaten (Zeichnungsursprung liegt nicht in der Gebaeudeecke)
trotzdem ab Index 1 gezaehlt werden.
"""
return int((coord_mm - origin_mm) // step_mm) + 1
def _cell_label(x_idx, y_idx, cfg):
x_label = _index_to_alpha(x_idx) if cfg["x_alpha"] else str(x_idx)
y_label = _index_to_alpha(y_idx) if cfg["y_alpha"] else str(y_idx)
return cfg["format"].format(x=x_label, y=y_label)
def compute_planquadrat(x_mm, y_mm, cfg, dx_mm=0.0, dy_mm=0.0):
"""Berechnet die Planquadrat-Bezeichnung fuer ein Element.
x_mm/y_mm = Mittelpunkt (Bounding-Box-Zentrum bzw. Einfuegepunkt, mm).
dx_mm/dy_mm = Ausdehnung der Bounding-Box (mm), optional (Default 0).
Ohne Ausdehnung (dx_mm/dy_mm <= 0, z.B. Sensoren/Weichen ohne eigene BBox)
wird wie bisher NUR die Zelle des Mittelpunkts geliefert (z.B. "A/1").
Mit Ausdehnung > 0 wird die Zelle der unteren/linken UND der oberen/
rechten Bounding-Box-Ecke berechnet - deckt das Element mehr als eine
Zelle ab (z.B. ein Kreisel ueber A/1 bis J/1, ein VarioFoerderer ueber
B/2 bis C/3), wird eine SPANNE "start-ende" geliefert (z.B. "A/1-J/1",
"B/2-C/3"); liegen beide Ecken in derselben Zelle, wird weiterhin nur
diese eine Zelle geliefert (kein "A/1-A/1").
"""
if dx_mm and dx_mm > 0 and dy_mm and dy_mm > 0:
x_lo_idx = _index_for(x_mm - dx_mm / 2.0, cfg["x_step_mm"], cfg.get("x_origin_mm", 0.0))
x_hi_idx = _index_for(x_mm + dx_mm / 2.0, cfg["x_step_mm"], cfg.get("x_origin_mm", 0.0))
y_lo_idx = _index_for(y_mm - dy_mm / 2.0, cfg["y_step_mm"], cfg.get("y_origin_mm", 0.0))
y_hi_idx = _index_for(y_mm + dy_mm / 2.0, cfg["y_step_mm"], cfg.get("y_origin_mm", 0.0))
if x_lo_idx == x_hi_idx and y_lo_idx == y_hi_idx:
return _cell_label(x_lo_idx, y_lo_idx, cfg)
start = _cell_label(x_lo_idx, y_lo_idx, cfg)
ende = _cell_label(x_hi_idx, y_hi_idx, cfg)
return f"{start}-{ende}"
x_idx = _index_for(x_mm, cfg["x_step_mm"], cfg.get("x_origin_mm", 0.0))
y_idx = _index_for(y_mm, cfg["y_step_mm"], cfg.get("y_origin_mm", 0.0))
return _cell_label(x_idx, y_idx, cfg)