Spaces:
Running
Running
| from __future__ import annotations | |
| import re | |
| from pathlib import Path | |
| import plotly.graph_objects as go | |
| _MOVE_RE = re.compile( | |
| r"^\s*(G0|G1)\s+X(-?\d+(?:\.\d+)?)\s+Y(-?\d+(?:\.\d+)?)" | |
| r"(?:\s+Z(-?\d+(?:\.\d+)?))?", | |
| re.IGNORECASE, | |
| ) | |
| def parse_gcode_path(gcode_text: str) -> dict: | |
| relative = True | |
| x = y = z = 0.0 | |
| print_segments: list[list[tuple[float, float, float]]] = [] | |
| travel_segments: list[list[tuple[float, float, float]]] = [] | |
| current_kind: str | None = None | |
| current_segment: list[tuple[float, float, float]] = [] | |
| all_x: list[float] = [] | |
| all_y: list[float] = [] | |
| all_z: list[float] = [] | |
| def flush_segment() -> None: | |
| nonlocal current_segment, current_kind | |
| if current_segment and current_kind is not None: | |
| target = print_segments if current_kind == "print" else travel_segments | |
| target.append(current_segment) | |
| current_segment = [] | |
| current_kind = None | |
| for raw_line in gcode_text.splitlines(): | |
| line = raw_line.strip() | |
| if not line: | |
| flush_segment() | |
| continue | |
| upper = line.upper() | |
| if upper.startswith("G90"): | |
| relative = False | |
| continue | |
| if upper.startswith("G91"): | |
| relative = True | |
| continue | |
| match = _MOVE_RE.match(line) | |
| if not match: | |
| flush_segment() | |
| continue | |
| gcmd = match.group(1).upper() | |
| dx = float(match.group(2)) | |
| dy = float(match.group(3)) | |
| dz_match = match.group(4) | |
| dz = float(dz_match) if dz_match is not None else 0.0 | |
| prev_pos = (x, y, z) | |
| if relative: | |
| x += dx | |
| y += dy | |
| z += dz | |
| else: | |
| x, y = dx, dy | |
| if dz_match is not None: | |
| z = dz | |
| kind = "travel" if gcmd == "G1" else "print" | |
| if kind != current_kind: | |
| flush_segment() | |
| current_kind = kind | |
| current_segment = [prev_pos] | |
| current_segment.append((x, y, z)) | |
| all_x.append(x) | |
| all_y.append(y) | |
| all_z.append(z) | |
| flush_segment() | |
| if all_x: | |
| bounds = ( | |
| (min(all_x), min(all_y), min(all_z)), | |
| (max(all_x), max(all_y), max(all_z)), | |
| ) | |
| else: | |
| bounds = ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)) | |
| return { | |
| "print_segments": print_segments, | |
| "travel_segments": travel_segments, | |
| "bounds": bounds, | |
| "point_count": len(all_x), | |
| } | |
| def _segments_to_xyz( | |
| segments: list[list[tuple[float, float, float]]], | |
| ) -> tuple[list[float | None], list[float | None], list[float | None]]: | |
| xs: list[float | None] = [] | |
| ys: list[float | None] = [] | |
| zs: list[float | None] = [] | |
| for segment in segments: | |
| for px, py, pz in segment: | |
| xs.append(px) | |
| ys.append(py) | |
| zs.append(pz) | |
| xs.append(None) | |
| ys.append(None) | |
| zs.append(None) | |
| return xs, ys, zs | |
| def build_toolpath_figure( | |
| parsed: dict, | |
| travel_opacity: float = 0.55, | |
| print_opacity: float = 1.0, | |
| travel_color: str = "#969696", | |
| print_color: str = "#1f77b4", | |
| ) -> go.Figure: | |
| print_xs, print_ys, print_zs = _segments_to_xyz(parsed["print_segments"]) | |
| travel_xs, travel_ys, travel_zs = _segments_to_xyz(parsed["travel_segments"]) | |
| fig = go.Figure() | |
| if travel_xs: | |
| fig.add_trace( | |
| go.Scatter3d( | |
| x=travel_xs, | |
| y=travel_ys, | |
| z=travel_zs, | |
| mode="lines", | |
| name="Travel (G0)", | |
| opacity=travel_opacity, | |
| line=dict(color=travel_color, width=2, dash="dot"), | |
| hoverinfo="skip", | |
| ) | |
| ) | |
| if print_xs: | |
| fig.add_trace( | |
| go.Scatter3d( | |
| x=print_xs, | |
| y=print_ys, | |
| z=print_zs, | |
| mode="lines", | |
| name="Print (G1)", | |
| opacity=print_opacity, | |
| line=dict(color=print_color, width=4), | |
| hovertemplate="X=%{x:.2f}<br>Y=%{y:.2f}<br>Z=%{z:.2f}<extra></extra>", | |
| ) | |
| ) | |
| (x_min, y_min, z_min), (x_max, y_max, z_max) = parsed["bounds"] | |
| fig.update_layout( | |
| uirevision="toolpath", | |
| scene=dict( | |
| xaxis_title="X (mm)", | |
| yaxis_title="Y (mm)", | |
| zaxis_title="Z (mm)", | |
| aspectmode="data", | |
| ), | |
| margin=dict(l=0, r=0, t=30, b=0), | |
| legend=dict(orientation="h", yanchor="bottom", y=1.0, xanchor="left", x=0.0), | |
| title=( | |
| f"Tool path — {len(parsed['print_segments'])} print / " | |
| f"{len(parsed['travel_segments'])} travel segments " | |
| f"X[{x_min:.1f},{x_max:.1f}] Y[{y_min:.1f},{y_max:.1f}] " | |
| f"Z[{z_min:.1f},{z_max:.1f}]" | |
| ), | |
| ) | |
| return fig | |
| def render_gcode_file(path: str | Path) -> tuple[go.Figure, dict]: | |
| text = Path(path).read_text() | |
| parsed = parse_gcode_path(text) | |
| return build_toolpath_figure(parsed), parsed | |