Source code for gcode_reader.visualize

import pandas as pd
import numpy as np

from .analysis import _df_column_name_validation
from .emulate.operations import Operation, ProcessData
from .emulate._dataframe import insert_positive_start_cumsum
from .emulate.additive_part import AdditivePart


[docs] def plot_gcode_dataframe(df, x="x", y="y", z="z", tag=None, **kwargs): """Generates and displays a 3D plot of a G-code toolpath from a DataFrame. Args: df (pd.DataFrame): DataFrame containing processed G-code. It is expected to have columns for coordinates and an optional 'tags' column. x (str, optional): The name of the column containing the X-axis data. Defaults to "x". y (str, optional): The name of the column containing the Y-axis data. Defaults to "y". z (str, optional): The name of the column containing the Z-axis data. Defaults to "z". tag (str, optional): A tag to filter the DataFrame. If provided, only rows where this tag is present in the 'tags' column will be plotted. Defaults to None. **kwargs: Additional keyword arguments to pass to `plotly.express.line_3d`. Returns: plotly.graph_objects.Figure: The Plotly Figure object. """ # NOTE: Using a lazy load import here. Not the best practice, but it keeps plotly as an optional dependency import plotly.express as px if tag is None: df_plot = df else: # Vectorized string search: find rows where tag appears in tags column # Use str.contains() which is much faster than iterrows() tag_mask = df["tags"].str.contains(tag, na=False, regex=False) index = df[tag_mask].index if len(index) == 0: print("No data was found") return None # Make a copy and add 1 so lines are completed index_2 = index.copy() index_2 += 1 # Union the index and index_2 index = index.union(index_2) if index[-1] >= len(df): index = index[:-1] df_plot = df.loc[index] fig = px.line_3d(df_plot, x=x, y=y, z=z, **kwargs) fig.update_layout(scene_aspectmode="data", showlegend=False) return fig
[docs] def plot_operation( operation: Operation, command_type: type = None, **kwargs, ): """ "Generates and displays a 3D plot from a processed Operation object. This function visualizes the toolpath defined by an Operation. It can either plot the entire path as a single continuous line or plot only the segments that correspond to a specific command type (e.g., `Move`) as separate polylines. Args: operation (Operation): The processed Operation object to be plotted. command_type (type, optional): A command class used to filter the plot. If provided, only toolpath segments generated by this command type (or its subclasses) will be shown. Defaults to None. **kwargs: Additional keyword arguments to pass to `plotly.express.line_3d`. Returns: plotly.graph_objects.Figure: The Plotly Figure object. Raises: ValueError: If the operation has not been processed or if the number of commands does not match the number of process data points. """ # NOTE: Using a lazy load import here. Not the best practice, but it keeps plotly as an optional dependency import plotly.express as px # Ensure the Operation has been processed n_commands = len(operation.commands) n_process_data = len(operation.process_data) if not n_commands == n_process_data: raise ValueError( "The number of commands does not match the number of process data points." ) elif operation.processed_by_machine is None: raise ValueError("The operation must be processed by a machine.") # Create a DataFrame from the operation more efficiently: # Extract fields directly to arrays instead of calling to_dict() for each record elapsed_times = np.array([data.elapsed_time for data in operation.process_data]) locations = np.array( [ data.location if data.location is not None else (np.nan, np.nan, np.nan) for data in operation.process_data ] ) tool_directions = np.array( [ ( data.tool_direction if data.tool_direction is not None else (np.nan, np.nan, np.nan) ) for data in operation.process_data ] ) relative_movements = np.array( [ ( data.relative_movement if data.relative_movement is not None else (np.nan, np.nan, np.nan) ) for data in operation.process_data ] ) distances = np.array([data.distance for data in operation.process_data]) feed_rates = np.array([data.feed_rate for data in operation.process_data]) # Build DataFrame from pre-materialized arrays df = pd.DataFrame( { "elapsed_time": elapsed_times, "location": [tuple(loc) for loc in locations], "tool_direction": [tuple(td) for td in tool_directions], "relative_movement": [tuple(rm) for rm in relative_movements], "distance": distances, "feed_rate": feed_rates, } ) df[["x", "y", "z"]] = pd.DataFrame(locations, index=df.index) if command_type is not None: df["cmd_match"] = [ int(isinstance(cmd, command_type)) for cmd in operation.commands ] df = insert_positive_start_cumsum(df, "cmd_match", "line") is_start_point = (df["cmd_match"] == 0) & (df["line"].shift(-1) > 0) df.loc[is_start_point, "line"] = df["line"].shift(-1)[is_start_point] else: df["line"] = 0 fig = px.line_3d( df, x="x", y="y", z="z", title=f'{operation.name}{f": {command_type.__name__} Commands" if command_type is not None else ""}', line_group="line", # This tells Plotly to draw a separate line for each unique line value **kwargs, ) fig.update_layout(scene_aspectmode="data", showlegend=False) return fig
[docs] def plot_event_series( event_series: pd.DataFrame, color_by="e", colorscale="Viridis", **kwargs ): """Creates a 3D plot of an Event Series colored by a continuous scalar value. Args: event_series (pd.DataFrame): DataFrame containing the event series data. Must include columns for 'x', 'y', 'z', 'e', 'time'. color_by (str, optional): The name of the column used to color the line. Defaults to "e". colorscale (str, optional): The name of the Plotly colorscale to use for the gradient. Defaults to "Viridis". show (bool, optional): If True, the plot is displayed immediately. Else return the figure. Defaults to True. **kwargs: Additional keyword arguments to pass to `fig.update_layout`. Returns: plotly.graph_objects.Figure: The Plotly Figure object. """ import plotly.express as px import plotly.graph_objects as go _df_column_name_validation(event_series, ("x", "y", "z", "e", "time")) df = event_series.copy() colorbar_title = color_by.capitalize() fig = go.Figure( data=[ go.Scatter3d( x=df["x"], y=df["y"], z=df["z"], mode="lines", # This tells Scatter3d to connect the points line=dict( color=df[color_by], colorscale=colorscale, width=6, showscale=True, colorbar=dict(title=colorbar_title), ), **kwargs, ) ] ) fig.update_layout(scene_aspectmode="data") return fig
[docs] def plot_additive_part( part: AdditivePart, layer_range=None, layer_index=None, **kwargs ): """Generates and displays a 3D plot from a AdditivePart object. This function visualizes the depoisition toolpath defined in an AdditivePart for a provided range of layers, or a specified layer index. Args: part (AdditivePart): The AdditivePart object to be plotted. layer_range (tuple, optional): A range of layer indices to plot. layer_index (int, optional): A specified layer index to plot. Takes precidence over layer_range. **kwargs: Additional keyword arguments to pass to `plotly.express.line_3d`. Returns: plotly.graph_objects.Figure: The Plotly Figure object. """ import plotly.express as px import logging if layer_index is not None: layer_index = int(layer_index) layer_range = (layer_index, layer_index + 1) layer_range = part._validate_layer_range(layer_range) all_points = [] path_group = [] filament_idx = 1 for layer in part.generate_layers(layer_range): for filament in layer.filaments: locs = filament.locations if len(locs) == 0: continue all_points.append(locs) path_group.extend([filament_idx] * len(locs)) filament_idx += 1 if not all_points: logging.warning("No deposition paths found for the specified layer range.") return None all_points = np.vstack(all_points) df = pd.DataFrame(all_points, columns=["x", "y", "z"]) df["filament"] = path_group df["filament"] = df["filament"].astype(str) fig = px.line_3d( df, x="x", y="y", z="z", line_group="filament", # This breaks the line into separate groups title=f"Additive Part - Layers ({layer_range[0]}, {layer_range[1]})", **kwargs, ) fig.update_layout(scene_aspectmode="data") return fig