2Robust DXF export utilities for chamber geometry.
4This module provides functions to export chamber contours as CAD-compatible DXF files
5with proper handling of:
6- Coordinate quantization (consistent precision)
7- Vertex deduplication (no overlapping points)
8- Zero-length segment removal
9- Endpoint continuity validation
10- Single closed LWPOLYLINE output
14from pathlib
import Path
15from typing
import Optional, Tuple
33 Round coordinates to fixed decimal places for CAD compatibility.
35 This ensures consistent coordinate representation and eliminates
36 floating-point precision issues that cause gaps in CAD imports.
41 Array of (x, y) points, shape (N, 2)
43 Number of decimal places to round to (default: 6)
48 Quantized points array with same shape
50 return np.round(pts, decimals=precision)
55 Remove duplicate/near-duplicate consecutive vertices.
57 This handles the case where segment boundaries have overlapping points,
58 ensuring each vertex appears exactly once in the output polyline.
63 Array of (x, y) points, shape (N, 2)
65 Distance threshold - points closer than this are considered duplicates
70 Deduplicated points array
76 keep_mask = np.ones(len(pts), dtype=bool)
79 for i
in range(1, len(pts)):
80 dist = np.linalg.norm(pts[i] - pts[i-1])
89 Remove points that would create zero-length line segments.
91 This is similar to deduplicate_vertices but specifically targets
92 zero-length segments that cause CAD import errors.
97 Array of (x, y) points, shape (N, 2)
99 Length threshold - segments shorter than this are removed
104 Cleaned points array with no zero-length segments
111def validate_continuity(pts: np.ndarray, max_gap: float = 1e-6) -> Tuple[bool, Optional[int], Optional[float]]:
113 Validate that the polyline forms a continuous path.
115 Checks that there are no unexpected gaps between consecutive vertices.
120 Array of (x, y) points, shape (N, 2)
122 Maximum allowed gap between consecutive points for typical geometry.
123 Gaps larger than this trigger a warning.
127 Tuple[bool, Optional[int], Optional[float]]
128 (is_valid, gap_index, gap_distance)
129 - is_valid: True if all consecutive points are reasonably close
130 - gap_index: Index of first large gap (if any)
131 - gap_distance: Distance of first large gap (if any)
134 return True,
None,
None
137 diffs = np.diff(pts, axis=0)
138 distances = np.linalg.norm(diffs, axis=1)
141 median_dist = np.median(distances)
146 suspicious_threshold =
max(max_gap, median_dist * 100)
148 for i, dist
in enumerate(distances):
149 if dist > suspicious_threshold:
150 return False, i, dist
152 return True,
None,
None
157 Ensure vertices at segment boundaries are properly shared.
159 After quantization, this ensures that connecting points between
160 segments (cylindrical->contraction->nozzle) are exactly the same.
165 Array of (x, y) points, shape (N, 2)
167 Number of decimal places for coordinate quantization
172 Points array with shared vertices at boundaries
184 precision: int = DXF_PRECISION,
185 epsilon: float = VERTEX_EPSILON) -> np.ndarray:
187 Full preparation pipeline for polyline points.
189 Applies all cleaning steps in the correct order:
190 1. Quantize coordinates
191 2. Deduplicate vertices
192 3. Remove zero-length segments
193 4. Validate continuity
198 Raw array of (x, y) points, shape (N, 2)
200 Decimal places for coordinate quantization
202 Threshold for vertex deduplication
207 Cleaned, validated points ready for DXF export
223 f
"Large gap detected in geometry at index {gap_idx}: {gap_dist:.6e} m. "
224 f
"This may indicate a geometry generation issue."
232 add_centerline: bool =
True,
233 add_mirrored: bool =
False,
234 layer_name: str =
"CONTOUR",
235 centerline_layer: str =
"CENTERLINE",
236 precision: int = DXF_PRECISION) ->
None:
238 Export chamber contour to a CAD-compatible DXF file.
240 Creates a single LWPOLYLINE with properly shared vertices,
241 quantized coordinates, and no duplicate or zero-length entities.
245 chamber_pts : np.ndarray
246 Array of (x, y) points representing the chamber contour
248 Path to save the DXF file
249 add_centerline : bool
250 If True, adds a centerline (y=0) as a reference line
252 If True, also adds the mirrored (lower half) contour
254 DXF layer name for the contour
255 centerline_layer : str
256 DXF layer name for the centerline
258 Decimal places for coordinate quantization
262 "ezdxf library is required for DXF export. "
263 "Install it with: pip install ezdxf"
267 dxf_path = Path(export_path)
268 dxf_path.parent.mkdir(parents=
True, exist_ok=
True)
274 doc = ezdxf.new(
'R2010')
275 msp = doc.modelspace()
278 doc.layers.add(layer_name, color=7)
280 doc.layers.add(centerline_layer, color=1)
283 points = [(float(pt[0]), float(pt[1]))
for pt
in cleaned_pts]
287 polyline = msp.add_lwpolyline(
289 dxfattribs={
'layer': layer_name}
295 mirrored_points = [(float(pt[0]), -float(pt[1]))
for pt
in reversed(cleaned_pts)]
298 dxfattribs={
'layer': layer_name}
303 x_min = cleaned_pts[:, 0].
min()
304 x_max = cleaned_pts[:, 0].
max()
306 margin = (x_max - x_min) * 0.05
308 (float(x_min - margin), 0.0),
309 (float(x_max + margin), 0.0),
310 dxfattribs={
'layer': centerline_layer}
314 doc.saveas(str(dxf_path))
315 print(f
"Chamber contour exported to {export_path}")
316 print(f
" - Points: {len(cleaned_pts)} (after deduplication)")
317 print(f
" - Precision: {precision} decimal places")
322 layer_name: str =
"CONTOUR",
323 precision: int = DXF_PRECISION) ->
None:
325 Export chamber contour as a closed LWPOLYLINE (full cross-section).
327 Creates a single closed polyline representing the full chamber
328 cross-section by mirroring the upper half contour.
332 chamber_pts : np.ndarray
333 Array of (x, y) points representing the upper half contour
335 Path to save the DXF file
337 DXF layer name for the contour
339 Decimal places for coordinate quantization
343 "ezdxf library is required for DXF export. "
344 "Install it with: pip install ezdxf"
351 lower_pts = upper_pts[::-1].copy()
352 lower_pts[:, 1] = -lower_pts[:, 1]
363 full_pts = np.vstack([upper_pts, lower_pts[1:-1]])
369 dxf_path = Path(export_path)
370 dxf_path.parent.mkdir(parents=
True, exist_ok=
True)
373 doc = ezdxf.new(
'R2010')
374 msp = doc.modelspace()
377 doc.layers.add(layer_name, color=7)
380 points = [(float(pt[0]), float(pt[1]))
for pt
in full_pts]
383 polyline = msp.add_lwpolyline(
386 dxfattribs={
'layer': layer_name}
390 doc.saveas(str(dxf_path))
391 print(f
"Closed chamber contour exported to {export_path}")
392 print(f
" - Points: {len(full_pts)}")
396if __name__ ==
"__main__":
401 x = np.linspace(-0.1, 0.2, 100)
402 y = np.where(x < 0, 0.05, 0.05 - x * 0.1)
403 pts = np.column_stack([x, y])
406 pts = np.vstack([pts[:50], pts[49:51], pts[50:]])
408 print(
"Testing DXF export utilities...")
409 print(f
"Original points: {len(pts)}")
412 print(f
"Cleaned points: {len(cleaned)}")
416 print(
"Test complete!")
np.ndarray deduplicate_vertices(np.ndarray pts, float epsilon=VERTEX_EPSILON)
np.ndarray ensure_shared_vertices(np.ndarray pts, int precision=DXF_PRECISION)
np.ndarray prepare_polyline_points(np.ndarray pts, int precision=DXF_PRECISION, float epsilon=VERTEX_EPSILON)
None export_closed_contour_dxf(np.ndarray chamber_pts, str export_path, str layer_name="CONTOUR", int precision=DXF_PRECISION)
np.ndarray remove_zero_length_segments(np.ndarray pts, float epsilon=VERTEX_EPSILON)
None export_chamber_dxf(np.ndarray chamber_pts, str export_path, bool add_centerline=True, bool add_mirrored=False, str layer_name="CONTOUR", str centerline_layer="CENTERLINE", int precision=DXF_PRECISION)
np.ndarray quantize_coordinates(np.ndarray pts, int precision=DXF_PRECISION)
Tuple[bool, Optional[int], Optional[float]] validate_continuity(np.ndarray pts, float max_gap=1e-6)