Merge branch 'master' of https://gitea.schoenenberger.de/Schoenenberger_Systeme_GmbH/dxfmakros
This commit is contained in:
@@ -150,17 +150,21 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM
|
||||
| `omniflo_utils.py` | Gemeinsame Hilfsfunktionen fuer Omniflo-Skripte: `ROW_GROUPS` (Reihen-Gruppierung), `SWITCH_FILTERS` (Schalter-Filter), `build_row_layout`, `import_element_as_block`, `draw_cross`, `load_omniflo_data` |
|
||||
| `set_einfuegepkt.py` | Setzt `$INSBASE` (Einfuegepunkt) in Omniflo-DXF-Dateien. Schalter je Typ: `--boegen`, `--weichen45`, `--weichen90`, `--weichenkoerper`, `--weichen-parallel`, `--delta`, `--dreifachweiche`, `--sternweiche`, `--show-omniflo` |
|
||||
| `set_koords.py` | Setzt Koordinatensystem-Bloecke (K1-K4) in Omniflo-DXF-Dateien. Schalter: `--k1set` bis `--k4set`, `--set-all`, `--force`, `--show-omniflo`, `--test`, `--number` |
|
||||
| `set_attributs.py` | Fuegt ATTDEF-Attribute zu Omniflo-DXF-Dateien hinzu. Liest Konfiguration aus `cfg/attradd.cfg`. Schalter: `--number`, `--dry-run` |
|
||||
| `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Aufruf: `python export_sivas.py <export_raw.json> <data_dir> <output.csv>` |
|
||||
| `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py <export_raw.json> <data_dir> <output.csv>` |
|
||||
| `split_block_library.py` | Teilt `ils_library.dxf` in einzelne DXF-Dateien pro Block auf (mit Abhaengigkeiten). Export-Prefixe: Vario_, Staustrecke_, _3D_AS_, _3D_ES_ |
|
||||
|
||||
### Entwickler-Tools (`lib/`) - kein ezdxf, reine Text-/AutoLISP-Verarbeitung
|
||||
|
||||
| Skript | Beschreibung |
|
||||
|---|---|
|
||||
| `dbg2lsp.py` | Instrumentiert AutoLISP-Funktionen automatisch mit `ssg_dbg.lsp` (`dbgf`/`dbg`/`dbgreturn`/`dbgopen`/`dbgclose`). Schalter: `--method` (Funktion instrumentieren), `--recursive` (Aufrufgraph mitverfolgen), `--add-open DATEI METHODE` (Debug-Datei oeffnen/schliessen), `--dry-run`. Details: `doc/dbg2lsp.md` |
|
||||
|
||||
### Typische Verarbeitungskette (Omniflo-DXF)
|
||||
|
||||
1. `set_attributs.py` - Attribute zu Quell-DXFs hinzufuegen
|
||||
2. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen
|
||||
3. `set_koords.py` - Koordinatensysteme (K1-K4) setzen
|
||||
4. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung
|
||||
1. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen
|
||||
2. `set_koords.py` - Koordinatensysteme (K1-K4) setzen
|
||||
3. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung
|
||||
|
||||
## Entwicklungsstand
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM Instrumentiert AutoLISP-Funktionen mit ssg_dbg.lsp
|
||||
REM (dbgf/dbg/dbgreturn/dbgopen/dbgclose) - siehe doc/dbg2lsp.md
|
||||
REM ================================================================
|
||||
|
||||
call "%~dp0setenv.bat"
|
||||
|
||||
python "%DXFM_LIB%\dbg2lsp.py" %*
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+206
@@ -0,0 +1,206 @@
|
||||
# dbg2lsp
|
||||
|
||||
Instrumentiert AutoLISP-Funktionen (`.lsp`) automatisch mit den `ssg_dbg.lsp`-Routinen
|
||||
(`dbgf`, `dbg`, `dbgreturn`, `dbgopen`, `dbgclose`) - so wie es beim manuellen Debuggen
|
||||
bereits von Hand gemacht wird (siehe z.B. `Lisp/KreiselInsert.lsp::ils-eckrad-insert`).
|
||||
Erspart das muehsame und fehleranfaellige manuelle Einfuegen dieser Aufrufe in
|
||||
lange Aufrufketten (Kreisel/VarioFoerderer/Gefaellestrecke/Separator-Scanner o.ae.),
|
||||
bevor ein Testlauf in BricsCAD gestartet wird.
|
||||
|
||||
- [Aufruf](#aufruf)
|
||||
- [Was wird eingefuegt](#was-wird-eingefuegt)
|
||||
- [--method](#--method)
|
||||
- [--recursive](#--recursive)
|
||||
- [--add-open](#--add-open)
|
||||
- [Schalter](#schalter)
|
||||
- [Typischer Ablauf](#typischer-ablauf)
|
||||
- [Beispiele](#beispiele)
|
||||
- [Funktionsweise / Aufloesung](#funktionsweise--aufloesung)
|
||||
- [Grenzen](#grenzen)
|
||||
- [Idempotenz](#idempotenz)
|
||||
|
||||
## Aufruf
|
||||
|
||||
```
|
||||
bin\dbg2lsp.bat <datei.lsp> [--method NAME ...] [--recursive] [--add-open DATEI METHODE] [Schalter]
|
||||
```
|
||||
|
||||
Das Batch-Skript ruft `setenv.bat` auf und startet `lib/dbg2lsp.py` mit den
|
||||
uebergebenen Argumenten. `<datei.lsp>` dient als Anker (ihr Verzeichnis wird immer
|
||||
mitdurchsucht) - die gesuchte Funktion muss nicht zwingend in genau dieser Datei
|
||||
stehen, siehe [Funktionsweise](#funktionsweise--aufloesung).
|
||||
|
||||
## Was wird eingefuegt
|
||||
|
||||
### --method
|
||||
|
||||
Fuer die angegebene Funktion (mehrfach angebbar) wird direkt als erste Rumpf-Form
|
||||
|
||||
```lisp
|
||||
(dbgf "funktionsname")
|
||||
(dbg 'parameter1)
|
||||
(dbg 'parameter2)
|
||||
...
|
||||
```
|
||||
|
||||
eingefuegt (ein `dbg`-Aufruf je Eingabeparameter - die Locals nach `/` in der
|
||||
Parameterliste NICHT, das sind keine Eingabewerte), und die **letzte Form des
|
||||
Funktionsrumpfs** wird eingeklammert:
|
||||
|
||||
```lisp
|
||||
(dbgreturn <letzte Rumpf-Form>)
|
||||
```
|
||||
|
||||
`dbgreturn` loggt und liefert den Rueckgabewert unveraendert weiter (siehe
|
||||
`Lisp/ssg_dbg.lsp`) - das Verhalten der Funktion aendert sich dadurch nicht.
|
||||
|
||||
### --recursive
|
||||
|
||||
Verfolgt zusaetzlich den Aufrufgraphen: jede Funktion, die im (Original-)Rumpf
|
||||
aufgerufen wird - auch beliebig verschachtelt, z.B. in einem `if`/`progn`, aber
|
||||
NICHT innerhalb einer `'quotierten Liste` (die ist reine Datenliste, z.B.
|
||||
`'(("OSMODE") ("CECOLOR"))`) - wird ebenfalls mit `--method`-Logik instrumentiert,
|
||||
sofern sich ihr `(defun ...)` in einem der durchsuchten Verzeichnisse findet.
|
||||
|
||||
AutoLISP-/BricsCAD-Builtins (`princ`, `command`, `strcat`, `rtos`, `entmake`, `vla-*`, ...)
|
||||
haben dort naturgemaess kein `defun` und werden dadurch automatisch **nicht** angefasst -
|
||||
es gibt keine Builtin-Blacklist zu pflegen. Die `ssg_dbg.lsp`-Funktionen selbst
|
||||
(`dbgf`, `dbg`, `dbgp`, `dbgmsg`, `dbgreturn`, `dbgopen`, `dbgclose`, `dbgflush`, `dbgon`, `dbgoff`
|
||||
und ihre internen `dbg-*`-Helfer) sind fest ausgeschlossen, damit eine bereits
|
||||
instrumentierte Funktion nicht dazu fuehrt, dass das Debug-Framework sich selbst
|
||||
instrumentiert.
|
||||
|
||||
Bereits instrumentierte Funktionen werden uebersprungen (siehe [Idempotenz](#idempotenz)),
|
||||
ihr Rumpf wird aber trotzdem nach weiteren, noch nicht instrumentierten Aufrufen
|
||||
durchsucht - die Rekursion bricht dadurch nicht an einer schon bearbeiteten Stelle ab.
|
||||
|
||||
### --add-open
|
||||
|
||||
Fuegt in der angegebenen Methode (typischerweise ein `c:BEFEHL`-Kommando, das
|
||||
einen ganzen Testlauf umspannt) ein:
|
||||
|
||||
```lisp
|
||||
(dbgopen "<dbg-datei>" "<envvar>")
|
||||
```
|
||||
|
||||
als erste Rumpf-Form, sowie
|
||||
|
||||
```lisp
|
||||
(dbgclose)
|
||||
```
|
||||
|
||||
vor **jedem** `(exit)`-Aufruf im Rumpf (auch verschachtelt) und zusaetzlich am
|
||||
Ende des Rumpfs - ausser die letzte Rumpf-Form ist bereits selbst ein `(exit)`
|
||||
(dann waere ein zweites `dbgclose` ohnehin unerreichbarer Code).
|
||||
|
||||
`--add-open DATEI METHODE` ist unabhaengig von `--method`/`--recursive` nutzbar
|
||||
und mehrfach angebbar; alle drei Mechanismen lassen sich in einem Aufruf kombinieren.
|
||||
|
||||
## Schalter
|
||||
|
||||
| Schalter | Beschreibung |
|
||||
|---|---|
|
||||
| `datei` (Pflicht, Position 1) | Anker-`.lsp`-Datei; ihr Verzeichnis wird immer mitdurchsucht |
|
||||
| `--method NAME` | Funktion mit `dbgf`/`dbg`/`dbgreturn` instrumentieren (mehrfach angebbar) |
|
||||
| `--recursive` | Auch alle im Rumpf aufgerufenen projekteigenen Funktionen instrumentieren |
|
||||
| `--search-dir DIR` | Weiteres Verzeichnis nach `.lsp`-Dateien durchsuchen (mehrfach angebbar). Default: `Lisp/` und das Verzeichnis von `datei` |
|
||||
| `--max-depth N` | Maximale Rekursionstiefe fuer `--recursive` (Default: `3`) |
|
||||
| `--exclude NAME` | Funktionsname von `--recursive` ausschliessen (mehrfach angebbar) |
|
||||
| `--add-open DATEI METHODE` | `dbgopen`/`dbgclose` in `METHODE` (aus `DATEI`) einfuegen (mehrfach angebbar) |
|
||||
| `--dbg-file NAME` | Dateiname fuer `(dbgopen ...)`. Default: aus dem `--add-open`-Methodennamen abgeleitet (`c:TEST_MUBEA` -> `test_mubea.dbg`) |
|
||||
| `--dbg-envvar NAME` | Umgebungsvariable fuer `(dbgopen ...)` (Default: `DXFM_LOG`) |
|
||||
| `--force` | Auch instrumentieren, wenn bereits `(dbgf ...)`/`(dbgopen ...)` vorhanden zu sein scheint |
|
||||
| `--dry-run` | Nur ein Diff anzeigen, keine Dateien schreiben |
|
||||
|
||||
## Typischer Ablauf
|
||||
|
||||
1. **Immer zuerst `--dry-run`** laufen lassen und das Diff pruefen (mehrere Dateien
|
||||
koennen betroffen sein, siehe `--recursive`).
|
||||
2. Ohne `--dry-run` erneut aufrufen - die Dateien werden direkt (in-place) geschrieben.
|
||||
3. In BricsCAD `(load ...)` neu laden bzw. den Testlauf starten.
|
||||
4. Die `.dbg`-Datei (per `--add-open` geoeffnet, Pfad = `%DXFM_LOG%\<--dbg-file>`)
|
||||
auswerten.
|
||||
5. Da die Dateien in-place geschrieben werden und ueber Git versioniert sind: nach
|
||||
dem Debuggen die Debug-Aufrufe entweder manuell entfernen oder die Datei(en)
|
||||
per `git checkout -- <datei>` verwerfen (das Werkzeug selbst hat keinen
|
||||
"--remove"-Schalter).
|
||||
|
||||
## Beispiele
|
||||
|
||||
Eine einzelne Funktion instrumentieren:
|
||||
|
||||
```
|
||||
bin\dbg2lsp.bat tests\test_mubea.lsp --method mubea:build-separator-one --dry-run
|
||||
```
|
||||
|
||||
Eine Funktion UND alles, was sie aufruft (bis Tiefe 3, Default):
|
||||
|
||||
```
|
||||
bin\dbg2lsp.bat tests\test_mubea.lsp --method mubea:build-kreisel --recursive --dry-run
|
||||
```
|
||||
|
||||
Mehrere Einstiegspunkte in einem Lauf, plus Oeffnen/Schliessen der Debug-Datei
|
||||
im umgebenden Testbefehl - das entspricht genau der Instrumentierung, die fuer
|
||||
den urspruenglichen Scanner-Bug in `TEST_MUBEA` von Hand gemacht wurde:
|
||||
|
||||
```
|
||||
bin\dbg2lsp.bat tests\test_mubea.lsp ^
|
||||
--method mubea:build-kreisel ^
|
||||
--method mubea:build-vario-one ^
|
||||
--method mubea:build-gf-one ^
|
||||
--method mubea:build-separator-one ^
|
||||
--recursive ^
|
||||
--add-open tests\test_mubea.lsp c:TEST_MUBEA
|
||||
```
|
||||
|
||||
Bestimmte generische Helfer von der Rekursion ausschliessen (z.B. um `ssg-start`/`ssg-end`
|
||||
nicht mit anzufassen):
|
||||
|
||||
```
|
||||
bin\dbg2lsp.bat tests\test_mubea.lsp --method mubea:build-kreisel --recursive ^
|
||||
--exclude ssg-start --exclude ssg-end
|
||||
```
|
||||
|
||||
## Funktionsweise / Aufloesung
|
||||
|
||||
- `datei` wird zuerst relativ zum aktuellen Arbeitsverzeichnis, sonst relativ zur
|
||||
Projektwurzel (erkannt an `.git/` bzw. `Lisp/`, ausgehend von `datei` nach oben
|
||||
gesucht) aufgeloest.
|
||||
- Durchsucht werden: das Verzeichnis von `datei`, `Lisp/` (relativ zur Projektwurzel),
|
||||
jedes `--search-dir` sowie das Verzeichnis jeder `--add-open`-Datei - jeweils
|
||||
rekursiv nach `*.lsp`.
|
||||
- Aus all diesen Dateien wird EIN Index `Funktionsname -> (defun ...)` aufgebaut
|
||||
(case-insensitiv, wie AutoLISP-Symbole zur Laufzeit). `--method`/`--add-open`
|
||||
muessen daher nicht in `datei` selbst stehen, sondern werden ueberall gefunden.
|
||||
- Klammer-/String-/Kommentar-Erkennung ist zeichenweise implementiert (kein
|
||||
vollstaendiger Lisp-Reader) - genuegt aber, um Top-Level-`(defun ...)`-Bloecke,
|
||||
ihre Parameterliste und die letzte Rumpf-Form zuverlaessig zu erkennen, auch bei
|
||||
mehrzeiligen Parameterlisten mit Locals nach `/`.
|
||||
- Alle Aenderungen werden zunaechst als Liste von reinen Text-Einfuegungen
|
||||
gesammelt (keine Loeschungen) und erst am Ende je Datei angewandt - Positionen
|
||||
bleiben dadurch unabhaengig von der Bearbeitungsreihenfolge gueltig.
|
||||
|
||||
## Grenzen
|
||||
|
||||
- **Nur die letzte Rumpf-Form wird zu `dbgreturn`.** Fruehe `(exit)`-Ausstiege
|
||||
MITTEN im Rumpf werden von `--method` nicht mit einem eigenen `dbgreturn`
|
||||
versehen (nur `--add-open` behandelt `(exit)`-Stellen explizit, dort aber nur
|
||||
mit `dbgclose`, nicht mit einem geloggten Rueckgabewert).
|
||||
- **Kein "--remove".** Rueckbau erfolgt manuell oder per `git checkout -- <datei>`.
|
||||
- **`--recursive` kann viele Dateien anfassen** - projekteigene Helfer wie
|
||||
`ssg-start`/`ssg-val`/`ssg-attrib-merge` werden mitinstrumentiert, sobald sie im
|
||||
Aufrufgraphen auftauchen. `--max-depth` und `--exclude` begrenzen das gezielt;
|
||||
ohne `--dry-run`-Kontrolle vorher nicht blind auf grosse Funktionen mit hoher
|
||||
`--max-depth` loslassen.
|
||||
- **Mehrfache Definitionen desselben Namens** in unterschiedlichen Dateien: es
|
||||
gewinnt der erste beim Durchsuchen gefundene (Dateireihenfolge nicht garantiert
|
||||
sortiert-stabil ueber Verzeichnisgrenzen) - in dieser Codebasis kommt das
|
||||
ueblicherweise nicht vor.
|
||||
|
||||
## Idempotenz
|
||||
|
||||
Vor dem Einfuegen prueft das Werkzeug, ob die erste Rumpf-Form bereits `(dbgf ...)`
|
||||
(fuer `--method`) bzw. `(dbgopen ...)` (fuer `--add-open`) ist, und ueberspringt die
|
||||
Funktion in dem Fall mit einer `[skip]`-Meldung. Mit `--force` wird trotzdem erneut
|
||||
eingefuegt (fuehrt bei wiederholtem `--force`-Aufruf zu doppelten `dbgf`/`dbg`-Zeilen -
|
||||
in dem Fall vorher besser die Datei zuruecksetzen).
|
||||
-162
@@ -35,18 +35,6 @@ Diese Werkzeuge ermöglichen die massenhafte Aufbereitung von einzelnen dxf Date
|
||||
- [--number SIVASNR](#--number-sivasnr-1)
|
||||
- [Uebersicht K-Zuordnung](#uebersicht-k-zuordnung)
|
||||
- [Umgebungsvariablen](#umgebungsvariablen-1)
|
||||
- [set\_attributs](#set_attributs)
|
||||
- [Aufruf](#aufruf-2)
|
||||
- [Datenquellen](#datenquellen-2)
|
||||
- [Ablauf](#ablauf-2)
|
||||
- [Konfiguration (attradd.cfg)](#konfiguration-attraddcfg)
|
||||
- [Attribute Omniflo Kurve (Bogen)](#attribute-omniflo-kurve-bogen)
|
||||
- [Attribute Omniflo Weiche](#attribute-omniflo-weiche)
|
||||
- [CSV-Export-Felder (Merkmale)](#csv-export-felder-merkmale)
|
||||
- [Schalter](#schalter-2)
|
||||
- [--number SIVASNR / -n SIVASNR](#--number-sivasnr---n-sivasnr)
|
||||
- [--dry-run / -d](#--dry-run---d)
|
||||
- [Umgebungsvariablen](#umgebungsvariablen-2)
|
||||
|
||||
|
||||
# set_einfuegepunkt
|
||||
@@ -274,153 +262,3 @@ Nur die angegebene 9-stellige Sivasnr verarbeiten. Kombinierbar mit jedem Schalt
|
||||
|---|---|
|
||||
| `DXFM_DATA` | Pfad zu `data/` (JSON + DXF Quelldateien) |
|
||||
| `DXFM_RESULTS` | Pfad zu `results/` (Ausgabe) |
|
||||
|
||||
---
|
||||
|
||||
# set_attributs
|
||||
|
||||
Fuegt Attribute (ATTDEF) zu allen Omniflo DXF-Dateien hinzu und verpackt die
|
||||
gesamte Geometrie in eine benannte Blockdefinition. Die Attribut-Tags und deren
|
||||
Werte werden ueber eine Konfigurationsdatei gesteuert.
|
||||
|
||||
**Voraussetzung:** Die Quelldateien in `data/omniflo/` sollten zuvor durch
|
||||
`set_einfuegepkt.py` verarbeitet worden sein, damit `$INSBASE` korrekt gesetzt ist.
|
||||
|
||||
## Aufruf
|
||||
|
||||
```
|
||||
python lib/set_attributs.py [--number SIVASNR] [--dry-run]
|
||||
```
|
||||
|
||||
## Datenquellen
|
||||
|
||||
| Pfad | Inhalt |
|
||||
|---|---|
|
||||
| `cfg/attradd.cfg` | Attribut-Konfiguration (Tags und Quellfelder je Elementtyp) |
|
||||
| `data/json/omniflo_boegen.json` | Liste aller Boegen mit Metadaten |
|
||||
| `data/json/omniflo_weichen.json` | Liste aller Weichen mit Metadaten |
|
||||
| `data/omniflo/*.dxf` | DXF-Quelldateien (Basis: Ausgabe von `set_einfuegepkt.py`) |
|
||||
|
||||
## Ablauf
|
||||
|
||||
1. Konfiguration aus `cfg/attradd.cfg` lesen
|
||||
2. JSON-Daten laden und Lookup-Tabelle (Sivasnr -> Typ + Eintrag) erstellen
|
||||
3. Fuer jede Sivasnr die DXF-Datei oeffnen, bestehende ATTDEFs entfernen
|
||||
4. Neue ATTDEFs gemaess Config einfuegen (Layer `ATTRIB`, unterhalb der Geometrie)
|
||||
5. Alle Entities (Geometrie + ATTDEFs) in eine Blockdefinition verschieben:
|
||||
- Blockname = Sivasnummer (identisch mit dem Dateinamen ohne `.dxf`)
|
||||
- Basispunkt der Blockdefinition = `$INSBASE` aus dem DXF-Header
|
||||
6. Im Modelspace einen INSERT auf den neuen Block einfuegen, mit ATTRIB-Entities
|
||||
befuellt aus den ATTDEF-Standardwerten (ermoeglicht Attributanzeige per Klick in BricsCAD)
|
||||
7. Ergebnis nach `results/omniflo/` speichern
|
||||
|
||||
## Ausgabestruktur (DXF)
|
||||
|
||||
```
|
||||
BLOCKS
|
||||
BLOCK "<sivasnr>" (Basispunkt = $INSBASE)
|
||||
<Geometrie-Entities>
|
||||
ATTDEF BESCHR = "..."
|
||||
ATTDEF ARTINR = "..."
|
||||
...
|
||||
ENDBLK
|
||||
|
||||
ENTITIES (Modelspace)
|
||||
INSERT "<sivasnr>" at (0, 0)
|
||||
ATTRIB BESCHR = "..."
|
||||
ATTRIB ARTINR = "..."
|
||||
...
|
||||
SEQEND
|
||||
```
|
||||
|
||||
Der INSERT im Modelspace traegt die konkreten Attributwerte und ist in BricsCAD
|
||||
per Klick editierbar. Die Blockdefinition mit ATTDEFs dient als Vorlage fuer
|
||||
den Block-Import via `import_element_as_block` (omniflo_utils.py).
|
||||
|
||||
## Konfiguration (attradd.cfg)
|
||||
|
||||
Die Config-Datei ist im INI-Format mit Abschnitten je Elementtyp.
|
||||
Quellwerte koennen sein:
|
||||
- **JSON-Feldname** (z.B. `ProfilTyp`) - Wert wird aus dem JSON-Eintrag gelesen
|
||||
- **Fester Text** in Anfuehrungszeichen (z.B. `"Omniflo Kurve"`)
|
||||
- **Platzhalter** `{sivasnr}` - wird durch die Sivasnr ersetzt
|
||||
|
||||
### Attribute Omniflo Kurve (Bogen)
|
||||
|
||||
| ATTDEF-Tag | Quelle | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `BESCHR` | `ProfilTyp` | Profilbezeichnung aus JSON |
|
||||
| `ARTINR` | `Sivasnr` | Sivas-Artikelnummer |
|
||||
| `TEILEART` | `"Omniflo Kurve"` | Fester Typname |
|
||||
| `RADIUS` | `Radius` | Kurvenradius in mm |
|
||||
| `WINKEL` | `KurvenWinkel` | Kurvenwinkel in Grad |
|
||||
| `SIVASNR_TEF` | `SivasnrTEF` | Sivas-Nr. des TEF-Antriebs (leer = kein Antrieb) |
|
||||
| `LAYER` | *(aus [layer])* | Layer-Name gemaess Kategorisierung |
|
||||
|
||||
### Attribute Omniflo Weiche
|
||||
|
||||
| ATTDEF-Tag | Quelle | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `BESCHR` | `ProfilTyp` | Profilbezeichnung aus JSON |
|
||||
| `ARTINR` | `Sivasnr` | Sivas-Artikelnummer |
|
||||
| `TEILEART` | `"Omniflo Weiche"` | Fester Typname |
|
||||
| `WEICHENTYP` | `WeichenTyp` | Weichentyp (Einzelweiche, Doppelweiche, Dreiwegeweiche, ...) |
|
||||
| `WINKEL` | `KurvenWinkel` | Kurvenwinkel in Grad |
|
||||
| `RICHTUNG` | `KurvenRichtung` | Richtungskennung (1=Links, 2=Rechts, 3=Beide, 7=Dreiweg) |
|
||||
| `SIVASNR_TEF` | `SivasnrTEF` | Sivas-Nr. des TEF-Antriebs (leer = kein Antrieb) |
|
||||
| `LAYER` | *(aus [layer])* | Layer-Name gemaess Kategorisierung |
|
||||
|
||||
### CSV-Export-Felder (Merkmale)
|
||||
|
||||
Die Attribute werden beim CSV-Export (`export_csv.py`, aufgerufen ueber `c:EXPORTCSV`) wie folgt in CSV-Merkmale ueberfuehrt.
|
||||
CSV-Format: `Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale`
|
||||
|
||||
**Omniflo Kurve:**
|
||||
|
||||
| Merkmal | Quelle | Typ |
|
||||
| --- | --- | --- |
|
||||
| `Kurvenwinkel` | JSON `KurvenWinkel` | float |
|
||||
| `Radius` | JSON `Radius` | float |
|
||||
| `Höhe` | Attribut `HOEHE` / Z-Koordinate | string |
|
||||
| `Drehung` | Attribut `DREHUNG` / CAD-Rotation | float |
|
||||
| `SivasNummer` | JSON `Sivasnr` | string |
|
||||
|
||||
**Omniflo Weiche:**
|
||||
|
||||
| Merkmal | Quelle | Typ |
|
||||
| --- | --- | --- |
|
||||
| `Weichentyp` | JSON `WeichenTyp` | string |
|
||||
| `Richtung` | JSON `KurvenRichtung` | string |
|
||||
| `Weichenwinkel` | JSON `KurvenWinkel` | float |
|
||||
| `Höhe` | Attribut `HOEHE` / Z-Koordinate | string |
|
||||
| `Drehung` | Attribut `DREHUNG` / CAD-Rotation | float |
|
||||
| `Antrieb Kurve` | `SivasnrTEF` != null | bool |
|
||||
| `SivasNummer` | JSON `Sivasnr` | string |
|
||||
|
||||
|
||||
Höhe und Drehung werden beim Export selbst aus den Blockinformationen entnommen und dann ins json geschrieben.
|
||||
|
||||
| ATTDEF-Tag | Quelle | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `HOEHE` | `"2000"` | Montagehoehe in mm (wird beim Export aus Z-Koordinate aktualisiert) |
|
||||
| `DREHUNG` | `"0"` | Drehwinkel in Grad (wird beim Export aus CAD-Rotation aktualisiert) |
|
||||
|
||||
## Schalter
|
||||
|
||||
### --number SIVASNR / -n SIVASNR
|
||||
|
||||
Nur die angegebene Sivasnr verarbeiten.
|
||||
|
||||
### --dry-run / -d
|
||||
|
||||
Nur anzeigen welche Attribute gesetzt wuerden, ohne DXF-Dateien zu schreiben.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
| Variable | Verwendung |
|
||||
|---|---|
|
||||
| `DXFM_DATA` | Pfad zu `data/` (JSON + DXF Quelldateien) |
|
||||
| `DXFM_CFG` | Pfad zu `cfg/` (attradd.cfg) |
|
||||
| `DXFM_RESULTS` | Pfad zu `results/` (Ausgabe nach `results/omniflo/`) |
|
||||
|
||||
Alle drei haben Fallback auf relative Pfade vom Projektverzeichnis.
|
||||
|
||||
+725
@@ -0,0 +1,725 @@
|
||||
"""
|
||||
Instrumentiert AutoLISP-Funktionen (.lsp) automatisch mit den ssg_dbg.lsp-
|
||||
Routinen (dbgf, dbg, dbgreturn, dbgopen, dbgclose) - so wie es beim manuellen
|
||||
Debuggen (siehe z.B. Lisp/KreiselInsert.lsp::ils-eckrad-insert) bereits
|
||||
gemacht wird.
|
||||
|
||||
Fuer eine gegebene Funktion (--method) wird eingefuegt:
|
||||
- (dbgf "funktionsname") direkt als erste Form im Rumpf
|
||||
- (dbg 'parameter) je Eingabeparameter (die Locals nach "/" NICHT)
|
||||
- (dbgreturn <letzte Rumpf-Form>) die letzte Form des Rumpfs wird
|
||||
eingeklammert, damit ihr Rueckgabe-
|
||||
wert mitgeloggt wird
|
||||
|
||||
Mit --recursive wird zusaetzlich jede Funktion instrumentiert, die im Rumpf
|
||||
(auch verschachtelt, aber NICHT innerhalb von 'quotierten Listen) aufgerufen
|
||||
wird - vorausgesetzt, ihr (defun ...) laesst sich in einem der --search-dir-
|
||||
Verzeichnisse finden (Bibliotheks-/AutoLISP-Builtins wie princ, command,
|
||||
strcat etc. haben dort kein defun und werden dadurch automatisch NICHT
|
||||
angefasst). Der Aufrufgraph wird rekursiv weiterverfolgt (mit --max-depth
|
||||
begrenzt), einmal besuchte Funktionen werden nicht doppelt instrumentiert.
|
||||
|
||||
Mit --add-open DATEI METHODE wird in der angegebenen Methode zusaetzlich
|
||||
- (dbgopen "<dbg-datei>" "<envvar>") als erste Rumpf-Form
|
||||
- (dbgclose) vor jedem (exit)-Aufruf im Rumpf
|
||||
UND am Ende des Rumpfs (falls die
|
||||
letzte Form nicht bereits (exit) ist)
|
||||
eingefuegt - fuer den Einstiegspunkt (typischerweise ein c:BEFEHL-Kommando),
|
||||
der die .dbg-Datei fuer einen ganzen Testlauf oeffnen/schliessen soll.
|
||||
--add-open ist unabhaengig von --method/--recursive nutzbar und mehrfach
|
||||
angebbar.
|
||||
|
||||
Aufruf-Beispiele:
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-separator-one
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --add-open tests/test_mubea.lsp c:TEST_MUBEA
|
||||
|
||||
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive \\
|
||||
--add-open tests/test_mubea.lsp c:TEST_MUBEA --dry-run
|
||||
|
||||
Details, Grenzen und weitere Beispiele: siehe doc/dbg2lsp.md
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ssg_dbg.lsp-Funktionen, die NIE instrumentiert oder in den Aufrufgraphen
|
||||
# aufgenommen werden duerfen - sonst wuerde --recursive versuchen, dbgf
|
||||
# selbst mit dbgf zu instrumentieren, sobald es auf eine bereits (manuell
|
||||
# oder durch einen frueheren Lauf) instrumentierte Funktion trifft.
|
||||
# ---------------------------------------------------------------------------
|
||||
DBG_FRAMEWORK_NAMES = {
|
||||
"dbgf", "dbg", "dbgp", "dbgmsg", "dbgreturn", "dbgopen", "dbgclose",
|
||||
"dbgflush", "dbgon", "dbgoff",
|
||||
"dbg-flush", "dbg-bufwrite", "dbg-tabs", "dbg-tostring", "dbg-list-tostring",
|
||||
}
|
||||
|
||||
DEFAULT_DBG_ENVVAR = "DXFM_LOG"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zeichenweises Scannen mit String-/Kommentar-Bewusstsein (identische Logik
|
||||
# wie beim manuellen Klammer-Balance-Check waehrend der Mubea-Instrumentierung)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LispSyntaxError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _skip_string(text, i):
|
||||
"""i zeigt auf das oeffnende '\"'. Gibt den Index NACH dem schliessenden '\"' zurueck."""
|
||||
n = len(text)
|
||||
i += 1
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == "\\":
|
||||
i += 2
|
||||
continue
|
||||
if c == '"':
|
||||
return i + 1
|
||||
i += 1
|
||||
raise LispSyntaxError("Nicht geschlossener String ab Position %d" % i)
|
||||
|
||||
|
||||
def _skip_line_comment(text, i):
|
||||
nl = text.find("\n", i)
|
||||
return len(text) if nl == -1 else nl + 1
|
||||
|
||||
|
||||
def find_matching_paren(text, open_idx):
|
||||
"""open_idx zeigt auf '('. Gibt den Index der zugehoerigen ')' zurueck."""
|
||||
n = len(text)
|
||||
if text[open_idx] != "(":
|
||||
raise LispSyntaxError("Position %d ist keine '('" % open_idx)
|
||||
depth = 0
|
||||
i = open_idx
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
i = _skip_string(text, i)
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
if c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
i += 1
|
||||
raise LispSyntaxError("Keine schliessende Klammer fuer '(' ab Position %d gefunden" % open_idx)
|
||||
|
||||
|
||||
def _skip_ws_and_comments(text, i):
|
||||
n = len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c in " \t\r\n":
|
||||
i += 1
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
break
|
||||
return i
|
||||
|
||||
|
||||
_TOKEN_STOP_CHARS = set(" \t\r\n()\";'")
|
||||
|
||||
|
||||
def read_next_token(text, i, limit):
|
||||
"""Liest ab i (nach Whitespace/Kommentaren) das naechste Token (Symbol/Zahl)
|
||||
bis zum naechsten Trennzeichen. Gibt (token, end_index) oder (None, i) zurueck,
|
||||
wenn an Position i eine '(' / '"' / EOF folgt (kein einfaches Token)."""
|
||||
i = _skip_ws_and_comments(text, i)
|
||||
if i >= limit:
|
||||
return None, i
|
||||
if text[i] in "(\"'":
|
||||
return None, i
|
||||
start = i
|
||||
while i < limit and text[i] not in _TOKEN_STOP_CHARS:
|
||||
i += 1
|
||||
if i == start:
|
||||
return None, i
|
||||
return text[start:i], i
|
||||
|
||||
|
||||
def iter_top_level_spans(text, start, end):
|
||||
"""Liefert (form_start, form_end) fuer jede balancierte Top-Level-Form
|
||||
zwischen start und end (end exklusiv, typischerweise die Position der
|
||||
schliessenden ')' des umgebenden defun). Reine Atome (Symbole/Zahlen ohne
|
||||
umschliessende Klammer) werden ebenfalls als Form erkannt."""
|
||||
spans = []
|
||||
i = start
|
||||
while i < end:
|
||||
i = _skip_ws_and_comments(text, i)
|
||||
if i >= end:
|
||||
break
|
||||
c = text[i]
|
||||
if c == "(":
|
||||
close = find_matching_paren(text, i)
|
||||
spans.append((i, close + 1))
|
||||
i = close + 1
|
||||
elif c == "'":
|
||||
# Quotiertes Atom/Liste - als eine Form ab dem Apostroph behandeln.
|
||||
j = i + 1
|
||||
if j < end and text[j] == "(":
|
||||
close = find_matching_paren(text, j)
|
||||
spans.append((i, close + 1))
|
||||
i = close + 1
|
||||
else:
|
||||
tok, tend = read_next_token(text, j, end)
|
||||
spans.append((i, tend))
|
||||
i = tend
|
||||
elif c == '"':
|
||||
send = _skip_string(text, i)
|
||||
spans.append((i, send))
|
||||
i = send
|
||||
else:
|
||||
tok, tend = read_next_token(text, i, end)
|
||||
if tok is None:
|
||||
i += 1
|
||||
else:
|
||||
spans.append((i, tend))
|
||||
i = tend
|
||||
return spans
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# (defun ...) - Erkennung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class DefunInfo:
|
||||
def __init__(self, name, path, defun_start, defun_end,
|
||||
arglist_start, arglist_end, body_start, body_end,
|
||||
params, locals_):
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.defun_start = defun_start
|
||||
self.defun_end = defun_end # Position NACH der schliessenden ')' des defun
|
||||
self.arglist_start = arglist_start
|
||||
self.arglist_end = arglist_end
|
||||
self.body_start = body_start
|
||||
self.body_end = body_end # Position der schliessenden ')' des defun (exklusiv)
|
||||
self.params = params
|
||||
self.locals = locals_
|
||||
|
||||
def __repr__(self):
|
||||
return "DefunInfo(%r in %s)" % (self.name, self.path)
|
||||
|
||||
|
||||
def _parse_arglist(text, arglist_start, arglist_end):
|
||||
"""arglist_start zeigt auf '(' der Parameterliste, arglist_end auf die
|
||||
zugehoerige ')'. Gibt (params, locals) zurueck - locals ist alles nach
|
||||
einem "/"-Trenner (leer, falls keiner vorhanden)."""
|
||||
params = []
|
||||
locals_ = []
|
||||
seen_slash = False
|
||||
i = arglist_start + 1
|
||||
while i < arglist_end:
|
||||
tok, tend = read_next_token(text, i, arglist_end)
|
||||
if tok is None:
|
||||
# z.B. eine verschachtelte Liste als Defaultwert (in AutoLISP
|
||||
# eigentlich nicht ueblich) - ueberspringen statt abzubrechen.
|
||||
i = _skip_ws_and_comments(text, i)
|
||||
if i < arglist_end and text[i] == "(":
|
||||
i = find_matching_paren(text, i) + 1
|
||||
continue
|
||||
i += 1
|
||||
continue
|
||||
if tok == "/":
|
||||
seen_slash = True
|
||||
elif seen_slash:
|
||||
locals_.append(tok)
|
||||
else:
|
||||
params.append(tok)
|
||||
i = tend
|
||||
return params, locals_
|
||||
|
||||
|
||||
def find_defuns_in_text(text, path):
|
||||
"""Findet alle (defun NAME (...) ...) Top-Level-Definitionen im Text."""
|
||||
defuns = []
|
||||
i = 0
|
||||
n = len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
i = _skip_string(text, i)
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
if c == "(":
|
||||
close = find_matching_paren(text, i)
|
||||
head_tok, head_end = read_next_token(text, i + 1, close)
|
||||
if head_tok is not None and head_tok.lower() == "defun":
|
||||
name_tok, name_end = read_next_token(text, head_end, close)
|
||||
if name_tok:
|
||||
al_start = _skip_ws_and_comments(text, name_end)
|
||||
if al_start < close and text[al_start] == "(":
|
||||
al_end = find_matching_paren(text, al_start)
|
||||
params, locals_ = _parse_arglist(text, al_start, al_end)
|
||||
body_start = al_end + 1
|
||||
defuns.append(DefunInfo(
|
||||
name=name_tok, path=path,
|
||||
defun_start=i, defun_end=close + 1,
|
||||
arglist_start=al_start, arglist_end=al_end,
|
||||
body_start=body_start, body_end=close,
|
||||
params=params, locals_=locals_,
|
||||
))
|
||||
i = close + 1
|
||||
continue
|
||||
i += 1
|
||||
return defuns
|
||||
|
||||
|
||||
def build_defun_index(paths):
|
||||
"""path -> Dateitext (einmal eingelesen) und name.lower() -> DefunInfo
|
||||
(erster Treffer gewinnt; mehrfach definierte Namen sind in dieser
|
||||
Code-Basis nicht vorgesehen)."""
|
||||
file_texts = {}
|
||||
index = {}
|
||||
for path in paths:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
file_texts[path] = text
|
||||
for d in find_defuns_in_text(text, path):
|
||||
index.setdefault(d.name.lower(), d)
|
||||
return file_texts, index
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Aufruf-Erkennung (fuer --recursive und --add-open/exit-Suche)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def scan_calls(text, start, end):
|
||||
"""Liefert eine Liste von (head_name, call_start, call_end) fuer jede
|
||||
NICHT quotierte Aufruf-Form (...) im Bereich [start, end) - beliebig
|
||||
verschachtelt, aber nicht innerhalb von 'quotierten Listen (deren Inhalt
|
||||
ist reine Datenliste, dort kann nichts "aufgerufen" werden)."""
|
||||
calls = []
|
||||
|
||||
def walk(s, e):
|
||||
i = s
|
||||
while i < e:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
i = _skip_string(text, i)
|
||||
continue
|
||||
if c == ";":
|
||||
i = _skip_line_comment(text, i)
|
||||
continue
|
||||
if c == "'":
|
||||
j = i + 1
|
||||
if j < e and text[j] == "(":
|
||||
# quotierte Liste: reine Daten, nicht weiter absteigen
|
||||
close = find_matching_paren(text, j)
|
||||
i = close + 1
|
||||
continue
|
||||
i = j
|
||||
continue
|
||||
if c == "(":
|
||||
close = find_matching_paren(text, i)
|
||||
head_tok, head_end = read_next_token(text, i + 1, close)
|
||||
if head_tok and head_tok[0] not in "0123456789.\"'":
|
||||
calls.append((head_tok, i, close + 1))
|
||||
walk(i + 1, close)
|
||||
i = close + 1
|
||||
continue
|
||||
i += 1
|
||||
|
||||
walk(start, end)
|
||||
return calls
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen fuer Einfuegetexte
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _line_indent(text, pos):
|
||||
"""Fuehrender Whitespace der Zeile, in der pos liegt (fuer die Einrueckung
|
||||
neu eingefuegter Debug-Zeilen)."""
|
||||
nl = text.rfind("\n", 0, pos)
|
||||
line_start = nl + 1
|
||||
i = line_start
|
||||
while i < pos and text[i] in " \t":
|
||||
i += 1
|
||||
indent = text[line_start:i]
|
||||
return indent if indent else " "
|
||||
|
||||
|
||||
def _body_indent(text, body_start, body_end):
|
||||
"""Einrueckung der ERSTEN echten Rumpf-Form. NICHT einfach _line_indent
|
||||
auf body_start anwenden: bei mehrzeiligen Parameterlisten (z.B.
|
||||
"(defun foo (a b /\n c d)" - "/"-Locals auf eigener Zeile) liegt
|
||||
body_start mitten in der Fortsetzungszeile der Parameterliste, deren
|
||||
Ausrichtung nichts mit der eigentlichen Rumpf-Einrueckung zu tun hat."""
|
||||
first = _skip_ws_and_comments(text, body_start)
|
||||
if first >= body_end:
|
||||
return " "
|
||||
return _line_indent(text, first)
|
||||
|
||||
|
||||
def _already_has_call(text, body_start, body_end, head_name):
|
||||
spans = iter_top_level_spans(text, body_start, body_end)
|
||||
if not spans:
|
||||
return False
|
||||
fs, fe = spans[0]
|
||||
if text[fs] != "(":
|
||||
return False
|
||||
tok, _ = read_next_token(text, fs + 1, fe)
|
||||
return bool(tok) and tok.lower() == head_name.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Edit-Sammlung: (position, prioritaet, text) - reine Einfuegungen, angewandt
|
||||
# in absteigender Position (und bei Gleichstand nach Prioritaet), damit
|
||||
# fruehere Positionen gueltig bleiben.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EditSet:
|
||||
def __init__(self):
|
||||
self.edits = {} # path -> list[(pos, prio, text)]
|
||||
|
||||
def insert(self, path, pos, prio, text):
|
||||
self.edits.setdefault(path, []).append((pos, prio, text))
|
||||
|
||||
def apply(self, file_texts):
|
||||
"""Gibt {path: neuer_text} fuer alle Dateien mit mindestens einer
|
||||
Einfuegung zurueck."""
|
||||
result = {}
|
||||
for path, items in self.edits.items():
|
||||
text = file_texts[path]
|
||||
# Bei gleicher Position: kleinere Prioritaet zuerst im Ergebnis-
|
||||
# text (prio dient als "vor/nach"-Reihenfolge an derselben Stelle).
|
||||
items_sorted = sorted(items, key=lambda t: (t[0], t[1]))
|
||||
out = []
|
||||
last = 0
|
||||
# Gruppieren nach Position, damit mehrere Einfuegungen an
|
||||
# derselben Stelle in Prioritaets-Reihenfolge zusammenkommen.
|
||||
grouped = {}
|
||||
order = []
|
||||
for pos, prio, ins_text in items_sorted:
|
||||
if pos not in grouped:
|
||||
grouped[pos] = []
|
||||
order.append(pos)
|
||||
grouped[pos].append(ins_text)
|
||||
for pos in order:
|
||||
out.append(text[last:pos])
|
||||
out.extend(grouped[pos])
|
||||
last = pos
|
||||
out.append(text[last:])
|
||||
result[path] = "".join(out)
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Instrumentierung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def instrument_method(defun, file_texts, edits, force=False, log=print):
|
||||
"""Fuegt (dbgf ...), (dbg 'param) je Parameter und (dbgreturn ...) um die
|
||||
letzte Rumpf-Form ein."""
|
||||
text = file_texts[defun.path]
|
||||
|
||||
if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgf"):
|
||||
log(" [skip] %s (%s) ist bereits instrumentiert (dbgf vorhanden) - --force zum Erzwingen" %
|
||||
(defun.name, defun.path))
|
||||
return False
|
||||
|
||||
indent = _body_indent(text, defun.body_start, defun.body_end)
|
||||
lines = ['(dbgf "%s")' % defun.name]
|
||||
for p in defun.params:
|
||||
lines.append("(dbg '%s)" % p)
|
||||
# Kein Trailing-Newline/-Indent: body_start hat im Original bereits sein
|
||||
# eigenes "\n<indent>" vor der ersten Rumpf-Form, das bleibt unveraendert
|
||||
# stehen und schliesst direkt an die letzte hier eingefuegte Zeile an.
|
||||
insertion = "\n" + "\n".join(indent + l for l in lines)
|
||||
edits.insert(defun.path, defun.body_start, 10, insertion)
|
||||
|
||||
spans = iter_top_level_spans(text, defun.body_start, defun.body_end)
|
||||
if spans:
|
||||
last_start, last_end = spans[-1]
|
||||
edits.insert(defun.path, last_start, 20, "(dbgreturn ")
|
||||
edits.insert(defun.path, last_end, 21, ")")
|
||||
else:
|
||||
log(" [warn] %s (%s) hat einen leeren Rumpf - kein dbgreturn eingefuegt" %
|
||||
(defun.name, defun.path))
|
||||
|
||||
log(" [ok] %s (%s): dbgf + %d x dbg + dbgreturn" %
|
||||
(defun.name, defun.path, len(defun.params)))
|
||||
return True
|
||||
|
||||
|
||||
def instrument_add_open(defun, file_texts, edits, dbg_file, dbg_envvar, force=False, log=print):
|
||||
"""Fuegt (dbgopen "dbg_file" "dbg_envvar") als erste Rumpf-Form ein sowie
|
||||
(dbgclose) vor jedem (exit)-Aufruf und (sofern die letzte Rumpf-Form nicht
|
||||
selbst ein (exit) ist) am Ende des Rumpfs."""
|
||||
text = file_texts[defun.path]
|
||||
|
||||
if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgopen"):
|
||||
log(" [skip] %s (%s) hat bereits (dbgopen ...) - --force zum Erzwingen" %
|
||||
(defun.name, defun.path))
|
||||
return False
|
||||
|
||||
indent = _body_indent(text, defun.body_start, defun.body_end)
|
||||
# Kein Trailing-Newline/-Indent (siehe instrument_method): die naechste
|
||||
# Einfuegung an derselben Position (z.B. dbgf durch --method) oder das
|
||||
# Original selbst liefert das fuehrende "\n<indent>" bereits mit.
|
||||
open_line = "\n" + indent + '(dbgopen "%s" "%s")' % (dbg_file, dbg_envvar)
|
||||
edits.insert(defun.path, defun.body_start, 5, open_line)
|
||||
|
||||
calls = scan_calls(text, defun.body_start, defun.body_end)
|
||||
exit_calls = [c for c in calls if c[0].lower() == "exit"]
|
||||
for _, cstart, _cend in exit_calls:
|
||||
cindent = _line_indent(text, cstart)
|
||||
edits.insert(defun.path, cstart, 15, "(dbgclose)\n" + cindent)
|
||||
|
||||
spans = iter_top_level_spans(text, defun.body_start, defun.body_end)
|
||||
last_is_exit = False
|
||||
if spans:
|
||||
last_start, last_end = spans[-1]
|
||||
tok, _ = read_next_token(text, last_start + 1, last_end) if text[last_start] == "(" else (None, 0)
|
||||
last_is_exit = bool(tok) and tok.lower() == "exit"
|
||||
if not last_is_exit:
|
||||
edits.insert(defun.path, defun.body_end, 25, indent + "(dbgclose)\n")
|
||||
|
||||
log(" [ok] %s (%s): dbgopen + dbgclose (%d exit-Stellen + Rumpfende)" %
|
||||
(defun.name, defun.path, len(exit_calls)))
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rekursion ueber den Aufrufgraphen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def instrument_recursive(start_defun, index, file_texts, edits, max_depth, exclude, force, log):
|
||||
visited = set()
|
||||
|
||||
def recurse(defun, depth):
|
||||
key = defun.name.lower()
|
||||
if key in visited:
|
||||
return
|
||||
visited.add(key)
|
||||
|
||||
instrument_method(defun, file_texts, edits, force=force, log=log)
|
||||
|
||||
if depth >= max_depth:
|
||||
log(" [info] max-depth (%d) bei %s erreicht - keine weiteren Aufrufe verfolgt" %
|
||||
(max_depth, defun.name))
|
||||
return
|
||||
|
||||
text = file_texts[defun.path]
|
||||
calls = scan_calls(text, defun.body_start, defun.body_end)
|
||||
seen_here = set()
|
||||
for head, _s, _e in calls:
|
||||
head_l = head.lower()
|
||||
if head_l in seen_here:
|
||||
continue
|
||||
seen_here.add(head_l)
|
||||
if head_l in DBG_FRAMEWORK_NAMES or head_l in exclude:
|
||||
continue
|
||||
callee = index.get(head_l)
|
||||
if callee is None or callee.name.lower() == key:
|
||||
continue
|
||||
recurse(callee, depth + 1)
|
||||
|
||||
recurse(start_defun, 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pfad-/Umgebungshilfen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def find_repo_root(start_path):
|
||||
cur = os.path.abspath(start_path)
|
||||
if os.path.isfile(cur):
|
||||
cur = os.path.dirname(cur)
|
||||
while True:
|
||||
if os.path.isdir(os.path.join(cur, ".git")) or os.path.isdir(os.path.join(cur, "Lisp")):
|
||||
return cur
|
||||
parent = os.path.dirname(cur)
|
||||
if parent == cur:
|
||||
return None
|
||||
cur = parent
|
||||
|
||||
|
||||
def resolve_file(path, repo_root):
|
||||
if os.path.isfile(path):
|
||||
return os.path.abspath(path)
|
||||
if repo_root:
|
||||
candidate = os.path.join(repo_root, path)
|
||||
if os.path.isfile(candidate):
|
||||
return os.path.abspath(candidate)
|
||||
return None
|
||||
|
||||
|
||||
def collect_lsp_files(dirs):
|
||||
paths = []
|
||||
seen = set()
|
||||
for d in dirs:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for root, _dirnames, filenames in os.walk(d):
|
||||
for fn in filenames:
|
||||
if fn.lower().endswith(".lsp"):
|
||||
full = os.path.abspath(os.path.join(root, fn))
|
||||
if full not in seen:
|
||||
seen.add(full)
|
||||
paths.append(full)
|
||||
return paths
|
||||
|
||||
|
||||
def dbg_file_name_for(method_name):
|
||||
"""Leitet einen .dbg-Dateinamen aus einem Methodennamen ab, z.B.
|
||||
"c:TEST_MUBEA" -> "test_mubea.dbg", "mubea:build-kreisel" -> "build-kreisel.dbg"."""
|
||||
base = method_name.split(":")[-1]
|
||||
return base.lower() + ".dbg"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="dbg2lsp.py",
|
||||
description="Instrumentiert AutoLISP-Funktionen mit ssg_dbg.lsp (dbgf/dbg/dbgreturn/dbgopen/dbgclose).",
|
||||
)
|
||||
parser.add_argument("file", help="Anker-.lsp-Datei (Verzeichnis wird immer mit durchsucht)")
|
||||
parser.add_argument("--method", action="append", default=[],
|
||||
metavar="NAME",
|
||||
help="Funktionsname, der mit dbgf/dbg/dbgreturn instrumentiert wird. Mehrfach angebbar.")
|
||||
parser.add_argument("--recursive", action="store_true",
|
||||
help="Auch alle im Rumpf aufgerufenen projekteigenen Funktionen instrumentieren (rekursiv)")
|
||||
parser.add_argument("--search-dir", action="append", default=[],
|
||||
metavar="DIR",
|
||||
help="Weiteres Verzeichnis nach .lsp-Dateien durchsuchen (fuer --recursive). "
|
||||
"Default: Lisp/ und das Verzeichnis von FILE.")
|
||||
parser.add_argument("--max-depth", type=int, default=3,
|
||||
help="Maximale Rekursionstiefe fuer --recursive (Default: 3)")
|
||||
parser.add_argument("--exclude", action="append", default=[],
|
||||
metavar="NAME",
|
||||
help="Funktionsname von --recursive ausschliessen (mehrfach angebbar)")
|
||||
parser.add_argument("--add-open", nargs=2, action="append", default=[],
|
||||
metavar=("DATEI", "METHODE"),
|
||||
help="(dbgopen ...)/(dbgclose) in METHODE (aus DATEI) einfuegen. Mehrfach angebbar.")
|
||||
parser.add_argument("--dbg-file", default=None,
|
||||
help="Dateiname fuer (dbgopen ...) (Default: aus dem --add-open-Methodennamen abgeleitet, z.B. test_mubea.dbg)")
|
||||
parser.add_argument("--dbg-envvar", default=DEFAULT_DBG_ENVVAR,
|
||||
help='Umgebungsvariable fuer (dbgopen ...) (Default: "%s")' % DEFAULT_DBG_ENVVAR)
|
||||
parser.add_argument("--force", action="store_true",
|
||||
help="Auch instrumentieren, wenn bereits (dbgf ...)/(dbgopen ...) vorhanden zu sein scheint")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Nur ein Diff anzeigen, keine Dateien schreiben")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
repo_root = find_repo_root(args.file) or find_repo_root(".")
|
||||
main_file = resolve_file(args.file, repo_root)
|
||||
if main_file is None:
|
||||
print("FEHLER: Datei nicht gefunden: %s" % args.file, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
search_dirs = [os.path.dirname(main_file)]
|
||||
if repo_root:
|
||||
search_dirs.append(os.path.join(repo_root, "Lisp"))
|
||||
for d in args.search_dir:
|
||||
resolved = d if os.path.isdir(d) else os.path.join(repo_root or ".", d)
|
||||
search_dirs.append(resolved)
|
||||
for f, _m in args.add_open:
|
||||
resolved = resolve_file(f, repo_root)
|
||||
if resolved:
|
||||
search_dirs.append(os.path.dirname(resolved))
|
||||
|
||||
lsp_files = set(collect_lsp_files(search_dirs))
|
||||
lsp_files.add(main_file)
|
||||
lsp_files = sorted(lsp_files)
|
||||
|
||||
file_texts, index = build_defun_index(lsp_files)
|
||||
|
||||
if not args.method and not args.add_open:
|
||||
print("FEHLER: mindestens --method oder --add-open angeben.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
edits = EditSet()
|
||||
exclude = {n.lower() for n in args.exclude}
|
||||
touched_any = False
|
||||
|
||||
for method_name in args.method:
|
||||
defun = index.get(method_name.lower())
|
||||
if defun is None:
|
||||
print("FEHLER: Funktion '%s' nicht gefunden (durchsucht: %s)" %
|
||||
(method_name, ", ".join(lsp_files)), file=sys.stderr)
|
||||
continue
|
||||
print("== --method %s ==" % method_name)
|
||||
if args.recursive:
|
||||
instrument_recursive(defun, index, file_texts, edits,
|
||||
max_depth=args.max_depth, exclude=exclude,
|
||||
force=args.force, log=print)
|
||||
else:
|
||||
instrument_method(defun, file_texts, edits, force=args.force, log=print)
|
||||
touched_any = True
|
||||
|
||||
for file_arg, method_name in args.add_open:
|
||||
resolved = resolve_file(file_arg, repo_root)
|
||||
if resolved is None:
|
||||
print("FEHLER: Datei nicht gefunden: %s" % file_arg, file=sys.stderr)
|
||||
continue
|
||||
# Falls die Datei noch nicht im Index ist (z.B. ausserhalb der
|
||||
# Search-Dirs), jetzt einlesen und mit aufnehmen.
|
||||
if resolved not in file_texts:
|
||||
with open(resolved, "r", encoding="utf-8") as f:
|
||||
file_texts[resolved] = f.read()
|
||||
for d in find_defuns_in_text(file_texts[resolved], resolved):
|
||||
index.setdefault(d.name.lower(), d)
|
||||
defun = index.get(method_name.lower())
|
||||
if defun is None or defun.path != resolved:
|
||||
# ggf. gleichnamige Funktion aus anderer Datei ignorieren -
|
||||
# explizit in der genannten Datei suchen.
|
||||
local_defuns = find_defuns_in_text(file_texts[resolved], resolved)
|
||||
defun = next((d for d in local_defuns if d.name.lower() == method_name.lower()), None)
|
||||
if defun is None:
|
||||
print("FEHLER: Funktion '%s' nicht in %s gefunden" % (method_name, file_arg), file=sys.stderr)
|
||||
continue
|
||||
print("== --add-open %s %s ==" % (file_arg, method_name))
|
||||
dbg_file = args.dbg_file or dbg_file_name_for(method_name)
|
||||
instrument_add_open(defun, file_texts, edits, dbg_file=dbg_file, dbg_envvar=args.dbg_envvar,
|
||||
force=args.force, log=print)
|
||||
touched_any = True
|
||||
|
||||
if not touched_any:
|
||||
return 1
|
||||
|
||||
changed = edits.apply(file_texts)
|
||||
if not changed:
|
||||
print("Keine Aenderungen vorgenommen.")
|
||||
return 0
|
||||
|
||||
for path, new_text in changed.items():
|
||||
old_text = file_texts[path]
|
||||
if args.dry_run:
|
||||
rel = os.path.relpath(path, repo_root) if repo_root else path
|
||||
diff = difflib.unified_diff(
|
||||
old_text.splitlines(keepends=True),
|
||||
new_text.splitlines(keepends=True),
|
||||
fromfile=rel, tofile=rel,
|
||||
)
|
||||
sys.stdout.writelines(diff)
|
||||
else:
|
||||
with open(path, "w", encoding="utf-8", newline="") as f:
|
||||
f.write(new_text)
|
||||
print("Geschrieben: %s" % path)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user