# utils.py API Documentation

Comprehensive documentation for the `utils.py` module containing helper functions for file operations, DXF manipulation, and data processing.

## Table of Contents

1. [File Operations](#file-operations)
2. [Environment Variables](#environment-variables)
3. [JSON Operations](#json-operations)
4. [DXF File Operations](#dxf-file-operations)
5. [ezdxf Operations (High-Level DXF Helpers)](#ezdxf-operations)
6. [Dictionary Operations](#dictionary-operations)

---

## File Operations

### `check_file_in_work(work_dir, filename) → Tuple[Path, bool]`

Check if a file exists, either at the given path or in the work directory.

**Args:**
- `work_dir` (str | Path): Working directory to check if file not found at filename path
- `filename` (str | Path): Path to the file to check

**Returns:**
- `Tuple[Path, bool]`: (resolved_path, exists_flag)
  - `resolved_path`: Path where file was found (or would be)
  - `exists_flag`: True if file exists, False otherwise

**Example:**
```python
from utils import check_file_in_work
work_dir = Path("c:/work")
filepath, exists = check_file_in_work(work_dir, "test.dxf")
if exists:
    print(f"File found at: {filepath}")
```

---

## Environment Variables

### `check_environment_var(env_str: str) → Path`

Get and validate an environment variable as a Path.

**Args:**
- `env_str` (str): Name of the environment variable

**Returns:**
- `Path`: Path object from the environment variable

**Exits:**
- Exits the program if the environment variable is not set or empty

**Example:**
```python
from utils import check_environment_var
work_dir = check_environment_var('PROJECT_WORK')
config_dir = check_environment_var('PROJECT_CFG')
```

---

## JSON Operations

### `to_json(d: Any, pretty: bool = True) → str`

Convert a Python object to a JSON string.

**Args:**
- `d` (Any): Object to convert to JSON
- `pretty` (bool): If True, format with indentation for readability (default: True)

**Returns:**
- `str`: JSON string representation of the object

**Note:**
Uses `ensure_ascii=False` to properly display German umlauts (ä, ö, ü)

**Example:**
```python
from utils import to_json
data = {"name": "Test", "wert": 42}
json_str = to_json(data)
print(json_str)
```

### `load_json(jsonfilename: str) → dict`

Load JSON data from a file.

**Args:**
- `jsonfilename` (str): Path to the JSON file

**Returns:**
- `dict`: Parsed JSON data as a dictionary

**Example:**
```python
from utils import load_json
config = load_json("config.json")
```

### `write_results(jsn_results: str, out_dir: Path, filename: str) → None`

Write JSON results to a file.

**Args:**
- `jsn_results` (str): JSON string to write
- `out_dir` (Path): Output directory path
- `filename` (str): Name of the output file

**Example:**
```python
from utils import write_results, to_json
from pathlib import Path

data = {"results": [1, 2, 3]}
write_results(to_json(data), Path("output"), "results.json")
```

---

## DXF File Operations

### `dxf_is_binary(dxf_path: Path) → bool`

Check if a DXF file is in binary format.

**Args:**
- `dxf_path` (Path): Path to the DXF file

**Returns:**
- `bool`: True if the file is a binary DXF, False otherwise

**Example:**
```python
from utils import dxf_is_binary
from pathlib import Path

if dxf_is_binary(Path("drawing.dxf")):
    print("Binary DXF file detected")
else:
    print("ASCII DXF file")
```

### `get_dxf_file(filepath: Path) → ezdxf.Drawing`

Load a DXF file using ezdxf.

**Args:**
- `filepath` (Path): Path to the DXF file

**Returns:**
- `ezdxf.Drawing`: ezdxf Drawing object

**Exits:**
- Exit code 1: I/O errors
- Exit code 2: Invalid/corrupted DXF files

**Example:**
```python
from utils import get_dxf_file
from pathlib import Path

doc = get_dxf_file(Path("drawing.dxf"))
msp = doc.modelspace()
```

---

## ezdxf Operations

High-level helper functions for working with ezdxf objects.

### Attribute Extraction

#### `extract_insert_attributes(insert, error_collector=None) → dict`

Extrahiert alle Attribute aus einem INSERT-Block. Unterstützt zweistufige Blockstruktur.

**Args:**
- `insert`: INSERT-Entity des Blocks
- `error_collector` (optional): ErrorCollector für Warnungen

**Returns:**
- `dict`: Dictionary mit allen gefundenen Attributen `{tag: text}`

**Note:**
- Funktioniert ohne explizites doc-Objekt wenn `insert.doc` verfügbar
- Für neue Code: Verwenden Sie `extract_insert_attributes_with_doc()`

**Example:**
```python
from utils import extract_insert_attributes

for entity in msp.query('INSERT'):
    attributes = extract_insert_attributes(entity)
    if 'IO' in attributes:
        print(f"IO: {attributes['IO']}")
```

#### `extract_insert_attributes_with_doc(doc, insert, error_collector=None) → dict`

Extrahiert alle Attribute aus einem INSERT-Block mit explizitem doc-Parameter.
**Empfohlen für neuen Code** - bessere Unterstützung für zweistufige Blockstrukturen.

**Args:**
- `doc`: DXF-Dokument
- `insert`: INSERT-Entity des Blocks
- `error_collector` (optional): ErrorCollector für Warnungen

**Returns:**
- `dict`: Dictionary mit allen gefundenen Attributen `{tag: text}`

**Example:**
```python
from utils import extract_insert_attributes_with_doc

doc = get_dxf_file(Path("drawing.dxf"))
msp = doc.modelspace()

for entity in msp.query('INSERT'):
    attributes = extract_insert_attributes_with_doc(doc, entity)
    print(f"Block: {entity.dxf.name}, Attributes: {attributes}")
```

#### `extract_attributes_with_positions(insert_iterable, round_decimals=1) → Tuple[list, list]`

Wandelt eine Iterable von INSERT-Objekten in zwei Listen um.
**Kompatibilität** mit `getpositions.py attribs_to_dicts()`.

**Args:**
- `insert_iterable`: Iterable von INSERT-Entities (z.B. msp oder iterdxf.modelspace())
- `round_decimals` (int): Anzahl Nachkommastellen für Positions-Rundung (default: 1)

**Returns:**
- `Tuple[list, list]`: (all_inserts, all_positions)
  - `all_inserts`: Liste von Dicts mit Attribut-Tags und deren Textwerten
  - `all_positions`: Liste von Dicts mit Attribut-Tags und deren (x, y, z)-Positionen

**Note:**
- Blöcke ohne Attribute werden übersprungen
- Ersetzt die alte `attribs_to_dicts()` aus getpositions.py

**Example:**
```python
from utils import extract_attributes_with_positions

all_inserts, all_positions = extract_attributes_with_positions(msp)
for insert_data, positions in zip(all_inserts, all_positions):
    print(f"Attributes: {insert_data}")
    print(f"Positions: {positions}")
```

### Layer Operations

#### `ensure_layer_exists(doc, layer_name, color=None, linetype=None, lineweight=None, description=None) → Layer`

Stellt sicher, dass ein Layer existiert und erstellt ihn falls nötig.

**Args:**
- `doc`: DXF-Dokument
- `layer_name` (str): Name des Layers
- `color` (int, optional): ACI Color Index (1-255)
- `linetype` (str, optional): Name des Linientyps (z.B. 'CONTINUOUS', 'DASHED')
- `lineweight` (int, optional): Linienbreite in mm*100 (z.B. 25 für 0.25mm)
- `description` (str, optional): Layer-Beschreibung

**Returns:**
- `Layer`: Layer-Objekt (existierend oder neu erstellt)

**Example:**
```python
from utils import ensure_layer_exists

# Einfacher Layer
ensure_layer_exists(doc, 'ILS_MOTOR')

# Layer mit Farbe und Beschreibung
ensure_layer_exists(doc, 'ILS_CABLE', color=5, description='Cable routing layer')

# Layer mit allen Optionen
ensure_layer_exists(doc, 'ILS_SPECIAL',
                   color=3, linetype='DASHED',
                   lineweight=25, description='Special equipment')
```

#### `create_layers_from_config(doc, layer_definitions) → None`

Erstellt mehrere Layer aus einer Konfiguration.

**Args:**
- `doc`: DXF-Dokument
- `layer_definitions` (dict): Dictionary mit Layer-Definitionen

**Format:**
```python
{
    'layer_name': {
        'color': int (optional),
        'linetype': str (optional),
        'lineweight': int (optional),
        'description': str (optional)
    },
    ...
}
```

**Example:**
```python
from utils import create_layers_from_config

layer_config = {
    'ILS_MOTOR': {'color': 1, 'description': 'Motor layer'},
    'ILS_SENSOR': {'color': 3, 'lineweight': 25},
    'ILS_CABLE': {'color': 5, 'linetype': 'DASHED'}
}
create_layers_from_config(doc, layer_config)
```

### Geometry Creation

#### `add_rectangle_lwpolyline(msp, x, y, width, height, layer='0', color=None, rgb=None, closed=True) → LWPolyline`

Fügt eine rechteckige LWPOLYLINE hinzu.

**Args:**
- `msp`: Modelspace des DXF-Dokuments
- `x` (float): X-Koordinate der unteren linken Ecke
- `y` (float): Y-Koordinate der unteren linken Ecke
- `width` (float): Breite des Rechtecks
- `height` (float): Höhe des Rechtecks
- `layer` (str): Layer-Name (default: '0')
- `color` (int, optional): ACI Color Index (1-255)
- `rgb` (tuple, optional): RGB-Tuple (r, g, b) mit Werten 0-255
- `closed` (bool): Ob die Polylinie geschlossen sein soll (default: True)

**Returns:**
- `LWPolyline`: LWPOLYLINE-Entity

**Example:**
```python
from utils import add_rectangle_lwpolyline

# Einfaches Rechteck
add_rectangle_lwpolyline(msp, 0, 0, 100, 50, layer='ILS_FRAME')

# Rechteck mit Farbe
add_rectangle_lwpolyline(msp, 10, 10, 200, 100, color=5)

# Rechteck mit RGB-Farbe
add_rectangle_lwpolyline(msp, 20, 20, 150, 75, rgb=(255, 0, 0))
```

#### `add_text_with_alignment(msp, text, x, y, height=50, layer='0', color=None, halign=0, valign=0, rotation=0.0, style='Standard') → Text`

Fügt einen Text mit Ausrichtung hinzu.

**Args:**
- `msp`: Modelspace des DXF-Dokuments
- `text` (str): Textinhalt
- `x` (float): X-Koordinate
- `y` (float): Y-Koordinate
- `height` (float): Texthöhe (default: 50)
- `layer` (str): Layer-Name (default: '0')
- `color` (int, optional): ACI Color Index (1-255)
- `halign` (int): Horizontale Ausrichtung (0=links, 1=mitte, 2=rechts)
- `valign` (int): Vertikale Ausrichtung (0=basis, 1=unten, 2=mitte, 3=oben)
- `rotation` (float): Rotation in Grad (default: 0.0)
- `style` (str): Textstil-Name (default: 'Standard')

**Returns:**
- `Text`: TEXT-Entity

**Example:**
```python
from utils import add_text_with_alignment

# Einfacher Text links unten
add_text_with_alignment(msp, "Test", 100, 100, layer='ILS_TEXT')

# Zentrierter Text
add_text_with_alignment(msp, "Centered", 200, 200, halign=1, valign=2)

# Rotierter Text mit Farbe
add_text_with_alignment(msp, "Rotated", 300, 300,
                       color=3, rotation=45.0, height=75)
```

### Block Operations

#### `add_blockref_with_attributes(msp, block_name, insert_point, attributes=None, layer='0', scale=1.0, rotation=0.0) → Insert`

Fügt eine Block-Referenz (INSERT) mit Attributen hinzu.

**Args:**
- `msp`: Modelspace des DXF-Dokuments
- `block_name` (str): Name des Blocks
- `insert_point` (tuple): Einfügepunkt als (x, y) oder (x, y, z) Tuple
- `attributes` (dict, optional): Dictionary mit Attribut-Namen und -Werten
- `layer` (str): Layer-Name (default: '0')
- `scale` (float): Skalierungsfaktor (default: 1.0)
- `rotation` (float): Rotation in Grad (default: 0.0)

**Returns:**
- `Insert`: INSERT-Entity mit gesetzten Attributen

**Example:**
```python
from utils import add_blockref_with_attributes

# Block ohne Attribute
add_blockref_with_attributes(msp, "MOTOR_SYMBOL", (100, 100))

# Block mit Attributen
attrs = {"IO": "MA0001", "KENNZEICHNUNG": "=A01+UH00"}
add_blockref_with_attributes(msp, "io", (200, 200), attributes=attrs)

# Block mit Skalierung und Rotation
add_blockref_with_attributes(msp, "SENSOR", (300, 300),
                            scale=0.5, rotation=90.0, layer='ILS_SENSOR')
```

### Entity Queries

#### `query_entities_by_layer(msp, entity_type=None, layers=None, exclude_layers=None) → list`

Filtert Entities nach Typ und Layer.

**Args:**
- `msp`: Modelspace des DXF-Dokuments
- `entity_type` (str, optional): Entity-Typ (z.B. 'INSERT', 'LINE', 'TEXT'), None für alle
- `layers` (list, optional): Liste der zu inkludierenden Layer (None = alle außer exclude_layers)
- `exclude_layers` (list, optional): Liste der zu exkludierenden Layer (None = keine Exklusion)

**Returns:**
- `list`: Liste der gefilterten Entities

**Examples:**
```python
from utils import query_entities_by_layer

# Alle INSERTs auf Layer 'ILS_MOTOR'
motors = query_entities_by_layer(msp, 'INSERT', layers=['ILS_MOTOR'])

# Alle Entities außer auf Layer '0'
entities = query_entities_by_layer(msp, exclude_layers=['0'])

# Alle LINEs auf ILS_* Layern (mit zusätzlichem Filtern)
all_lines = query_entities_by_layer(msp, 'LINE')
ils_lines = [e for e in all_lines if e.dxf.layer.startswith('ILS_')]

# Alle INSERTs auf mehreren Layern
inserts = query_entities_by_layer(msp, 'INSERT',
                                 layers=['ILS_MOTOR', 'ILS_SENSOR'])
```

---

## Dictionary Operations

### `merge_two_dicts(x, y) → dict`

Merge two dictionaries, with values from y overwriting those in x.

**Args:**
- `x` (dict): First dictionary (base)
- `y` (dict): Second dictionary (override values)

**Returns:**
- `dict`: New dictionary with merged values

**Example:**
```python
from utils import merge_two_dicts

defaults = {"color": 1, "layer": "0", "scale": 1.0}
overrides = {"color": 5, "scale": 2.0}
result = merge_two_dicts(defaults, overrides)
# Result: {"color": 5, "layer": "0", "scale": 2.0}
```

---

## Migration Guide

### From old `attribs_to_dicts()` to new API

**Old code (getpositions.py):**
```python
from getpositions import attribs_to_dicts
all_inserts, all_positions = attribs_to_dicts(msp)
```

**New code:**
```python
from utils import extract_attributes_with_positions
all_inserts, all_positions = extract_attributes_with_positions(msp)
```

### From old `extract_block_attributes()` to new API

**Old code (create_numbers.py):**
```python
def extract_block_attributes(doc, insert, error_collector=None):
    # 48 lines of code...
    return attributes
```

**New code:**
```python
from utils import extract_insert_attributes_with_doc as extract_block_attributes
# Direct usage - no local function needed
```

---

## Performance Notes

1. **`extract_attributes_with_positions`**: O(n) where n = number of INSERT entities
2. **`query_entities_by_layer`**: O(n) where n = total entities in msp
3. **`ensure_layer_exists`**: O(1) - uses dict lookup
4. **`create_layers_from_config`**: O(m) where m = number of layers to create

---

## Error Handling

Most functions follow these error handling patterns:

1. **Silent fallback**: Functions like `extract_insert_attributes()` return empty dict on error
2. **ErrorCollector integration**: Functions accepting `error_collector` parameter log warnings
3. **System exit**: `check_environment_var()` and `get_dxf_file()` exit on critical errors
4. **Exception propagation**: JSON and file operations raise native Python exceptions

---

## Best Practices

1. **Always use `extract_insert_attributes_with_doc()`** for new code (better error handling)
2. **Use `ensure_layer_exists()`** before adding entities to avoid missing layer errors
3. **Prefer `query_entities_by_layer()`** over manual filtering for better readability
4. **Use `create_layers_from_config()`** when setting up multiple layers from configuration files
5. **Pass `error_collector`** when available for comprehensive error tracking

---

## Version History

- **v1.2.0** (2026-01-30): Added 8 new ezdxf helper functions
  - `extract_insert_attributes_with_doc()`
  - `extract_attributes_with_positions()`
  - `ensure_layer_exists()`
  - `add_rectangle_lwpolyline()`
  - `add_text_with_alignment()`
  - `add_blockref_with_attributes()`
  - `query_entities_by_layer()`
  - `create_layers_from_config()`
- **v1.1.0**: Refactored `extract_insert_attributes()` to support two-level blocks
- **v1.0.0**: Initial release with file, JSON, and DXF operations
