- "engine/output/dxf.py" - "engine/api/routes.py" - "engine/tests/test_output.py" GSD-Task: S01/T01
116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
"""DXF output generator — AC1015+ DXF from PostProcessResult using ezdxf."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
from typing import Literal
|
|
|
|
import ezdxf
|
|
|
|
from pipeline.postprocess import PathInfo, PostProcessResult
|
|
|
|
# DXF $INSUNITS codes per the HEADER variable specification
|
|
_INSUNITS_MAP: dict[str, int] = {
|
|
"inches": 1,
|
|
"mm": 4,
|
|
}
|
|
|
|
# $MEASUREMENT: 0 = Imperial (inches), 1 = Metric (mm)
|
|
_MEASUREMENT_MAP: dict[str, int] = {
|
|
"inches": 0,
|
|
"mm": 1,
|
|
}
|
|
|
|
|
|
def _add_path_to_msp(
|
|
msp: ezdxf.layouts.BaseLayout,
|
|
path: PathInfo,
|
|
layer: str = "0",
|
|
scale_factor: float = 1.0,
|
|
) -> None:
|
|
"""Add a single PathInfo as an LWPOLYLINE entity to the modelspace.
|
|
|
|
Closed paths get the LWPOLYLINE close flag set.
|
|
Islands are placed on a separate "ISLANDS" layer for downstream CAM tools.
|
|
|
|
When *scale_factor* is not 1.0, all coordinates are multiplied by it
|
|
to convert from pixel space to real-world units.
|
|
"""
|
|
coords = path.simplified_coords
|
|
if len(coords) < 2:
|
|
return
|
|
|
|
target_layer = "ISLANDS" if path.is_island else layer
|
|
|
|
# LWPOLYLINE uses (x, y[, start_width, end_width, bulge]) tuples
|
|
points = [(x * scale_factor, y * scale_factor) for x, y in coords]
|
|
|
|
# Remove duplicate close point if the polyline close flag handles it
|
|
if path.is_closed and len(points) > 1 and points[0] == points[-1]:
|
|
points = points[:-1]
|
|
|
|
msp.add_lwpolyline(
|
|
points,
|
|
dxfattribs={"layer": target_layer},
|
|
close=path.is_closed,
|
|
)
|
|
|
|
|
|
def generate_dxf(
|
|
result: PostProcessResult,
|
|
*,
|
|
units: Literal["inches", "mm"] | None = None,
|
|
scale_factor: float = 1.0,
|
|
layer_map: dict[int, str] | None = None,
|
|
) -> bytes:
|
|
"""Generate an AC1015 (AutoCAD R2000) DXF file from post-processed path data.
|
|
|
|
Creates LWPOLYLINE entities from simplified coordinate data. Islands (holes)
|
|
are placed on an "ISLANDS" layer; outer contours on the default "0" layer.
|
|
|
|
Args:
|
|
result: PostProcessResult from the post-processing pipeline.
|
|
units: Target unit system — ``'inches'`` or ``'mm'``. When set, the
|
|
``$INSUNITS`` and ``$MEASUREMENT`` DXF header variables are written
|
|
so downstream tools (Inkscape, LightBurn, AutoCAD) interpret
|
|
coordinate values in the correct unit.
|
|
scale_factor: Multiplier applied to every coordinate. Use this to
|
|
convert from pixel space to real-world units (e.g. 1/96 for
|
|
96 PPI → inches, 25.4/96 for 96 PPI → mm).
|
|
layer_map: Optional mapping of path index → DXF layer name. Paths
|
|
whose index is absent fall back to the default layer logic
|
|
(``"ISLANDS"`` for islands, ``"0"`` for outer contours).
|
|
|
|
Returns:
|
|
DXF file content as bytes.
|
|
"""
|
|
doc = ezdxf.new(dxfversion="R2000") # AC1015
|
|
|
|
# --- Unit metadata in DXF header ---
|
|
if units is not None:
|
|
doc.header["$INSUNITS"] = _INSUNITS_MAP[units]
|
|
doc.header["$MEASUREMENT"] = _MEASUREMENT_MAP[units]
|
|
|
|
msp = doc.modelspace()
|
|
|
|
# Create ISLANDS layer for hole/island paths
|
|
doc.layers.add("ISLANDS", color=1) # color 1 = red in AutoCAD
|
|
|
|
# Create any custom layers requested by layer_map
|
|
if layer_map:
|
|
for layer_name in set(layer_map.values()):
|
|
if layer_name not in ("0", "ISLANDS"):
|
|
doc.layers.add(layer_name)
|
|
|
|
for idx, path in enumerate(result.paths):
|
|
# Determine the base layer: explicit layer_map entry, or default logic
|
|
if layer_map and idx in layer_map:
|
|
base_layer = layer_map[idx]
|
|
else:
|
|
base_layer = "0"
|
|
_add_path_to_msp(msp, path, layer=base_layer, scale_factor=scale_factor)
|
|
|
|
# Write to string buffer, then encode to bytes
|
|
stream = io.StringIO()
|
|
doc.write(stream)
|
|
return stream.getvalue().encode("utf-8")
|