From 9135d939ff1434f8ed3f06275dd378176c52ca4f Mon Sep 17 00:00:00 2001 From: Anders Wolstrup Date: Fri, 12 Jun 2026 13:52:06 +0100 Subject: [PATCH 1/4] Added infinite axis to lab (initial commit) --- lab/fullcontrol/infaxis/__init__.py | 7 + lab/fullcontrol/infaxis/axis.py | 22 ++ lab/fullcontrol/infaxis/common.py | 336 +++++++++++++++++++++++++ lab/fullcontrol/infaxis/controls.py | 16 ++ lab/fullcontrol/infaxis/point.py | 170 +++++++++++++ lab/fullcontrol/infaxis/printer.py | 32 +++ lab/fullcontrol/infaxis/state.py | 101 ++++++++ lab/fullcontrol/infaxis/steps2gcode.py | 25 ++ tutorials/lab_infaxis_4_demo.ipynb | 94 +++++++ tutorials/lab_infaxis_5_demo.ipynb | 152 +++++++++++ 10 files changed, 955 insertions(+) create mode 100644 lab/fullcontrol/infaxis/__init__.py create mode 100644 lab/fullcontrol/infaxis/axis.py create mode 100644 lab/fullcontrol/infaxis/common.py create mode 100644 lab/fullcontrol/infaxis/controls.py create mode 100644 lab/fullcontrol/infaxis/point.py create mode 100644 lab/fullcontrol/infaxis/printer.py create mode 100644 lab/fullcontrol/infaxis/state.py create mode 100644 lab/fullcontrol/infaxis/steps2gcode.py create mode 100644 tutorials/lab_infaxis_4_demo.ipynb create mode 100644 tutorials/lab_infaxis_5_demo.ipynb diff --git a/lab/fullcontrol/infaxis/__init__.py b/lab/fullcontrol/infaxis/__init__.py new file mode 100644 index 00000000..6a8a225e --- /dev/null +++ b/lab/fullcontrol/infaxis/__init__.py @@ -0,0 +1,7 @@ +from .point import Point +from .controls import GcodeControls +from .printer import Printer +from .state import State +from .steps2gcode import gcode +from .common import transform +from .axis import Axis \ No newline at end of file diff --git a/lab/fullcontrol/infaxis/axis.py b/lab/fullcontrol/infaxis/axis.py new file mode 100644 index 00000000..b9291ea0 --- /dev/null +++ b/lab/fullcontrol/infaxis/axis.py @@ -0,0 +1,22 @@ +from typing import Optional +from pydantic import BaseModel +from fullcontrol import Point + +class Axis(BaseModel): + name: Optional[str] = None # Used for the output gcode. And sets type if type not given + type: Optional[str] = None # 'A', 'B', 'C', 'X', 'Y', 'Z' + active: float = 0 # current position (for rotational axes, the angle of rotation in degrees. for linear axes, the position in mm.) + orientation: float = 1 # 1 or -1, 1 means the axis follows mathematical convention (either positive/counterclockwise rotation or fits the right hand rule for linear axes) + offset: Optional[Point] = None # the offset in mm in x,y,z from the previous axis in the chain. + + def __init__(self, **data): + super().__init__(**data) + + if self.type is None: + if self.name is not None and self.name in ["A", "B", "C"]: + self.type = self.name + else: + raise ValueError("Cannot set axis type based on name") + + if self.offset is None: + self.offset = Point(x=0, y=0, z=0) diff --git a/lab/fullcontrol/infaxis/common.py b/lab/fullcontrol/infaxis/common.py new file mode 100644 index 00000000..9af97cee --- /dev/null +++ b/lab/fullcontrol/infaxis/common.py @@ -0,0 +1,336 @@ +from typing import Union + +import numpy as np +import plotly.graph_objects as go +from fullcontrol.visualize.tube_mesh import CylindersMesh, FlowTubeMesh, MeshExporter +from fullcontrol.visualize.controls import PlotControls +from fullcontrol.visualize.plot_data import PlotData +from fullcontrol.visualize.state import State +from fullcontrol.visualize.plotly import generate_mesh +# # see comment in __init__.py about why this module exists + +# # import functions and classes that will be accessible to the user +from fullcontrol.common import check +from fullcontrol.geometry import move, move_polar, travel_to # don't import all geometry functions since they are not designed for multiaxis Points +from fullcontrol.combinations.gcode_and_visualize.classes import * +import fullcontrol.geometry as xyz_geom + +def generate_single_path_vase_lod_surface_arrays( + path, + center_xy=None, + points_per_turn: int = 128, + turn_stride: int = 2, + color_mode: str = "none", +): + points = np.asarray([path.xvals, path.yvals, path.zvals], dtype=np.float32).T + + if len(points) < 4: + return None, None, None, None + + good = np.ones(len(points), dtype=bool) + dups = np.all(np.diff(points, axis=0) == 0, axis=1) + good[1:] = ~dups + points = points[good] + + if len(points) < 4: + return None, None, None, None + + source_color_scalar = None + + if color_mode == "path": + if getattr(path, "colors", None) is not None and len(path.colors) > 0: + colors_arr = np.asarray(path.colors, dtype=np.float32) + + if len(colors_arr) == len(good): + colors_arr = colors_arr[good] + + if len(colors_arr) == len(points): + source_color_scalar = ( + 0.299 * colors_arr[:, 0] + + 0.587 * colors_arr[:, 1] + + 0.114 * colors_arr[:, 2] + ).astype(np.float32) + + if center_xy is None: + cx = np.float32(0.5 * (np.min(points[:, 0]) + np.max(points[:, 0]))) + cy = np.float32(0.5 * (np.min(points[:, 1]) + np.max(points[:, 1]))) + else: + cx, cy = center_xy + + theta = np.unwrap(np.arctan2(points[:, 1] - cy, points[:, 0] - cx)).astype(np.float32) + + if theta[-1] < theta[0]: + theta = -theta + + turn_coord = ((theta - theta[0]) / np.float32(2.0 * np.pi)).astype(np.float32) + + monotonic_good = np.concatenate([[True], np.diff(turn_coord) > 1e-10]) + turn_coord = turn_coord[monotonic_good] + points = points[monotonic_good] + + if source_color_scalar is not None: + source_color_scalar = source_color_scalar[monotonic_good] + + if len(points) < 4: + return None, None, None, None + + first_complete_turn = int(np.ceil(turn_coord[0])) + last_complete_turn = int(np.floor(turn_coord[-1])) - 1 + + if last_complete_turn <= first_complete_turn: + return None, None, None, None + + all_turns = np.arange(first_complete_turn, last_complete_turn + 1, dtype=np.float32) + + selected_turns = all_turns[::max(1, int(turn_stride))] + + if selected_turns[-1] != all_turns[-1]: + selected_turns = np.append(selected_turns, all_turns[-1]).astype(np.float32) + + if len(selected_turns) < 2: + return None, None, None, None + + phases = np.linspace( + 0.0, + 1.0, + points_per_turn + 1, + endpoint=True, + dtype=np.float32, + ) + + targets = selected_turns[:, None] + phases[None, :] + targets_flat = targets.ravel() + + x_grid = np.interp(targets_flat, turn_coord, points[:, 0]).reshape(targets.shape).astype(np.float32) + y_grid = np.interp(targets_flat, turn_coord, points[:, 1]).reshape(targets.shape).astype(np.float32) + z_grid = np.interp(targets_flat, turn_coord, points[:, 2]).reshape(targets.shape).astype(np.float32) + + if color_mode == "path" and source_color_scalar is not None: + color_grid = np.interp( + targets_flat, + turn_coord, + source_color_scalar, + ).reshape(targets.shape).astype(np.float32) + + elif color_mode == "height": + color_grid = z_grid + + elif color_mode == "turn": + color_grid = targets.astype(np.float32) + + else: + color_grid = np.zeros_like(z_grid, dtype=np.float32) + + return x_grid, y_grid, z_grid, color_grid + +def generate_single_path_vase_lod_surface_trace( + path, + center_xy=None, + points_per_turn: int = 128, + turn_stride: int = 2, + color_mode: str = "none", + colorscale="Viridis", + showscale: bool = False, + opacity: float = 1.0, +): + """ + Drop-in Plotly Surface trace for fast vase preview. + + Returns: + go.Surface or None + """ + + x, y, z, surfacecolor = generate_single_path_vase_lod_surface_arrays( + path, + center_xy=center_xy, + points_per_turn=points_per_turn, + turn_stride=turn_stride, + color_mode=color_mode, + ) + + if x is None: + return None + + return go.Surface( + x=x, + y=y, + z=z, + surfacecolor=surfacecolor, + colorscale=colorscale, + showscale=showscale, + opacity=opacity, + hoverinfo="skip", + contours=dict( + x=dict(show=False), + y=dict(show=False), + z=dict(show=False), + ), + # lighting=dict( + # ambient=0.65, + # diffuse=0.7, + # specular=0.05, + # roughness=1.0, + # ), + showlegend=False, + ) + + + +def fig_plot(data: PlotData, controls: PlotControls): + ''' + Plot data for x y z lines with RGB colors and annotations. + The style of the plot is governed by the controls. + + Args: + data (PlotData): The data to be plotted. + controls (PlotControls): The controls for customizing the plot. + + Returns: + None + ''' + + fig = go.Figure() + + if controls.tube_type is not None: + Mesh = {'flow': FlowTubeMesh, 'cylinders': CylindersMesh}[controls.tube_type] + else: # Fall back to FlowTubeMesh if no tube_type is explicitly specified + Mesh = FlowTubeMesh + + # generate line plots + max_width = 0 + + for path in data.paths: + colors_now = [f'rgb({color[0]*255:.2f}, {color[1]*255:.2f}, {color[2]*255:.2f})' for color in path.colors] + + linewidth_now = controls.line_width * 2 if path.extruder.on == True else controls.line_width * 0.5 + + ## Generate mesh now imported from main fullcontrol visualization module, the only reason it was here was for the global local_max variable, I've just changed the plot function to derive a similar variable from the path width instead. + if path.widths: + max_width = max(max_width, max(path.widths)) + + if path.extruder.on and controls.style == "vase_surface": + surface_trace = generate_single_path_vase_lod_surface_trace( + path, + points_per_turn=96, + turn_stride=3, + color_mode="height", + colorscale="viridis", + showscale=False, + opacity=1.0, + ) + + if surface_trace is not None: + fig.add_trace(surface_trace) + + + elif path.extruder.on and controls.style == 'tube': + sides, rounding_strength, flat_sides = controls.tube_sides, 0.4, False + mesh = generate_mesh(path, linewidth_now, Mesh, sides, rounding_strength, flat_sides, colors_now) + fig.add_trace(mesh.to_Mesh3d(colors=colors_now)) + + elif not controls.hide_travel or path.extruder.on: + fig.add_trace(go.Scatter3d( + mode='lines', + x=path.xvals, + y=path.yvals, + z=path.zvals, + showlegend=False, + line=dict(width=linewidth_now, color=colors_now), + )) + + + # find a bounding box, to create a plot with equally proportioned X Y Z scales (so a cuboid looks like a cuboid, not a cube) + bounding_box_size = max(data.bounding_box.maxx-data.bounding_box.minx, data.bounding_box.maxy - + data.bounding_box.miny, data.bounding_box.maxz-min(0, data.bounding_box.minz)) + bounding_box_size += 0.002 + bounding_box_size += max_width + + # generate annotations + annotations_pts = [] + annotations = [] + if controls.hide_annotations == False and not controls.neat_for_publishing: + # if controls.hide_annotations == False: # and not controls.neat_for_publishing: + for annotation in data.annotations: + x, y, z = (annotation[axis] for axis in 'xyz') + annotations_pts.append([x, y, z]) + annotations.append(dict( + showarrow=False, + x=x, y=y, z=z, + text=annotation['label'], + yshift=10)) + xs, ys, zs = zip(*annotations_pts) if annotations_pts else [[]]*3 + fig.add_trace(go.Scatter3d(mode='markers', x=xs, y=ys, z=zs, showlegend=False, marker=dict(size=2, color='red'))) + + # make sure the bounding box is big enough for the annotations + # the 0.001 is to make sure the annotations don't lie on the boundary + midx, midy, midz = (getattr(data.bounding_box, f'mid{axis}') for axis in 'xyz') + range + offset = 0.001 + offset_both_sides = 2 * offset + for (x, y, z) in annotations_pts: + if x < midx - bounding_box_size / 2 + offset: + bounding_box_size = 2 * (midx - x) + offset_both_sides + if x > midx + bounding_box_size / 2 - offset: + bounding_box_size = 2 * (x - midx) + offset_both_sides + if y < midy - bounding_box_size / 2 + offset: + bounding_box_size = 2 * (midy - y) + offset_both_sides + if y > midy + bounding_box_size / 2 - offset: + bounding_box_size = 2 * (y - midy) + offset_both_sides + if z < midz - bounding_box_size / 2 + offset: + bounding_box_size = 2 * (midz - z) + offset_both_sides + if z > midz + bounding_box_size / 2 - offset: + bounding_box_size = 2 * (z - midz) + offset_both_sides + + relative_centre_z = 0.5*data.bounding_box.rangez/bounding_box_size + camera_centre_z = -0.5 + relative_centre_z + camera = dict(eye=dict(x=-0.5/controls.zoom, y=-1/controls.zoom, z=-0.5+0.5/controls.zoom), + center=dict(x=0, y=0, z=camera_centre_z)) + fig.update_layout(template='plotly_dark', paper_bgcolor="black", scene_aspectmode='cube', + scene=dict(annotations=annotations, + xaxis=dict(backgroundcolor="black", nticks=10, + range=[data.bounding_box.midx-bounding_box_size/2, data.bounding_box.midx+bounding_box_size/2],), + yaxis=dict(backgroundcolor="black", nticks=10, + range=[data.bounding_box.midy-bounding_box_size/2, data.bounding_box.midy+bounding_box_size/2],), + zaxis=dict(backgroundcolor="black", nticks=10, range=[min(0, data.bounding_box.minz), bounding_box_size],), + ), scene_camera=camera, width=800, height=500, margin=dict(l=10, r=10, b=10, t=10, pad=4)) + if controls.hide_axes or controls.neat_for_publishing: + for axis in ['xaxis', 'yaxis', 'zaxis']: + fig.update_layout( + scene={axis: dict(showgrid=False, zeroline=False, visible=False)}) + if controls.neat_for_publishing: + fig.update_layout(width=500, height=500) + + # cicd_testing is a flag set by the CICD testing script (as a temporary environmental variable) to save the plot as a .png file + return fig + + +def transform(steps: list, result_type: str, controls: Union[GcodeControls, PlotControls] = None, show_tips: bool = True): + '''transform a fullcontrol design (a list of function class instances) into result_type + "gcode" or "plot". Optionally, GcodeControls or PlotControls can be passed to control + how the gcode or plot are generated. + ''' + + if result_type == 'gcode': + from fc_infaxis.steps2gcode import gcode + if controls is None: controls = GcodeControls() + return gcode(steps, controls) + + elif result_type == 'plot': + from fullcontrol.visualize.steps2visualization import visualize + if controls is None: controls = PlotControls() + return visualize(steps, controls, show_tips) + + elif result_type == 'fig': + from fullcontrol.visualize.steps2visualization import visualize + + if controls is None: controls = PlotControls() + plot_controls = controls + plot_controls.initialize() + + state = State(steps, plot_controls) + plot_data = PlotData(steps, state) + for step in steps: + step.visualize(state, plot_data, plot_controls) + plot_data.cleanup() + + return fig_plot(plot_data, plot_controls) \ No newline at end of file diff --git a/lab/fullcontrol/infaxis/controls.py b/lab/fullcontrol/infaxis/controls.py new file mode 100644 index 00000000..529787ce --- /dev/null +++ b/lab/fullcontrol/infaxis/controls.py @@ -0,0 +1,16 @@ +from typing import Optional +from pydantic import BaseModel +from fullcontrol import Point +from fullcontrol.combinations.gcode_and_visualize.classes import GcodeControls as BaseGcodeControls + + +class GcodeControls(BaseGcodeControls): + 'control to adjust the style and initialization of the gcode' + bed_center: Optional[Point] = Point(x=0, y=0, z=0) + head_chain: list = [] # Ordered list of axes in the head chain, used for the IK loop (back to front!) + bed_chain: list = [] # Ordered list of axes in the bed chain, used for the IK loop (back to front!) + xyz_orientation: Optional[list] = [1,1,1] # orientation of XYZ axes, 1 means the axis follows the right hand rule. + inverse_time_feedrate: Optional[bool] = False # if true, F command will be output as inverse time feedrate (e.g. F0.5 for 2 seconds per move) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. Note that when this is true, the print_speed and travel_speed attributes will be interpreted as seconds per move instead of mm/s. Also note that acceleration and deceleration will not be handled correctly when using inverse time feedrate, so it is recommended to use constant speed moves (G1 F...) when this is true. + distance_axis: Optional[bool] = False + model_XYZ_gcode: Optional[bool] = False # if true, the gcode output will have a UVW axis which shows the the XYZ movement in the model (Alternative to distance_axis). Intended to be used with the system axis (XYZAC for instance) all be treated as rotational in firmware and the UVW being linear aixs for motion planning. + verbose: Optional[bool] = False \ No newline at end of file diff --git a/lab/fullcontrol/infaxis/point.py b/lab/fullcontrol/infaxis/point.py new file mode 100644 index 00000000..e56b2c41 --- /dev/null +++ b/lab/fullcontrol/infaxis/point.py @@ -0,0 +1,170 @@ +from typing import Optional +from fullcontrol import Point as BasePoint +from copy import deepcopy +import numpy as np + +from infaxis.axis import Axis + +class Point(BasePoint): + axes: Optional[dict] = None # dictionary for assigning chages to the printer Axis based on the info in the point + + def infaxis_gcode(self, self_systemXYZ,state) -> float: + 'generate XYZABC gcode string to move from a point p to this point. return XYZABC string' + p = state.point_systemXYZ + s = '' + if self_systemXYZ.x != None and self_systemXYZ.x != p.x: + s += f'X{round(state.printer.xyz_orientation[0] * self_systemXYZ.x, 6):.6} ' + if self_systemXYZ.y != None and self_systemXYZ.y != p.y: + s += f'Y{round(state.printer.xyz_orientation[1] * self_systemXYZ.y, 6):.6} ' + if self_systemXYZ.z != None and self_systemXYZ.z != p.z: + s += f'Z{round(state.printer.xyz_orientation[2] * self_systemXYZ.z, 6):.6} ' + + # Currently this has no way of checking if movement occured in the chain axis, meaning they will always be included in the gcode output... + for axis in state.printer.head_chain + state.printer.bed_chain: + if axis.active != None: + # Not i fan of this float fix. The issue stems from the use of the axes dict in point since it doesnt force floats... but this is likely going to change anyway. + s += f'{axis.name}{round(float(axis.active), 12):.12} ' + + return s if s != '' else None + + def inverse_kinematics(self, state): + 'calcualte system XYZ for the current point XYZ (in part coordinates)' + def distance_forgiving(point1: Point, point2: Point) -> float: # copied from https://github.com/FullControlXYZ/fullcontrol/blob/master/fullcontrol/gcode/extrusion_classes.py + '''Calculate the distance between two points. x, y or z components are ignored unless defined in both points + + Args: + point1 (Point): The first point. + point2 (Point): The second point. + + Returns: + float: The distance between the two points. + ''' + dist_x = 0 if point1.x == None or point2.x == None else point1.x - point2.x + dist_y = 0 if point1.y == None or point2.y == None else point1.y - point2.y + dist_z = 0 if point1.z == None or point2.z == None else point1.z - point2.z + return ((dist_x)**2+(dist_y)**2+(dist_z)**2)**0.5 + + def model2system(model_point, state): + from math import cos, sin, tau + system_point = deepcopy(model_point) + + def chain_matrix(chain,start_point=Point(x=0,y=0,z=0)): + rev_chain = chain[::-1] + M = np.matrix([[start_point.x],[start_point.y],[start_point.z]]) + for axis in rev_chain: + Mo = np.matrix([[axis.offset.x],[axis.offset.y],[axis.offset.z]]) if axis.offset != None else np.matrix([[0],[0],[0]]) + M += Mo + if axis.type in ['X','Y','Z']: + i = 'XYZ'.index(axis.type) + M[i,0] += axis.active * axis.orientation + elif axis.type in ['A','B','C']: + angle_rad = axis.active * axis.orientation * tau / 360 + if axis.type == 'A': + Mrot = np.matrix([[1,0,0],[0,cos(angle_rad),-sin(angle_rad)],[0,sin(angle_rad),cos(angle_rad)]]) + elif axis.type == 'B': + Mrot = np.matrix([[cos(angle_rad),0,sin(angle_rad)],[0,1,0],[-sin(angle_rad),0,cos(angle_rad)]]) + elif axis.type == 'C': + Mrot = np.matrix([[cos(angle_rad),-sin(angle_rad),0],[sin(angle_rad),cos(angle_rad),0],[0,0,1]]) + M = np.matmul(Mrot, M) + return M + + head = state.printer.head_chain + bed = state.printer.bed_chain + + Mh = chain_matrix(head) + Mb = chain_matrix(bed,model_point) + + Msystem = Mb-Mh + x_system = Msystem.item(0) + state.printer.post_ik_offset.x + y_system = Msystem.item(1) + state.printer.post_ik_offset.y + z_system = Msystem.item(2) + state.printer.post_ik_offset.z + + system_point.x = round(x_system, 6) + system_point.y = round(y_system, 6) + system_point.z = round(z_system, 6) + + return system_point + + # make sure undefined attributes of the current point (self) are taken from the point in state + model_point = deepcopy(state.point) + model_point.update_from(self) + # Update Axis from point + for axis in state.printer.head_chain + state.printer.bed_chain: + if axis.name in self.axes and self.axes[axis.name] != None: + axis.active = self.axes[axis.name] + + # inverse kinematics: + system_point = model2system(model_point, state) + + #calculate distance from + dist = distance_forgiving(model_point,state.point) + dist_system = distance_forgiving(system_point,state.point_systemXYZ) + return system_point, dist, dist_system + + def gcode(self, state): + 'process this instance in a list of steps supplied by the designer to generate and return a line of gcode' + self_systemXYZ, dist, dist_system = self.inverse_kinematics(state) + infaxis_str = self.infaxis_gcode(self_systemXYZ,state) + if infaxis_str != None: # only write a line of gcode if movement occurs + G_str = 'G1 ' if state.extruder.on else 'G0 ' + E_str = state.extruder.e_gcode(self, state) + + if state.printer.inverse_time_feedrate and state.extruder.on: + if dist !=0: + f = 1 / (dist/state.printer.print_speed) + else: + f=60 # hardcoded value, move should take 1 second... dont know what that does though + #F_str = f'F{round(f, 0):.0f} ' + elif state.printer.distance_axis or state.printer.model_XYZ_gcode: #For printers with helper axis, but no inverse time feedrate + f = state.printer.print_speed + else: # If the printer has no helper features.... good luck + #F_str = state.printer.f_gcode(state) + f = state.printer.print_speed * (dist_system/dist) if (dist != 0 or dist_system != 0) else state.printer.print_speed # adjust feedrate based on the ratio of model distance to system distance to help keep print speed consistent. + F_str = f'F{round(f, 0):.0f} ' + + + + state.distance_accumulated += (dist**2-dist_system**2)**0.5 if dist - dist_system > 0 else 0 + if state.printer.model_XYZ_gcode: + infaxis_str = infaxis_str + f"U{round(self.x, 6):.6} V{round(self.y, 6):.6} W{round(self.z, 6):.6} " + elif state.printer.distance_axis: + infaxis_str = infaxis_str + f"U{state.distance_accumulated:.3f} " + gcode_str = f'{G_str}{F_str}{infaxis_str}{E_str}' + if state.printer.verbose: + gcode_str += f' ; distance: {dist:.3f}, system: {dist_system:.3f}' + state.printer.speed_changed = False + state.point.update_from(self) + state.point_systemXYZ.update_from(self_systemXYZ) + return gcode_str.strip() # strip the final space + + def visualize(self, state: 'State', plot_data: 'PlotData', plot_controls: 'PlotControls'): + ''' + Process a Point in a list of steps supplied by the designer to update plot_data and state. + + Args: + state ('State'): The current state of the plot. + plot_data ('PlotData'): The data used for plotting. + plot_controls ('PlotControls'): The controls for plotting. + + Returns: + None + ''' + + change_check = False + precision_xyz = 3 # number of decimal places to use for x y z values in plot_data + if self.x != None and self.x != state.point.x: + state.point.x = round(self.x, precision_xyz) + change_check = True + if self.y != None and self.y != state.point.y: + state.point.y = round(self.y, precision_xyz) + change_check = True + if self.z != None and self.z != state.point.z: + state.point.z = round(self.z, precision_xyz) + change_check = True + if self.color != None and self.color != state.point.color: + state.point.color = self.color + change_check = True + if change_check: + state.point.update_color(state, plot_data, plot_controls) + plot_data.paths[-1].add_point(state) + state.point_count_now += 1 \ No newline at end of file diff --git a/lab/fullcontrol/infaxis/printer.py b/lab/fullcontrol/infaxis/printer.py new file mode 100644 index 00000000..31fd2e76 --- /dev/null +++ b/lab/fullcontrol/infaxis/printer.py @@ -0,0 +1,32 @@ +from typing import Optional +from fullcontrol import Printer as BasePrinter +from fullcontrol import Point + + +class Printer(BasePrinter): + 'generic gcode Printer with 5-axis aspects added/modified' + bed_center: Point = None + post_ik_offset: Point = None + head_chain: list = None + bed_chain: list = None + xyz_orientation: list = None + inverse_time_feedrate: bool = None # if true, F command will be output as inverse time feedrate (e.g. F2 for 30 seconds per move (1/2 minutes per move)) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. + distance_axis: bool = None + model_XYZ_gcode: bool = None + verbose: bool = None + + def f_gcode(self, state): + if self.speed_changed == True: + return f'F{self.print_speed if state.extruder.on else self.travel_speed:.1f}'.rstrip('0').rstrip('.') + ' ' + else: + return '' + + def gcode(self, state): + 'process this instance in a list of steps supplied by the designer to generate and return a line of gcode' + # update all attributes of the tracking instance with the new instance (self) + state.printer.update_from(self) + if self.print_speed != None \ + or self.travel_speed != None: + state.printer.speed_changed = True + if self.new_command != None: + state.printer.command_list = {**state.printer.command_list, **self.new_command} \ No newline at end of file diff --git a/lab/fullcontrol/infaxis/state.py b/lab/fullcontrol/infaxis/state.py new file mode 100644 index 00000000..e513566a --- /dev/null +++ b/lab/fullcontrol/infaxis/state.py @@ -0,0 +1,101 @@ +from typing import Optional +from pydantic import BaseModel +from importlib import import_module + +from fullcontrol.gcode.extrusion_classes import ExtrusionGeometry, Extruder +from fullcontrol.gcode.controls import GcodeControls + +from infaxis.point import Point +from infaxis.printer import Printer +from infaxis.controls import GcodeControls + + +class State(BaseModel): + ''' this tracks the state of instances of interest adjusted in the list + of steps (points, extruder, etc.). some relevant shared variables and + initialisation methods are also included. a list of steps and + GcodeControls must be passed upon instantiation to allow initialization + of various attributes + ''' + + extruder: Optional[Extruder] = None + printer: Optional[Printer] = None + extrusion_geometry: Optional[ExtrusionGeometry] = None + steps: Optional[list] = None + point: Optional[Point] = Point() + point_systemXYZ: Optional[Point] = Point() + i: Optional[int] = 0 + gcode: Optional[list] = [] + distance_accumulated: Optional[float] = 0.0 + + def __init__(self, steps: list, gcode_controls: GcodeControls): + super().__init__() + # initialize state based on the named-printer default initialization_data and initialization_data over-rides passed by designer in gcode_controls + + def first_infaxis_point(steps: list, fully_defined: bool = True) -> Point: + 'return first Point in list. if the parameter fully_defined is true, return first Point with x,y,z' + if type(steps).__name__ == 'list': + for i in range(len(steps)): + if type(steps[i]).__name__ == 'Point': + if fully_defined: + if steps[i].x != None and steps[i].y != None and steps[i].z != None: + return steps[i] + else: + return steps[i] + if fully_defined: + raise Exception(f'No point found in steps with all five axis defined') + if not fully_defined: + raise Exception(f'No point found in steps') + + # the following line was edited from 3-axis gcode since 5-axis gcode is output in a simple form for now + initialization_data = import_module(f'fullcontrol.devices.community.singletool.generic').set_up(gcode_controls.initialization_data) + + self.extruder = Extruder( + units=initialization_data['e_units'], + dia_feed=initialization_data['dia_feed'], + total_volume=0, + total_volume_ref=0, + on=True) # on=True is different from 3-axis gcode since the primer has been disabled + self.extruder.update_e_ratio() + + # Calculate post_ik_offset from bed center and chains + + post_ik_offset = Point(x=gcode_controls.bed_center.x,y=gcode_controls.bed_center.y,z=gcode_controls.bed_center.z) + + for link in gcode_controls.head_chain: + post_ik_offset.x += link.offset.x + post_ik_offset.y += link.offset.y + post_ik_offset.z += link.offset.z + + for link in gcode_controls.bed_chain: + post_ik_offset.x += -link.offset.x + post_ik_offset.y += -link.offset.y + post_ik_offset.z += -link.offset.z + + self.printer = Printer( + command_list=initialization_data['printer_command_list'], + print_speed=initialization_data['print_speed'], + travel_speed=initialization_data['travel_speed'], + head_chain=gcode_controls.head_chain, + bed_chain=gcode_controls.bed_chain, + xyz_orientation=gcode_controls.xyz_orientation, + bed_center=gcode_controls.bed_center, + post_ik_offset=post_ik_offset, + inverse_time_feedrate=gcode_controls.inverse_time_feedrate, + distance_axis=gcode_controls.distance_axis, + model_XYZ_gcode=gcode_controls.model_XYZ_gcode, + verbose=gcode_controls.verbose, + speed_changed=True) + + self.extrusion_geometry = ExtrusionGeometry( + area_model=initialization_data['area_model'], + width=initialization_data['extrusion_width'], + height=initialization_data['extrusion_height']) + self.extrusion_geometry.update_area() + + # primer_steps = import_module(f'fullcontrol.gcode.primer_library.travel').primer(first_XYZBC_point(steps)) + primer_steps = [] + primer_steps.append(Extruder(on=False)) + primer_steps.append(first_infaxis_point(steps)) # move fast to start position + primer_steps.append(Extruder(on=True)) + self.steps = initialization_data['starting_procedure_steps'] + primer_steps + steps + initialization_data['ending_procedure_steps'] \ No newline at end of file diff --git a/lab/fullcontrol/infaxis/steps2gcode.py b/lab/fullcontrol/infaxis/steps2gcode.py new file mode 100644 index 00000000..b7f7e651 --- /dev/null +++ b/lab/fullcontrol/infaxis/steps2gcode.py @@ -0,0 +1,25 @@ + +import os +from datetime import datetime + +from infaxis.state import State +from infaxis.controls import GcodeControls + + +def gcode(steps: list, gcode_controls: GcodeControls = GcodeControls()): + 'return a gcode string generated from a list of steps' + state = State(steps, gcode_controls) + # need a while loop because some classes may change the length of state.steps + while state.i < len(state.steps): + # call the gcode function of each class instance in 'steps' + gcode_line = state.steps[state.i].gcode(state) + if gcode_line != None: + state.gcode.append(gcode_line) + state.i += 1 + gc = '\n'.join(state.gcode) + + if gcode_controls.save_as != None: + filename = gcode_controls.save_as + datetime.now().strftime("__%d-%m-%Y__%H-%M-%S.gcode") + open(filename, 'w').write(gc) + else: + return gc \ No newline at end of file diff --git a/tutorials/lab_infaxis_4_demo.ipynb b/tutorials/lab_infaxis_4_demo.ipynb new file mode 100644 index 00000000..1b575ecd --- /dev/null +++ b/tutorials/lab_infaxis_4_demo.ipynb @@ -0,0 +1,94 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9cad98ab", + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.infaxis as fci\n", + "import fullcontrol as fc\n", + "from math import sin, cos, tau\n", + "\n", + "fc.Point = fci.Point\n", + "fc.GcodeControls = fci.GcodeControls\n", + "fc.transform = fci.transform\n", + "fc.Axis = fci.Axis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ddc4461", + "metadata": {}, + "outputs": [], + "source": [ + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", + "gcode_controls = fc.GcodeControls(\n", + " head_chain = [],\n", + " bed_chain = [fc.Axis(name='C')],\n", + " initialization_data=print_settings \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "656e27c8", + "metadata": {}, + "outputs": [], + "source": [ + "r = 50 # radius of the spiral\n", + "density = 360 # number of points per revolution\n", + "layers = 60 # number of layers in the z direction for first segment\n", + "h = EH # height of each layer\n", + "z_start = h*0.5 # starting z height\n", + "\n", + "\n", + "steps = []\n", + "steps.append(fc.Printer(print_speed=2160))\n", + "for i in range(layers):\n", + " for j in range(density):\n", + " angle = 360*(i+j/density)\n", + " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", + "\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", + " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", + "\n", + "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "gcode = fc.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/lab_infaxis_5_demo.ipynb b/tutorials/lab_infaxis_5_demo.ipynb new file mode 100644 index 00000000..d1743815 --- /dev/null +++ b/tutorials/lab_infaxis_5_demo.ipynb @@ -0,0 +1,152 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "88f67711", + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.infaxis as fci\n", + "import fullcontrol as fc\n", + "from math import sin, cos, tau\n", + "\n", + "fc.Point = fci.Point\n", + "fc.GcodeControls = fci.GcodeControls\n", + "fc.transform = fci.transform\n", + "fc.Axis = fci.Axis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d54e7f4", + "metadata": {}, + "outputs": [], + "source": [ + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", + "gcode_controls = fc.GcodeControls(\n", + " head_chain = [fc.Axis(name='B')],\n", + " bed_chain = [fc.Axis(name='C')],\n", + " distance_axis = True,\n", + " verbose = True,\n", + " initialization_data=print_settings \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa20bc2b", + "metadata": {}, + "outputs": [], + "source": [ + "def vase_from_trace(trace_points,density):\n", + " steps = []\n", + " for i in range(len(trace_points)-1):\n", + " p = trace_points[i]\n", + " p_next = trace_points[i+1]\n", + " r = (p.x**2 + p.y**2)**0.5\n", + " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", + " dr = r_next - r\n", + " dz = p_next.z-p.z\n", + " ptilt = p.axes['B']\n", + " dtilt = p_next.axes['B']-ptilt\n", + " \n", + " dc = p_next.axes['C']-p.axes['C']\n", + " for j in range(density):\n", + " angle = p.axes['C'] + dc * j / density\n", + " tilt = ptilt + dtilt * j / density\n", + " r_final = r + dr * j / density\n", + " z_final = p.z + dz * j / density\n", + " \n", + " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", + " \n", + " return steps\n", + "\n", + "density = 360\n", + "r_start = 10\n", + "r_tilt = 5 # 10 layers\n", + "tilt_start = 0\n", + "tilt_end = 90\n", + "h = EH # height of each layer\n", + "z_start = h*0.5 # starting z height\n", + "layers = 20 # number of layers in the z direction for first segment\n", + "arc_layers = int((r_tilt * (tilt_end - tilt_start) / 360 *tau)/h)\n", + "d_tilted = 5\n", + "layers_tilted = int(d_tilted/h)\n", + "\n", + "trace = []\n", + "\n", + "for i in range(layers):\n", + " r = r_start\n", + " tilt = tilt_start\n", + " angle = i * 360\n", + " z = z_start + h * i\n", + "\n", + " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", + "\n", + "angle_offset = fc.last_point(trace).axes['C']\n", + "z_offset = fc.last_point(trace).z\n", + "\n", + "for i in range(1,arc_layers):\n", + " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", + " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", + " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", + " angle = i * 360 + angle_offset\n", + " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", + "\n", + "angle_offset = fc.last_point(trace).axes['C']\n", + "z_offset = fc.last_point(trace).z\n", + "r_offset = fc.last_point(trace).x\n", + "\n", + "for i in range(1,layers_tilted):\n", + " tilt = tilt_end\n", + " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", + " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", + " angle = i * 360 + angle_offset\n", + " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", + "\n", + "steps = vase_from_trace(trace, density)\n", + "steps.append(fc.last_point(trace))\n", + "\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from A=0 (blue) to A=90 (red)\n", + " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", + "\n", + "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", + "gcode = fc.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fiveaxis (3.12.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From a434a4339e93a06d87de28713e3f1812d107afb5 Mon Sep 17 00:00:00 2001 From: Anders Wolstrup Date: Fri, 12 Jun 2026 13:56:17 +0100 Subject: [PATCH 2/4] added colab tutorials --- bin/colab_tutorials.py | 10 +- .../colab/lab_infaxis_4_demo_colab.ipynb | 94 +++++++++++ .../colab/lab_infaxis_5_demo_colab.ipynb | 152 ++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 tutorials/colab/lab_infaxis_4_demo_colab.ipynb create mode 100644 tutorials/colab/lab_infaxis_5_demo_colab.ipynb diff --git a/bin/colab_tutorials.py b/bin/colab_tutorials.py index 5711bac8..70f4b52b 100644 --- a/bin/colab_tutorials.py +++ b/bin/colab_tutorials.py @@ -17,7 +17,9 @@ "lab_four_axis_demo.ipynb", "lab_five_axis_demo.ipynb", "lab_stl_output.ipynb", - "lab_3mf_output.ipynb"] + "lab_3mf_output.ipynb", + "lab_infaxis_4_demo.ipynb", + "lab_infaxis_5_demo.ipynb",] notebook_addresses = ["../tutorials/" + notebook_name for notebook_name in notebook_names] @@ -34,6 +36,8 @@ new_import_5ax = "if 'google.colab' in str(get_ipython()):\\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\\n" + old_import_5ax old_import_5ax2 = "import lab.fullcontrol.fiveaxisC0B1 as fc5" new_import_5ax2 = "if 'google.colab' in str(get_ipython()):\\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\\n" + old_import_5ax2 +old_import_infaxis = "import lab.fullcontrol.infaxis as fci" +new_import_infaxis = "if 'google.colab' in str(get_ipython()):\\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\\n" + old_import_infaxis string_to_delete = 'links will work in vscode, jupyter lab, etc. - the notebooks can also be accessed [online](https://github.com/FullControlXYZ/fullcontrol/tree/master/tutorials) and run in google colab' @@ -53,6 +57,10 @@ elif 'lab_five_axis_demo.ipynb' in notebook_address: content_string = content_string.replace(old_import_5ax, new_import_5ax) content_string = content_string.replace(old_import_5ax2, new_import_5ax2) + elif 'lab_infaxis_4_demo.ipynb' in notebook_address: + content_string = content_string.replace(old_import_infaxis, new_import_infaxis) + elif 'lab_infaxis_5_demo.ipynb' in notebook_address: + content_string = content_string.replace(old_import_infaxis, new_import_infaxis) else: content_string = content_string.replace(old_import, new_import) diff --git a/tutorials/colab/lab_infaxis_4_demo_colab.ipynb b/tutorials/colab/lab_infaxis_4_demo_colab.ipynb new file mode 100644 index 00000000..b0d319d3 --- /dev/null +++ b/tutorials/colab/lab_infaxis_4_demo_colab.ipynb @@ -0,0 +1,94 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9cad98ab", + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infaxis as fci\n", + "import fullcontrol as fc\n", + "from math import sin, cos, tau\n", + "\n", + "fc.Point = fci.Point\n", + "fc.GcodeControls = fci.GcodeControls\n", + "fc.transform = fci.transform\n", + "fc.Axis = fci.Axis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ddc4461", + "metadata": {}, + "outputs": [], + "source": [ + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", + "gcode_controls = fc.GcodeControls(\n", + " head_chain = [],\n", + " bed_chain = [fc.Axis(name='C')],\n", + " initialization_data=print_settings \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "656e27c8", + "metadata": {}, + "outputs": [], + "source": [ + "r = 50 # radius of the spiral\n", + "density = 360 # number of points per revolution\n", + "layers = 60 # number of layers in the z direction for first segment\n", + "h = EH # height of each layer\n", + "z_start = h*0.5 # starting z height\n", + "\n", + "\n", + "steps = []\n", + "steps.append(fc.Printer(print_speed=2160))\n", + "for i in range(layers):\n", + " for j in range(density):\n", + " angle = 360*(i+j/density)\n", + " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", + "\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", + " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", + "\n", + "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "gcode = fc.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/colab/lab_infaxis_5_demo_colab.ipynb b/tutorials/colab/lab_infaxis_5_demo_colab.ipynb new file mode 100644 index 00000000..25c8805e --- /dev/null +++ b/tutorials/colab/lab_infaxis_5_demo_colab.ipynb @@ -0,0 +1,152 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "88f67711", + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infaxis as fci\n", + "import fullcontrol as fc\n", + "from math import sin, cos, tau\n", + "\n", + "fc.Point = fci.Point\n", + "fc.GcodeControls = fci.GcodeControls\n", + "fc.transform = fci.transform\n", + "fc.Axis = fci.Axis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d54e7f4", + "metadata": {}, + "outputs": [], + "source": [ + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", + "gcode_controls = fc.GcodeControls(\n", + " head_chain = [fc.Axis(name='B')],\n", + " bed_chain = [fc.Axis(name='C')],\n", + " distance_axis = True,\n", + " verbose = True,\n", + " initialization_data=print_settings \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa20bc2b", + "metadata": {}, + "outputs": [], + "source": [ + "def vase_from_trace(trace_points,density):\n", + " steps = []\n", + " for i in range(len(trace_points)-1):\n", + " p = trace_points[i]\n", + " p_next = trace_points[i+1]\n", + " r = (p.x**2 + p.y**2)**0.5\n", + " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", + " dr = r_next - r\n", + " dz = p_next.z-p.z\n", + " ptilt = p.axes['B']\n", + " dtilt = p_next.axes['B']-ptilt\n", + " \n", + " dc = p_next.axes['C']-p.axes['C']\n", + " for j in range(density):\n", + " angle = p.axes['C'] + dc * j / density\n", + " tilt = ptilt + dtilt * j / density\n", + " r_final = r + dr * j / density\n", + " z_final = p.z + dz * j / density\n", + " \n", + " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", + " \n", + " return steps\n", + "\n", + "density = 360\n", + "r_start = 10\n", + "r_tilt = 5 # 10 layers\n", + "tilt_start = 0\n", + "tilt_end = 90\n", + "h = EH # height of each layer\n", + "z_start = h*0.5 # starting z height\n", + "layers = 20 # number of layers in the z direction for first segment\n", + "arc_layers = int((r_tilt * (tilt_end - tilt_start) / 360 *tau)/h)\n", + "d_tilted = 5\n", + "layers_tilted = int(d_tilted/h)\n", + "\n", + "trace = []\n", + "\n", + "for i in range(layers):\n", + " r = r_start\n", + " tilt = tilt_start\n", + " angle = i * 360\n", + " z = z_start + h * i\n", + "\n", + " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", + "\n", + "angle_offset = fc.last_point(trace).axes['C']\n", + "z_offset = fc.last_point(trace).z\n", + "\n", + "for i in range(1,arc_layers):\n", + " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", + " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", + " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", + " angle = i * 360 + angle_offset\n", + " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", + "\n", + "angle_offset = fc.last_point(trace).axes['C']\n", + "z_offset = fc.last_point(trace).z\n", + "r_offset = fc.last_point(trace).x\n", + "\n", + "for i in range(1,layers_tilted):\n", + " tilt = tilt_end\n", + " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", + " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", + " angle = i * 360 + angle_offset\n", + " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", + "\n", + "steps = vase_from_trace(trace, density)\n", + "steps.append(fc.last_point(trace))\n", + "\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from A=0 (blue) to A=90 (red)\n", + " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", + "\n", + "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", + "gcode = fc.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fiveaxis (3.12.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 729a0c8b3a0f9f654f1e794ba60121ac327cb6df Mon Sep 17 00:00:00 2001 From: andy g Date: Tue, 16 Jun 2026 18:24:57 +0100 Subject: [PATCH 3/4] Update infinaxis implementation and tutorials Implements the follow-up changes documented in lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md. Rename infaxis to infinaxis across package files, demos, tutorials, Colab notebooks, and the Colab generator. Align package exports with the root package flow and update demos to use configured Point helpers. Add xyz geometry helpers, controls/custom-axis examples, fixed-axis notes, and lowercase infinaxis tutorial navigation. --- .gitignore | 1 + bin/colab_tutorials.py | 17 +- lab/fullcontrol/fiveaxis.py | 7 + lab/fullcontrol/fiveaxisC0B1.py | 7 + lab/fullcontrol/fouraxis.py | 7 + lab/fullcontrol/infaxis/__init__.py | 7 - lab/fullcontrol/infinaxis/README.md | 7 + lab/fullcontrol/infinaxis/__init__.py | 1 + .../{infaxis/common.py => infinaxis/_plot.py} | 169 +-- .../{infaxis => infinaxis}/axis.py | 3 + lab/fullcontrol/infinaxis/common.py | 44 + .../{infaxis => infinaxis}/controls.py | 1 + .../infinaxis_changes_after_initial_pr.md | 1218 +++++++++++++++++ .../{infaxis => infinaxis}/point.py | 73 +- .../{infaxis => infinaxis}/printer.py | 1 + .../{infaxis => infinaxis}/state.py | 17 +- .../{infaxis => infinaxis}/steps2gcode.py | 6 +- lab/fullcontrol/infinaxis/xyz_add_axes.py | 23 + tutorials/README.md | 6 + tutorials/colab/contents_colab.ipynb | 7 + ...ipynb => infinaxis_4axis_demo_colab.ipynb} | 36 +- .../colab/infinaxis_5axis_demo_colab.ipynb | 206 +++ .../colab/infinaxis_controls_colab.ipynb | 372 +++++ .../colab/infinaxis_custom_axes_colab.ipynb | 73 + .../colab/infinaxis_xyz_geom_colab.ipynb | 79 ++ .../colab/lab_five_axis_demo_colab.ipynb | 687 +++++----- .../colab/lab_four_axis_demo_colab.ipynb | 521 +++---- .../colab/lab_infaxis_5_demo_colab.ipynb | 152 -- tutorials/contents.ipynb | 7 + ..._demo.ipynb => infinaxis_4axis_demo.ipynb} | 36 +- tutorials/infinaxis_5axis_demo.ipynb | 206 +++ tutorials/infinaxis_controls.ipynb | 372 +++++ tutorials/infinaxis_custom_axes.ipynb | 73 + tutorials/infinaxis_xyz_geom.ipynb | 79 ++ tutorials/lab_five_axis_demo.ipynb | 687 +++++----- tutorials/lab_four_axis_demo.ipynb | 521 +++---- tutorials/lab_infaxis_5_demo.ipynb | 152 -- 37 files changed, 4194 insertions(+), 1687 deletions(-) delete mode 100644 lab/fullcontrol/infaxis/__init__.py create mode 100644 lab/fullcontrol/infinaxis/README.md create mode 100644 lab/fullcontrol/infinaxis/__init__.py rename lab/fullcontrol/{infaxis/common.py => infinaxis/_plot.py} (57%) rename lab/fullcontrol/{infaxis => infinaxis}/axis.py (83%) create mode 100644 lab/fullcontrol/infinaxis/common.py rename lab/fullcontrol/{infaxis => infinaxis}/controls.py (92%) create mode 100644 lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md rename lab/fullcontrol/{infaxis => infinaxis}/point.py (72%) rename lab/fullcontrol/{infaxis => infinaxis}/printer.py (91%) rename lab/fullcontrol/{infaxis => infinaxis}/state.py (89%) rename lab/fullcontrol/{infaxis => infinaxis}/steps2gcode.py (85%) create mode 100644 lab/fullcontrol/infinaxis/xyz_add_axes.py rename tutorials/colab/{lab_infaxis_4_demo_colab.ipynb => infinaxis_4axis_demo_colab.ipynb} (66%) create mode 100644 tutorials/colab/infinaxis_5axis_demo_colab.ipynb create mode 100644 tutorials/colab/infinaxis_controls_colab.ipynb create mode 100644 tutorials/colab/infinaxis_custom_axes_colab.ipynb create mode 100644 tutorials/colab/infinaxis_xyz_geom_colab.ipynb delete mode 100644 tutorials/colab/lab_infaxis_5_demo_colab.ipynb rename tutorials/{lab_infaxis_4_demo.ipynb => infinaxis_4axis_demo.ipynb} (64%) create mode 100644 tutorials/infinaxis_5axis_demo.ipynb create mode 100644 tutorials/infinaxis_controls.ipynb create mode 100644 tutorials/infinaxis_custom_axes.ipynb create mode 100644 tutorials/infinaxis_xyz_geom.ipynb delete mode 100644 tutorials/lab_infaxis_5_demo.ipynb diff --git a/.gitignore b/.gitignore index ecf4d3cd..82f835dd 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,7 @@ venv/ ENV/ env.bak/ venv.bak/ +uv.lock # Spyder project settings .spyderproject diff --git a/bin/colab_tutorials.py b/bin/colab_tutorials.py index 70f4b52b..7e5c8952 100644 --- a/bin/colab_tutorials.py +++ b/bin/colab_tutorials.py @@ -18,8 +18,12 @@ "lab_five_axis_demo.ipynb", "lab_stl_output.ipynb", "lab_3mf_output.ipynb", - "lab_infaxis_4_demo.ipynb", - "lab_infaxis_5_demo.ipynb",] + "infinaxis_4axis_demo.ipynb", + "infinaxis_5axis_demo.ipynb", + "infinaxis_controls.ipynb", + "infinaxis_custom_axes.ipynb", + "infinaxis_xyz_geom.ipynb" + ] notebook_addresses = ["../tutorials/" + notebook_name for notebook_name in notebook_names] @@ -36,8 +40,8 @@ new_import_5ax = "if 'google.colab' in str(get_ipython()):\\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\\n" + old_import_5ax old_import_5ax2 = "import lab.fullcontrol.fiveaxisC0B1 as fc5" new_import_5ax2 = "if 'google.colab' in str(get_ipython()):\\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\\n" + old_import_5ax2 -old_import_infaxis = "import lab.fullcontrol.infaxis as fci" -new_import_infaxis = "if 'google.colab' in str(get_ipython()):\\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\\n" + old_import_infaxis +old_import_infinaxis = "import lab.fullcontrol.infinaxis as fci" +new_import_infinaxis = "if 'google.colab' in str(get_ipython()):\\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\\n" + old_import_infinaxis string_to_delete = 'links will work in vscode, jupyter lab, etc. - the notebooks can also be accessed [online](https://github.com/FullControlXYZ/fullcontrol/tree/master/tutorials) and run in google colab' @@ -57,12 +61,9 @@ elif 'lab_five_axis_demo.ipynb' in notebook_address: content_string = content_string.replace(old_import_5ax, new_import_5ax) content_string = content_string.replace(old_import_5ax2, new_import_5ax2) - elif 'lab_infaxis_4_demo.ipynb' in notebook_address: - content_string = content_string.replace(old_import_infaxis, new_import_infaxis) - elif 'lab_infaxis_5_demo.ipynb' in notebook_address: - content_string = content_string.replace(old_import_infaxis, new_import_infaxis) else: content_string = content_string.replace(old_import, new_import) + content_string = content_string.replace(old_import_infinaxis, new_import_infinaxis) if 'contents.ipynb' in notebook_address: content_string = content_string.replace(string_to_delete, '') diff --git a/lab/fullcontrol/fiveaxis.py b/lab/fullcontrol/fiveaxis.py index 1870c131..7c0146b5 100644 --- a/lab/fullcontrol/fiveaxis.py +++ b/lab/fullcontrol/fiveaxis.py @@ -1 +1,8 @@ +"""Fixed-axis wrapper retained for existing examples. + +New work may be better served by lab.fullcontrol.infinaxis, which is intended +to express equivalent dynamic axis setups. Exact gcode equivalence is still +being checked. +""" + from lab.fullcontrol.multiaxis.combinations.gcode_and_visualize.XYZBC.common import * diff --git a/lab/fullcontrol/fiveaxisC0B1.py b/lab/fullcontrol/fiveaxisC0B1.py index be1d7af3..723c18a7 100644 --- a/lab/fullcontrol/fiveaxisC0B1.py +++ b/lab/fullcontrol/fiveaxisC0B1.py @@ -1 +1,8 @@ +"""Fixed-axis wrapper retained for existing examples. + +New work may be better served by lab.fullcontrol.infinaxis, which is intended +to express equivalent dynamic axis setups. Exact gcode equivalence is still +being checked. +""" + from lab.fullcontrol.multiaxis.combinations.gcode_and_visualize.XYZC0B1.common import * diff --git a/lab/fullcontrol/fouraxis.py b/lab/fullcontrol/fouraxis.py index f093fcf7..b28476b4 100644 --- a/lab/fullcontrol/fouraxis.py +++ b/lab/fullcontrol/fouraxis.py @@ -1 +1,8 @@ +"""Fixed-axis wrapper retained for existing examples. + +New work may be better served by lab.fullcontrol.infinaxis, which is intended +to express equivalent dynamic axis setups. Exact gcode equivalence is still +being checked. +""" + from lab.fullcontrol.multiaxis.combinations.gcode_and_visualize.XYZB.common import * diff --git a/lab/fullcontrol/infaxis/__init__.py b/lab/fullcontrol/infaxis/__init__.py deleted file mode 100644 index 6a8a225e..00000000 --- a/lab/fullcontrol/infaxis/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .point import Point -from .controls import GcodeControls -from .printer import Printer -from .state import State -from .steps2gcode import gcode -from .common import transform -from .axis import Axis \ No newline at end of file diff --git a/lab/fullcontrol/infinaxis/README.md b/lab/fullcontrol/infinaxis/README.md new file mode 100644 index 00000000..5ce342d2 --- /dev/null +++ b/lab/fullcontrol/infinaxis/README.md @@ -0,0 +1,7 @@ +# Infinaxis Scope + +`infinaxis` is currently set up for systems with normal XYZ linear axes, plus rotational axes after those linear axes at the end of the kinematic chain. + +The inverse kinematics are calculated on that basis. This is useful because it explicitly works through nozzle offset from the rotation axes and part movement caused by rotation. The part-movement case is especially awkward: the effective offset depends on the exact current position within the part (distance from rotation axis), so it changes continuously as the toolpath moves. + +Tutorial notebooks demonstrate how infinaxis works, and can be found [here](https://github.com/FullControlXYZ/fullcontrol/tree/master/tutorials/README.md) \ No newline at end of file diff --git a/lab/fullcontrol/infinaxis/__init__.py b/lab/fullcontrol/infinaxis/__init__.py new file mode 100644 index 00000000..cf9226a3 --- /dev/null +++ b/lab/fullcontrol/infinaxis/__init__.py @@ -0,0 +1 @@ +from lab.fullcontrol.infinaxis.common import * diff --git a/lab/fullcontrol/infaxis/common.py b/lab/fullcontrol/infinaxis/_plot.py similarity index 57% rename from lab/fullcontrol/infaxis/common.py rename to lab/fullcontrol/infinaxis/_plot.py index 9af97cee..c36fcd78 100644 --- a/lab/fullcontrol/infaxis/common.py +++ b/lab/fullcontrol/infinaxis/_plot.py @@ -1,19 +1,9 @@ -from typing import Union - import numpy as np import plotly.graph_objects as go -from fullcontrol.visualize.tube_mesh import CylindersMesh, FlowTubeMesh, MeshExporter -from fullcontrol.visualize.controls import PlotControls -from fullcontrol.visualize.plot_data import PlotData -from fullcontrol.visualize.state import State + from fullcontrol.visualize.plotly import generate_mesh -# # see comment in __init__.py about why this module exists +from fullcontrol.visualize.tube_mesh import CylindersMesh, FlowTubeMesh -# # import functions and classes that will be accessible to the user -from fullcontrol.common import check -from fullcontrol.geometry import move, move_polar, travel_to # don't import all geometry functions since they are not designed for multiaxis Points -from fullcontrol.combinations.gcode_and_visualize.classes import * -import fullcontrol.geometry as xyz_geom def generate_single_path_vase_lod_surface_arrays( path, @@ -123,6 +113,7 @@ def generate_single_path_vase_lod_surface_arrays( return x_grid, y_grid, z_grid, color_grid + def generate_single_path_vase_lod_surface_trace( path, center_xy=None, @@ -133,13 +124,6 @@ def generate_single_path_vase_lod_surface_trace( showscale: bool = False, opacity: float = 1.0, ): - """ - Drop-in Plotly Surface trace for fast vase preview. - - Returns: - go.Surface or None - """ - x, y, z, surfacecolor = generate_single_path_vase_lod_surface_arrays( path, center_xy=center_xy, @@ -165,48 +149,26 @@ def generate_single_path_vase_lod_surface_trace( y=dict(show=False), z=dict(show=False), ), - # lighting=dict( - # ambient=0.65, - # diffuse=0.7, - # specular=0.05, - # roughness=1.0, - # ), showlegend=False, ) - -def fig_plot(data: PlotData, controls: PlotControls): - ''' - Plot data for x y z lines with RGB colors and annotations. - The style of the plot is governed by the controls. - - Args: - data (PlotData): The data to be plotted. - controls (PlotControls): The controls for customizing the plot. - - Returns: - None - ''' - +def fig_plot(data, controls): fig = go.Figure() if controls.tube_type is not None: - Mesh = {'flow': FlowTubeMesh, 'cylinders': CylindersMesh}[controls.tube_type] - else: # Fall back to FlowTubeMesh if no tube_type is explicitly specified + Mesh = {"flow": FlowTubeMesh, "cylinders": CylindersMesh}[controls.tube_type] + else: Mesh = FlowTubeMesh - # generate line plots max_width = 0 for path in data.paths: - colors_now = [f'rgb({color[0]*255:.2f}, {color[1]*255:.2f}, {color[2]*255:.2f})' for color in path.colors] - + colors_now = [f"rgb({color[0]*255:.2f}, {color[1]*255:.2f}, {color[2]*255:.2f})" for color in path.colors] linewidth_now = controls.line_width * 2 if path.extruder.on == True else controls.line_width * 0.5 - ## Generate mesh now imported from main fullcontrol visualization module, the only reason it was here was for the global local_max variable, I've just changed the plot function to derive a similar variable from the path width instead. if path.widths: - max_width = max(max_width, max(path.widths)) + max_width = max(max_width, max(path.widths)) if path.extruder.on and controls.style == "vase_surface": surface_trace = generate_single_path_vase_lod_surface_trace( @@ -222,15 +184,14 @@ def fig_plot(data: PlotData, controls: PlotControls): if surface_trace is not None: fig.add_trace(surface_trace) - - elif path.extruder.on and controls.style == 'tube': + elif path.extruder.on and controls.style == "tube": sides, rounding_strength, flat_sides = controls.tube_sides, 0.4, False mesh = generate_mesh(path, linewidth_now, Mesh, sides, rounding_strength, flat_sides, colors_now) fig.add_trace(mesh.to_Mesh3d(colors=colors_now)) elif not controls.hide_travel or path.extruder.on: fig.add_trace(go.Scatter3d( - mode='lines', + mode="lines", x=path.xvals, y=path.yvals, z=path.zvals, @@ -238,33 +199,32 @@ def fig_plot(data: PlotData, controls: PlotControls): line=dict(width=linewidth_now, color=colors_now), )) - - # find a bounding box, to create a plot with equally proportioned X Y Z scales (so a cuboid looks like a cuboid, not a cube) - bounding_box_size = max(data.bounding_box.maxx-data.bounding_box.minx, data.bounding_box.maxy - - data.bounding_box.miny, data.bounding_box.maxz-min(0, data.bounding_box.minz)) + bounding_box_size = max( + data.bounding_box.maxx - data.bounding_box.minx, + data.bounding_box.maxy - data.bounding_box.miny, + data.bounding_box.maxz - min(0, data.bounding_box.minz), + ) bounding_box_size += 0.002 bounding_box_size += max_width - # generate annotations annotations_pts = [] annotations = [] if controls.hide_annotations == False and not controls.neat_for_publishing: - # if controls.hide_annotations == False: # and not controls.neat_for_publishing: for annotation in data.annotations: - x, y, z = (annotation[axis] for axis in 'xyz') + x, y, z = (annotation[axis] for axis in "xyz") annotations_pts.append([x, y, z]) annotations.append(dict( showarrow=False, - x=x, y=y, z=z, - text=annotation['label'], - yshift=10)) - xs, ys, zs = zip(*annotations_pts) if annotations_pts else [[]]*3 - fig.add_trace(go.Scatter3d(mode='markers', x=xs, y=ys, z=zs, showlegend=False, marker=dict(size=2, color='red'))) - - # make sure the bounding box is big enough for the annotations - # the 0.001 is to make sure the annotations don't lie on the boundary - midx, midy, midz = (getattr(data.bounding_box, f'mid{axis}') for axis in 'xyz') - range + x=x, + y=y, + z=z, + text=annotation["label"], + yshift=10, + )) + xs, ys, zs = zip(*annotations_pts) if annotations_pts else [[]] * 3 + fig.add_trace(go.Scatter3d(mode="markers", x=xs, y=ys, z=zs, showlegend=False, marker=dict(size=2, color="red"))) + + midx, midy, midz = (getattr(data.bounding_box, f"mid{axis}") for axis in "xyz") offset = 0.001 offset_both_sides = 2 * offset for (x, y, z) in annotations_pts: @@ -281,56 +241,39 @@ def fig_plot(data: PlotData, controls: PlotControls): if z > midz + bounding_box_size / 2 - offset: bounding_box_size = 2 * (z - midz) + offset_both_sides - relative_centre_z = 0.5*data.bounding_box.rangez/bounding_box_size + relative_centre_z = 0.5 * data.bounding_box.rangez / bounding_box_size camera_centre_z = -0.5 + relative_centre_z - camera = dict(eye=dict(x=-0.5/controls.zoom, y=-1/controls.zoom, z=-0.5+0.5/controls.zoom), - center=dict(x=0, y=0, z=camera_centre_z)) - fig.update_layout(template='plotly_dark', paper_bgcolor="black", scene_aspectmode='cube', - scene=dict(annotations=annotations, - xaxis=dict(backgroundcolor="black", nticks=10, - range=[data.bounding_box.midx-bounding_box_size/2, data.bounding_box.midx+bounding_box_size/2],), - yaxis=dict(backgroundcolor="black", nticks=10, - range=[data.bounding_box.midy-bounding_box_size/2, data.bounding_box.midy+bounding_box_size/2],), - zaxis=dict(backgroundcolor="black", nticks=10, range=[min(0, data.bounding_box.minz), bounding_box_size],), - ), scene_camera=camera, width=800, height=500, margin=dict(l=10, r=10, b=10, t=10, pad=4)) + camera = dict( + eye=dict(x=-0.5 / controls.zoom, y=-1 / controls.zoom, z=-0.5 + 0.5 / controls.zoom), + center=dict(x=0, y=0, z=camera_centre_z), + ) + fig.update_layout( + template="plotly_dark", + paper_bgcolor="black", + scene_aspectmode="cube", + scene=dict( + annotations=annotations, + xaxis=dict( + backgroundcolor="black", + nticks=10, + range=[data.bounding_box.midx - bounding_box_size / 2, data.bounding_box.midx + bounding_box_size / 2], + ), + yaxis=dict( + backgroundcolor="black", + nticks=10, + range=[data.bounding_box.midy - bounding_box_size / 2, data.bounding_box.midy + bounding_box_size / 2], + ), + zaxis=dict(backgroundcolor="black", nticks=10, range=[min(0, data.bounding_box.minz), bounding_box_size]), + ), + scene_camera=camera, + width=800, + height=500, + margin=dict(l=10, r=10, b=10, t=10, pad=4), + ) if controls.hide_axes or controls.neat_for_publishing: - for axis in ['xaxis', 'yaxis', 'zaxis']: - fig.update_layout( - scene={axis: dict(showgrid=False, zeroline=False, visible=False)}) + for axis in ["xaxis", "yaxis", "zaxis"]: + fig.update_layout(scene={axis: dict(showgrid=False, zeroline=False, visible=False)}) if controls.neat_for_publishing: fig.update_layout(width=500, height=500) - # cicd_testing is a flag set by the CICD testing script (as a temporary environmental variable) to save the plot as a .png file return fig - - -def transform(steps: list, result_type: str, controls: Union[GcodeControls, PlotControls] = None, show_tips: bool = True): - '''transform a fullcontrol design (a list of function class instances) into result_type - "gcode" or "plot". Optionally, GcodeControls or PlotControls can be passed to control - how the gcode or plot are generated. - ''' - - if result_type == 'gcode': - from fc_infaxis.steps2gcode import gcode - if controls is None: controls = GcodeControls() - return gcode(steps, controls) - - elif result_type == 'plot': - from fullcontrol.visualize.steps2visualization import visualize - if controls is None: controls = PlotControls() - return visualize(steps, controls, show_tips) - - elif result_type == 'fig': - from fullcontrol.visualize.steps2visualization import visualize - - if controls is None: controls = PlotControls() - plot_controls = controls - plot_controls.initialize() - - state = State(steps, plot_controls) - plot_data = PlotData(steps, state) - for step in steps: - step.visualize(state, plot_data, plot_controls) - plot_data.cleanup() - - return fig_plot(plot_data, plot_controls) \ No newline at end of file diff --git a/lab/fullcontrol/infaxis/axis.py b/lab/fullcontrol/infinaxis/axis.py similarity index 83% rename from lab/fullcontrol/infaxis/axis.py rename to lab/fullcontrol/infinaxis/axis.py index b9291ea0..7cfdb95f 100644 --- a/lab/fullcontrol/infaxis/axis.py +++ b/lab/fullcontrol/infinaxis/axis.py @@ -12,6 +12,9 @@ class Axis(BaseModel): def __init__(self, **data): super().__init__(**data) + if self.name is not None and self.name.upper() in ["X", "Y", "Z"]: + raise ValueError('Axis.name cannot be "X", "Y", or "Z"; use Axis.type for linear kinematics with a different name') + if self.type is None: if self.name is not None and self.name in ["A", "B", "C"]: self.type = self.name diff --git a/lab/fullcontrol/infinaxis/common.py b/lab/fullcontrol/infinaxis/common.py new file mode 100644 index 00000000..a11780c8 --- /dev/null +++ b/lab/fullcontrol/infinaxis/common.py @@ -0,0 +1,44 @@ +from typing import Union + +from fullcontrol import * +import fullcontrol.geometry as xyz_geom +# base fc namespace for user access; infinaxis replacements override matching names +from lab.fullcontrol.infinaxis.axis import Axis +from lab.fullcontrol.infinaxis.controls import GcodeControls +from lab.fullcontrol.infinaxis.point import Point, configure_point +from lab.fullcontrol.infinaxis.printer import Printer +from lab.fullcontrol.infinaxis.steps2gcode import gcode +from lab.fullcontrol.infinaxis.xyz_add_axes import xyz_add_axes + + +def transform(steps: list, result_type: str, controls: Union[GcodeControls, PlotControls] = None, show_tips: bool = True): + '''transform a fullcontrol design (a list of function class instances) into result_type + "gcode" or "plot". Optionally, GcodeControls or PlotControls can be passed to control + how the gcode or plot are generated. + ''' + + if result_type == 'gcode': + if controls is None: controls = GcodeControls() + return gcode(steps, controls) + + elif result_type == 'plot': + from fullcontrol.visualize.steps2visualization import visualize + if controls is None: controls = PlotControls() + return visualize(steps, controls, show_tips) + + elif result_type == 'fig': + from fullcontrol.visualize.plot_data import PlotData + from fullcontrol.visualize.state import State as VisualizeState + from lab.fullcontrol.infinaxis._plot import fig_plot + + if controls is None: controls = PlotControls() + plot_controls = controls + plot_controls.initialize() + + state = VisualizeState(steps, plot_controls) + plot_data = PlotData(steps, state) + for step in steps: + step.visualize(state, plot_data, plot_controls) + plot_data.cleanup() + + return fig_plot(plot_data, plot_controls) diff --git a/lab/fullcontrol/infaxis/controls.py b/lab/fullcontrol/infinaxis/controls.py similarity index 92% rename from lab/fullcontrol/infaxis/controls.py rename to lab/fullcontrol/infinaxis/controls.py index 529787ce..2cee587a 100644 --- a/lab/fullcontrol/infaxis/controls.py +++ b/lab/fullcontrol/infinaxis/controls.py @@ -11,6 +11,7 @@ class GcodeControls(BaseGcodeControls): bed_chain: list = [] # Ordered list of axes in the bed chain, used for the IK loop (back to front!) xyz_orientation: Optional[list] = [1,1,1] # orientation of XYZ axes, 1 means the axis follows the right hand rule. inverse_time_feedrate: Optional[bool] = False # if true, F command will be output as inverse time feedrate (e.g. F0.5 for 2 seconds per move) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. Note that when this is true, the print_speed and travel_speed attributes will be interpreted as seconds per move instead of mm/s. Also note that acceleration and deceleration will not be handled correctly when using inverse time feedrate, so it is recommended to use constant speed moves (G1 F...) when this is true. + # the following two parameters (model_XYZ_gcode and distance_axis) may be useful for give more information for motion planning distance_axis: Optional[bool] = False model_XYZ_gcode: Optional[bool] = False # if true, the gcode output will have a UVW axis which shows the the XYZ movement in the model (Alternative to distance_axis). Intended to be used with the system axis (XYZAC for instance) all be treated as rotational in firmware and the UVW being linear aixs for motion planning. verbose: Optional[bool] = False \ No newline at end of file diff --git a/lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md b/lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md new file mode 100644 index 00000000..95598fdf --- /dev/null +++ b/lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md @@ -0,0 +1,1218 @@ +# Changes implemented to first pr submission + +- Renamed `infaxis` to `infinaxis` across package files, demos, tutorials, Colab notebooks, and the Colab generator. +- `lab.fullcontrol.infinaxis` now follows the root package flow: `__init__.py` imports from `common.py`. +- `common.py` imports the base `fullcontrol` namespace for normal user access, then overrides the infinaxis-specific public names: `Point`, `Printer`, `GcodeControls`, `Axis`, `gcode`, and `transform`. +- Demos/tutorials now use only `import lab.fullcontrol.infinaxis as fci`; no `fullcontrol` monkey-patching or extra `import fullcontrol as fc`. +- Helper-only plot functions moved out of public `common.py` into private `_plot.py`. +- Internal package imports now point at `lab.fullcontrol.infinaxis...`. +- `Point.axes` remains optional, and inverse kinematics only reads it when it is present, so `fci.Point(x=10)` works naturally. +- Added `fci.configure_point(head_chain, bed_chain)` to return a configured `Point` class. + - It keeps `axes={...}` as the internal storage model, but allows dot-style axis input/access like `Point(x=10, b=20, c=30)` or `Point(X=10, B=20, C=30)`. + - Axis matching is case-insensitive for user input, while stored axis keys preserve the configured `Axis.name` for gcode output. + - Gcode state now recognizes configured `Point` subclasses. +- Updated the two infinaxis demos, tutorial notebooks, and Colab copies to use `Point = fci.configure_point(head_chain, bed_chain)` with dot-style axis values instead of `axes={...}`. +- Added `fci.xyz_geom` and `fci.xyz_add_axes(...)`, matching the existing multiaxis pattern for using normal xyz geometry functions and converting their Points to infinaxis Points. +- Clarified optional helper gcode outputs: `distance_axis` can emit accumulated-distance `U`, while `model_XYZ_gcode` can emit model-space `U/V/W` for motion-planning support. +- Updated the root 5-axis demo to show gcode output with `verbose=False` and `verbose=True`. +- `Axis(name="X")`, `Axis(name="Y")`, and `Axis(name="Z")` now raise an exception; linear helper axes should use another name with `type="X"`, `type="Y"`, or `type="Z"`. +- Added a root custom-axis-names demo showing `CI` / `CII` with shared `type="C"` kinematics. +- Added a root controls demo showing `Axis.name/type/active/orientation/offset` plus `GcodeControls.verbose`, `distance_axis`, `model_XYZ_gcode`, `inverse_time_feedrate`, and `xyz_orientation`; uncertain descriptions are marked with `CHECK_THIS_DESCRIPTION`. +- Added cautious fixed-axis notes to `lab.fullcontrol.fouraxis`, `lab.fullcontrol.fiveaxis`, `lab.fullcontrol.fiveaxisC0B1`, and the four/five-axis tutorial and Colab notebooks. + + +Due to rename from infaxis to infinaxis, git tracking is not that valuable (lots of untracked/deleted files rather than comparisions of modification). A python script was written to do a comparison and results of that script are copied below. + +### python script: + +``` python +from __future__ import annotations + +import difflib +import json +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +OUTPUT = ROOT / "infinaxis_rename_comparison.md" + +PAIRS = [ + ("lab/fullcontrol/infaxis/__init__.py", "lab/fullcontrol/infinaxis/__init__.py"), + ("lab/fullcontrol/infaxis/axis.py", "lab/fullcontrol/infinaxis/axis.py"), + ("lab/fullcontrol/infaxis/common.py", "lab/fullcontrol/infinaxis/common.py"), + ("lab/fullcontrol/infaxis/controls.py", "lab/fullcontrol/infinaxis/controls.py"), + ("lab/fullcontrol/infaxis/point.py", "lab/fullcontrol/infinaxis/point.py"), + ("lab/fullcontrol/infaxis/printer.py", "lab/fullcontrol/infinaxis/printer.py"), + ("lab/fullcontrol/infaxis/state.py", "lab/fullcontrol/infinaxis/state.py"), + ("lab/fullcontrol/infaxis/steps2gcode.py", "lab/fullcontrol/infinaxis/steps2gcode.py"), + ("tutorials/lab_infaxis_4_demo.ipynb", "tutorials/lab_infinaxis_4_demo.ipynb"), + ("tutorials/lab_infaxis_5_demo.ipynb", "tutorials/lab_infinaxis_5_demo.ipynb"), + ("tutorials/colab/lab_infaxis_4_demo_colab.ipynb", "tutorials/colab/lab_infinaxis_4_demo_colab.ipynb"), + ("tutorials/colab/lab_infaxis_5_demo_colab.ipynb", "tutorials/colab/lab_infinaxis_5_demo_colab.ipynb"), +] + +NEW_ONLY = [ + "lab/fullcontrol/infinaxis/_plot.py", + "lab/fullcontrol/infinaxis/xyz_add_axes.py", +] + + +def git_show(path: str) -> str: + return subprocess.check_output(["git", "show", f"HEAD:{path}"], cwd=ROOT, text=True) + + +def normalize_text(text: str) -> str: + return ( + text.replace("infaxis", "infinaxis") + .replace("Infaxis", "Infinaxis") + .replace("INFAXIS", "INFINAXIS") + .replace("fc_infinaxis", "lab.fullcontrol.infinaxis") + ) + + +def normalize_notebook(text: str) -> str: + notebook = json.loads(text) + for cell in notebook.get("cells", []): + if cell.get("cell_type") == "code": + cell["execution_count"] = None + cell["outputs"] = [] + return json.dumps(notebook, indent=1, sort_keys=True) + "\n" + + +def normalized(path: str, text: str) -> list[str]: + text = normalize_text(text) + if path.endswith(".ipynb"): + text = normalize_notebook(text) + return text.splitlines(keepends=True) + + +def main() -> None: + sections: list[str] = [ + "# Infinaxis Rename Comparison\n\n", + "This file compares old tracked `infaxis` files from `HEAD` with the current working-tree `infinaxis` files.\n\n", + "Normalization applied before diffing:\n\n", + "- `infaxis`/`Infaxis`/`INFAXIS` text is treated as `infinaxis`/`Infinaxis`/`INFINAXIS`.\n", + "- notebook outputs and execution counts are cleared.\n", + "- notebook JSON is formatted consistently.\n\n", + "Use this as a temporary commit-description aid; it can be deleted after the rename/edit commit is prepared.\n\n", + ] + + changed = 0 + unchanged = 0 + missing = 0 + + for old_path, new_path in PAIRS: + sections.append(f"## `{old_path}` -> `{new_path}`\n\n") + new_file = ROOT / new_path + if not new_file.exists(): + sections.append(f"Missing new file: `{new_path}`\n\n") + missing += 1 + continue + + old_lines = normalized(old_path, git_show(old_path)) + new_lines = normalized(new_path, new_file.read_text()) + diff = list( + difflib.unified_diff( + old_lines, + new_lines, + fromfile=old_path.replace("infaxis", "infinaxis"), + tofile=new_path, + ) + ) + if diff: + changed += 1 + sections.append("```diff\n") + sections.extend(diff) + sections.append("```\n\n") + else: + unchanged += 1 + sections.append("No content changes after rename normalization.\n\n") + + if NEW_ONLY: + sections.append("## New files without old `infaxis` equivalent\n\n") + for path in NEW_ONLY: + exists = (ROOT / path).exists() + status = "present" if exists else "missing" + sections.append(f"- `{path}`: {status}\n") + sections.append("\n") + + summary = ( + "## Summary\n\n" + f"- compared pairs: {len(PAIRS)}\n" + f"- changed after normalization: {changed}\n" + f"- unchanged after normalization: {unchanged}\n" + f"- missing new files: {missing}\n\n" + ) + sections.insert(1, summary) + OUTPUT.write_text("".join(sections)) + print(OUTPUT) + + +if __name__ == "__main__": + main() +``` + +### python script output: + + +_____ +_____ +_____ + +### NOTE THIS WAS RUN BEFORE RENAMING SOME OF THE TUTORIALS AND CREATING THE README AT lab/fullcontrol/infinaxis/README.md + +# Infinaxis Rename Comparison + +## Summary + +- compared pairs: 12 +- changed after normalization: 12 +- unchanged after normalization: 0 +- missing new files: 0 + +This file compares old tracked `infaxis` files from `HEAD` with the current working-tree `infinaxis` files. + +Normalization applied before diffing: + +- `infaxis`/`Infaxis`/`INFAXIS` text is treated as `infinaxis`/`Infinaxis`/`INFINAXIS`. +- notebook outputs and execution counts are cleared. +- notebook JSON is formatted consistently. + +Use this as a temporary commit-description aid; it can be deleted after the rename/edit commit is prepared. + +## `lab/fullcontrol/infaxis/__init__.py` -> `lab/fullcontrol/infinaxis/__init__.py` + +```diff +--- lab/fullcontrol/infinaxis/__init__.py ++++ lab/fullcontrol/infinaxis/__init__.py +@@ -1,7 +1 @@ +-from .point import Point +-from .controls import GcodeControls +-from .printer import Printer +-from .state import State +-from .steps2gcode import gcode +-from .common import transform +-from .axis import Axis+from lab.fullcontrol.infinaxis.common import * +``` + +## `lab/fullcontrol/infaxis/axis.py` -> `lab/fullcontrol/infinaxis/axis.py` + +```diff +--- lab/fullcontrol/infinaxis/axis.py ++++ lab/fullcontrol/infinaxis/axis.py +@@ -12,6 +12,9 @@ + def __init__(self, **data): + super().__init__(**data) + ++ if self.name is not None and self.name.upper() in ["X", "Y", "Z"]: ++ raise ValueError('Axis.name cannot be "X", "Y", or "Z"; use Axis.type for linear kinematics with a different name') ++ + if self.type is None: + if self.name is not None and self.name in ["A", "B", "C"]: + self.type = self.name +``` + +## `lab/fullcontrol/infaxis/common.py` -> `lab/fullcontrol/infinaxis/common.py` + +```diff +--- lab/fullcontrol/infinaxis/common.py ++++ lab/fullcontrol/infinaxis/common.py +@@ -1,307 +1,14 @@ + from typing import Union + +-import numpy as np +-import plotly.graph_objects as go +-from fullcontrol.visualize.tube_mesh import CylindersMesh, FlowTubeMesh, MeshExporter +-from fullcontrol.visualize.controls import PlotControls +-from fullcontrol.visualize.plot_data import PlotData +-from fullcontrol.visualize.state import State +-from fullcontrol.visualize.plotly import generate_mesh +-# # see comment in __init__.py about why this module exists +- +-# # import functions and classes that will be accessible to the user +-from fullcontrol.common import check +-from fullcontrol.geometry import move, move_polar, travel_to # don't import all geometry functions since they are not designed for multiaxis Points +-from fullcontrol.combinations.gcode_and_visualize.classes import * ++from fullcontrol import * + import fullcontrol.geometry as xyz_geom +- +-def generate_single_path_vase_lod_surface_arrays( +- path, +- center_xy=None, +- points_per_turn: int = 128, +- turn_stride: int = 2, +- color_mode: str = "none", +-): +- points = np.asarray([path.xvals, path.yvals, path.zvals], dtype=np.float32).T +- +- if len(points) < 4: +- return None, None, None, None +- +- good = np.ones(len(points), dtype=bool) +- dups = np.all(np.diff(points, axis=0) == 0, axis=1) +- good[1:] = ~dups +- points = points[good] +- +- if len(points) < 4: +- return None, None, None, None +- +- source_color_scalar = None +- +- if color_mode == "path": +- if getattr(path, "colors", None) is not None and len(path.colors) > 0: +- colors_arr = np.asarray(path.colors, dtype=np.float32) +- +- if len(colors_arr) == len(good): +- colors_arr = colors_arr[good] +- +- if len(colors_arr) == len(points): +- source_color_scalar = ( +- 0.299 * colors_arr[:, 0] +- + 0.587 * colors_arr[:, 1] +- + 0.114 * colors_arr[:, 2] +- ).astype(np.float32) +- +- if center_xy is None: +- cx = np.float32(0.5 * (np.min(points[:, 0]) + np.max(points[:, 0]))) +- cy = np.float32(0.5 * (np.min(points[:, 1]) + np.max(points[:, 1]))) +- else: +- cx, cy = center_xy +- +- theta = np.unwrap(np.arctan2(points[:, 1] - cy, points[:, 0] - cx)).astype(np.float32) +- +- if theta[-1] < theta[0]: +- theta = -theta +- +- turn_coord = ((theta - theta[0]) / np.float32(2.0 * np.pi)).astype(np.float32) +- +- monotonic_good = np.concatenate([[True], np.diff(turn_coord) > 1e-10]) +- turn_coord = turn_coord[monotonic_good] +- points = points[monotonic_good] +- +- if source_color_scalar is not None: +- source_color_scalar = source_color_scalar[monotonic_good] +- +- if len(points) < 4: +- return None, None, None, None +- +- first_complete_turn = int(np.ceil(turn_coord[0])) +- last_complete_turn = int(np.floor(turn_coord[-1])) - 1 +- +- if last_complete_turn <= first_complete_turn: +- return None, None, None, None +- +- all_turns = np.arange(first_complete_turn, last_complete_turn + 1, dtype=np.float32) +- +- selected_turns = all_turns[::max(1, int(turn_stride))] +- +- if selected_turns[-1] != all_turns[-1]: +- selected_turns = np.append(selected_turns, all_turns[-1]).astype(np.float32) +- +- if len(selected_turns) < 2: +- return None, None, None, None +- +- phases = np.linspace( +- 0.0, +- 1.0, +- points_per_turn + 1, +- endpoint=True, +- dtype=np.float32, +- ) +- +- targets = selected_turns[:, None] + phases[None, :] +- targets_flat = targets.ravel() +- +- x_grid = np.interp(targets_flat, turn_coord, points[:, 0]).reshape(targets.shape).astype(np.float32) +- y_grid = np.interp(targets_flat, turn_coord, points[:, 1]).reshape(targets.shape).astype(np.float32) +- z_grid = np.interp(targets_flat, turn_coord, points[:, 2]).reshape(targets.shape).astype(np.float32) +- +- if color_mode == "path" and source_color_scalar is not None: +- color_grid = np.interp( +- targets_flat, +- turn_coord, +- source_color_scalar, +- ).reshape(targets.shape).astype(np.float32) +- +- elif color_mode == "height": +- color_grid = z_grid +- +- elif color_mode == "turn": +- color_grid = targets.astype(np.float32) +- +- else: +- color_grid = np.zeros_like(z_grid, dtype=np.float32) +- +- return x_grid, y_grid, z_grid, color_grid +- +-def generate_single_path_vase_lod_surface_trace( +- path, +- center_xy=None, +- points_per_turn: int = 128, +- turn_stride: int = 2, +- color_mode: str = "none", +- colorscale="Viridis", +- showscale: bool = False, +- opacity: float = 1.0, +-): +- """ +- Drop-in Plotly Surface trace for fast vase preview. +- +- Returns: +- go.Surface or None +- """ +- +- x, y, z, surfacecolor = generate_single_path_vase_lod_surface_arrays( +- path, +- center_xy=center_xy, +- points_per_turn=points_per_turn, +- turn_stride=turn_stride, +- color_mode=color_mode, +- ) +- +- if x is None: +- return None +- +- return go.Surface( +- x=x, +- y=y, +- z=z, +- surfacecolor=surfacecolor, +- colorscale=colorscale, +- showscale=showscale, +- opacity=opacity, +- hoverinfo="skip", +- contours=dict( +- x=dict(show=False), +- y=dict(show=False), +- z=dict(show=False), +- ), +- # lighting=dict( +- # ambient=0.65, +- # diffuse=0.7, +- # specular=0.05, +- # roughness=1.0, +- # ), +- showlegend=False, +- ) +- +- +- +-def fig_plot(data: PlotData, controls: PlotControls): +- ''' +- Plot data for x y z lines with RGB colors and annotations. +- The style of the plot is governed by the controls. +- +- Args: +- data (PlotData): The data to be plotted. +- controls (PlotControls): The controls for customizing the plot. +- +- Returns: +- None +- ''' +- +- fig = go.Figure() +- +- if controls.tube_type is not None: +- Mesh = {'flow': FlowTubeMesh, 'cylinders': CylindersMesh}[controls.tube_type] +- else: # Fall back to FlowTubeMesh if no tube_type is explicitly specified +- Mesh = FlowTubeMesh +- +- # generate line plots +- max_width = 0 +- +- for path in data.paths: +- colors_now = [f'rgb({color[0]*255:.2f}, {color[1]*255:.2f}, {color[2]*255:.2f})' for color in path.colors] +- +- linewidth_now = controls.line_width * 2 if path.extruder.on == True else controls.line_width * 0.5 +- +- ## Generate mesh now imported from main fullcontrol visualization module, the only reason it was here was for the global local_max variable, I've just changed the plot function to derive a similar variable from the path width instead. +- if path.widths: +- max_width = max(max_width, max(path.widths)) +- +- if path.extruder.on and controls.style == "vase_surface": +- surface_trace = generate_single_path_vase_lod_surface_trace( +- path, +- points_per_turn=96, +- turn_stride=3, +- color_mode="height", +- colorscale="viridis", +- showscale=False, +- opacity=1.0, +- ) +- +- if surface_trace is not None: +- fig.add_trace(surface_trace) +- +- +- elif path.extruder.on and controls.style == 'tube': +- sides, rounding_strength, flat_sides = controls.tube_sides, 0.4, False +- mesh = generate_mesh(path, linewidth_now, Mesh, sides, rounding_strength, flat_sides, colors_now) +- fig.add_trace(mesh.to_Mesh3d(colors=colors_now)) +- +- elif not controls.hide_travel or path.extruder.on: +- fig.add_trace(go.Scatter3d( +- mode='lines', +- x=path.xvals, +- y=path.yvals, +- z=path.zvals, +- showlegend=False, +- line=dict(width=linewidth_now, color=colors_now), +- )) +- +- +- # find a bounding box, to create a plot with equally proportioned X Y Z scales (so a cuboid looks like a cuboid, not a cube) +- bounding_box_size = max(data.bounding_box.maxx-data.bounding_box.minx, data.bounding_box.maxy - +- data.bounding_box.miny, data.bounding_box.maxz-min(0, data.bounding_box.minz)) +- bounding_box_size += 0.002 +- bounding_box_size += max_width +- +- # generate annotations +- annotations_pts = [] +- annotations = [] +- if controls.hide_annotations == False and not controls.neat_for_publishing: +- # if controls.hide_annotations == False: # and not controls.neat_for_publishing: +- for annotation in data.annotations: +- x, y, z = (annotation[axis] for axis in 'xyz') +- annotations_pts.append([x, y, z]) +- annotations.append(dict( +- showarrow=False, +- x=x, y=y, z=z, +- text=annotation['label'], +- yshift=10)) +- xs, ys, zs = zip(*annotations_pts) if annotations_pts else [[]]*3 +- fig.add_trace(go.Scatter3d(mode='markers', x=xs, y=ys, z=zs, showlegend=False, marker=dict(size=2, color='red'))) +- +- # make sure the bounding box is big enough for the annotations +- # the 0.001 is to make sure the annotations don't lie on the boundary +- midx, midy, midz = (getattr(data.bounding_box, f'mid{axis}') for axis in 'xyz') +- range +- offset = 0.001 +- offset_both_sides = 2 * offset +- for (x, y, z) in annotations_pts: +- if x < midx - bounding_box_size / 2 + offset: +- bounding_box_size = 2 * (midx - x) + offset_both_sides +- if x > midx + bounding_box_size / 2 - offset: +- bounding_box_size = 2 * (x - midx) + offset_both_sides +- if y < midy - bounding_box_size / 2 + offset: +- bounding_box_size = 2 * (midy - y) + offset_both_sides +- if y > midy + bounding_box_size / 2 - offset: +- bounding_box_size = 2 * (y - midy) + offset_both_sides +- if z < midz - bounding_box_size / 2 + offset: +- bounding_box_size = 2 * (midz - z) + offset_both_sides +- if z > midz + bounding_box_size / 2 - offset: +- bounding_box_size = 2 * (z - midz) + offset_both_sides +- +- relative_centre_z = 0.5*data.bounding_box.rangez/bounding_box_size +- camera_centre_z = -0.5 + relative_centre_z +- camera = dict(eye=dict(x=-0.5/controls.zoom, y=-1/controls.zoom, z=-0.5+0.5/controls.zoom), +- center=dict(x=0, y=0, z=camera_centre_z)) +- fig.update_layout(template='plotly_dark', paper_bgcolor="black", scene_aspectmode='cube', +- scene=dict(annotations=annotations, +- xaxis=dict(backgroundcolor="black", nticks=10, +- range=[data.bounding_box.midx-bounding_box_size/2, data.bounding_box.midx+bounding_box_size/2],), +- yaxis=dict(backgroundcolor="black", nticks=10, +- range=[data.bounding_box.midy-bounding_box_size/2, data.bounding_box.midy+bounding_box_size/2],), +- zaxis=dict(backgroundcolor="black", nticks=10, range=[min(0, data.bounding_box.minz), bounding_box_size],), +- ), scene_camera=camera, width=800, height=500, margin=dict(l=10, r=10, b=10, t=10, pad=4)) +- if controls.hide_axes or controls.neat_for_publishing: +- for axis in ['xaxis', 'yaxis', 'zaxis']: +- fig.update_layout( +- scene={axis: dict(showgrid=False, zeroline=False, visible=False)}) +- if controls.neat_for_publishing: +- fig.update_layout(width=500, height=500) +- +- # cicd_testing is a flag set by the CICD testing script (as a temporary environmental variable) to save the plot as a .png file +- return fig ++# base fc namespace for user access; infinaxis replacements override matching names ++from lab.fullcontrol.infinaxis.axis import Axis ++from lab.fullcontrol.infinaxis.controls import GcodeControls ++from lab.fullcontrol.infinaxis.point import Point, configure_point ++from lab.fullcontrol.infinaxis.printer import Printer ++from lab.fullcontrol.infinaxis.steps2gcode import gcode ++from lab.fullcontrol.infinaxis.xyz_add_axes import xyz_add_axes + + + def transform(steps: list, result_type: str, controls: Union[GcodeControls, PlotControls] = None, show_tips: bool = True): +@@ -311,7 +18,6 @@ + ''' + + if result_type == 'gcode': +- from lab.fullcontrol.infinaxis.steps2gcode import gcode + if controls is None: controls = GcodeControls() + return gcode(steps, controls) + +@@ -321,16 +27,18 @@ + return visualize(steps, controls, show_tips) + + elif result_type == 'fig': +- from fullcontrol.visualize.steps2visualization import visualize ++ from fullcontrol.visualize.plot_data import PlotData ++ from fullcontrol.visualize.state import State as VisualizeState ++ from lab.fullcontrol.infinaxis._plot import fig_plot + + if controls is None: controls = PlotControls() + plot_controls = controls + plot_controls.initialize() + +- state = State(steps, plot_controls) ++ state = VisualizeState(steps, plot_controls) + plot_data = PlotData(steps, state) + for step in steps: + step.visualize(state, plot_data, plot_controls) + plot_data.cleanup() + +- return fig_plot(plot_data, plot_controls)+ return fig_plot(plot_data, plot_controls) +``` + +## `lab/fullcontrol/infaxis/controls.py` -> `lab/fullcontrol/infinaxis/controls.py` + +```diff +--- lab/fullcontrol/infinaxis/controls.py ++++ lab/fullcontrol/infinaxis/controls.py +@@ -11,6 +11,7 @@ + bed_chain: list = [] # Ordered list of axes in the bed chain, used for the IK loop (back to front!) + xyz_orientation: Optional[list] = [1,1,1] # orientation of XYZ axes, 1 means the axis follows the right hand rule. + inverse_time_feedrate: Optional[bool] = False # if true, F command will be output as inverse time feedrate (e.g. F0.5 for 2 seconds per move) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. Note that when this is true, the print_speed and travel_speed attributes will be interpreted as seconds per move instead of mm/s. Also note that acceleration and deceleration will not be handled correctly when using inverse time feedrate, so it is recommended to use constant speed moves (G1 F...) when this is true. ++ # the following two parameters (model_XYZ_gcode and distance_axis) may be useful for give more information for motion planning + distance_axis: Optional[bool] = False + model_XYZ_gcode: Optional[bool] = False # if true, the gcode output will have a UVW axis which shows the the XYZ movement in the model (Alternative to distance_axis). Intended to be used with the system axis (XYZAC for instance) all be treated as rotational in firmware and the UVW being linear aixs for motion planning. + verbose: Optional[bool] = False``` + +## `lab/fullcontrol/infaxis/point.py` -> `lab/fullcontrol/infinaxis/point.py` + +```diff +--- lab/fullcontrol/infinaxis/point.py ++++ lab/fullcontrol/infinaxis/point.py +@@ -3,7 +3,56 @@ + from copy import deepcopy + import numpy as np + +-from infinaxis.axis import Axis ++from lab.fullcontrol.infinaxis.axis import Axis ++ ++ ++def _model_field_names(model_class): ++ return set(model_class.model_fields if hasattr(model_class, "model_fields") else model_class.__fields__) ++ ++ ++def _axis_names(head_chain=None, bed_chain=None): ++ axes = list(head_chain or []) + list(bed_chain or []) ++ return {axis.name for axis in axes if axis.name is not None} ++ ++ ++def configure_point(head_chain=None, bed_chain=None): ++ """Return a Point class whose extra constructor fields map to configured axes.""" ++ axis_name_lookup = {name.lower(): name for name in _axis_names(head_chain, bed_chain)} ++ ++ class ConfiguredPoint(Point): ++ # Keep axes as the storage model. This class is only a user-facing ++ # convenience so designs can write Point(x=..., b=...) and point.b. ++ def __init__(self, **data): ++ fields = _model_field_names(type(self)) ++ field_lookup = {name.lower(): name for name in fields} ++ axes = dict(data.pop("axes", None) or {}) ++ for name in list(data): ++ if name not in fields and name.lower() in field_lookup: ++ data[field_lookup[name.lower()]] = data.pop(name) ++ elif name.lower() in axis_name_lookup and name not in fields: ++ axes[axis_name_lookup[name.lower()]] = data.pop(name) ++ if axes: ++ data["axes"] = axes ++ super().__init__(**data) ++ ++ def __getattr__(self, name): ++ axes = getattr(self, "axes", None) ++ axis_name = axis_name_lookup.get(name.lower()) ++ if axes is not None and axis_name in axes: ++ return axes[axis_name] ++ raise AttributeError(name) ++ ++ def __setattr__(self, name, value): ++ axis_name = axis_name_lookup.get(name.lower()) ++ if axis_name is not None and name not in _model_field_names(type(self)): ++ axes = dict(getattr(self, "axes", None) or {}) ++ axes[axis_name] = value ++ super().__setattr__("axes", axes) ++ else: ++ super().__setattr__(name, value) ++ ++ return ConfiguredPoint ++ + + class Point(BasePoint): + axes: Optional[dict] = None # dictionary for assigning chages to the printer Axis based on the info in the point +@@ -89,9 +138,10 @@ + model_point = deepcopy(state.point) + model_point.update_from(self) + # Update Axis from point +- for axis in state.printer.head_chain + state.printer.bed_chain: +- if axis.name in self.axes and self.axes[axis.name] != None: +- axis.active = self.axes[axis.name] ++ if self.axes is not None: ++ for axis in state.printer.head_chain + state.printer.bed_chain: ++ if axis.name in self.axes and self.axes[axis.name] != None: ++ axis.active = self.axes[axis.name] + + # inverse kinematics: + system_point = model2system(model_point, state) +@@ -125,6 +175,7 @@ + + + state.distance_accumulated += (dist**2-dist_system**2)**0.5 if dist - dist_system > 0 else 0 ++ # the following two checks for model_XYZ_gcode and distance_axis are only passed if the user flags them in GcodeControls and may be useful for give more information for motion planning + if state.printer.model_XYZ_gcode: + infinaxis_str = infinaxis_str + f"U{round(self.x, 6):.6} V{round(self.y, 6):.6} W{round(self.z, 6):.6} " + elif state.printer.distance_axis: +@@ -167,4 +218,4 @@ + if change_check: + state.point.update_color(state, plot_data, plot_controls) + plot_data.paths[-1].add_point(state) +- state.point_count_now += 1+ state.point_count_now += 1 +``` + +## `lab/fullcontrol/infaxis/printer.py` -> `lab/fullcontrol/infinaxis/printer.py` + +```diff +--- lab/fullcontrol/infinaxis/printer.py ++++ lab/fullcontrol/infinaxis/printer.py +@@ -11,6 +11,7 @@ + bed_chain: list = None + xyz_orientation: list = None + inverse_time_feedrate: bool = None # if true, F command will be output as inverse time feedrate (e.g. F2 for 30 seconds per move (1/2 minutes per move)) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. ++ # the following two parameters (model_XYZ_gcode and distance_axis) may be useful for give more information for motion planning + distance_axis: bool = None + model_XYZ_gcode: bool = None + verbose: bool = None +``` + +## `lab/fullcontrol/infaxis/state.py` -> `lab/fullcontrol/infinaxis/state.py` + +```diff +--- lab/fullcontrol/infinaxis/state.py ++++ lab/fullcontrol/infinaxis/state.py +@@ -3,11 +3,10 @@ + from importlib import import_module + + from fullcontrol.gcode.extrusion_classes import ExtrusionGeometry, Extruder +-from fullcontrol.gcode.controls import GcodeControls + +-from infinaxis.point import Point +-from infinaxis.printer import Printer +-from infinaxis.controls import GcodeControls ++from lab.fullcontrol.infinaxis.point import Point ++from lab.fullcontrol.infinaxis.printer import Printer ++from lab.fullcontrol.infinaxis.controls import GcodeControls + + + class State(BaseModel): +@@ -36,14 +35,14 @@ + 'return first Point in list. if the parameter fully_defined is true, return first Point with x,y,z' + if type(steps).__name__ == 'list': + for i in range(len(steps)): +- if type(steps[i]).__name__ == 'Point': ++ if isinstance(steps[i], Point): + if fully_defined: + if steps[i].x != None and steps[i].y != None and steps[i].z != None: + return steps[i] + else: + return steps[i] + if fully_defined: +- raise Exception(f'No point found in steps with all five axis defined') ++ raise Exception(f'No point found in steps with fully defined x, y, and z') + if not fully_defined: + raise Exception(f'No point found in steps') + +@@ -98,4 +97,4 @@ + primer_steps.append(Extruder(on=False)) + primer_steps.append(first_infinaxis_point(steps)) # move fast to start position + primer_steps.append(Extruder(on=True)) +- self.steps = initialization_data['starting_procedure_steps'] + primer_steps + steps + initialization_data['ending_procedure_steps']+ self.steps = initialization_data['starting_procedure_steps'] + primer_steps + steps + initialization_data['ending_procedure_steps'] +``` + +## `lab/fullcontrol/infaxis/steps2gcode.py` -> `lab/fullcontrol/infinaxis/steps2gcode.py` + +```diff +--- lab/fullcontrol/infinaxis/steps2gcode.py ++++ lab/fullcontrol/infinaxis/steps2gcode.py +@@ -2,8 +2,8 @@ + import os + from datetime import datetime + +-from infinaxis.state import State +-from infinaxis.controls import GcodeControls ++from lab.fullcontrol.infinaxis.state import State ++from lab.fullcontrol.infinaxis.controls import GcodeControls + + + def gcode(steps: list, gcode_controls: GcodeControls = GcodeControls()): +@@ -22,4 +22,4 @@ + filename = gcode_controls.save_as + datetime.now().strftime("__%d-%m-%Y__%H-%M-%S.gcode") + open(filename, 'w').write(gc) + else: +- return gc+ return gc +``` + +## `tutorials/lab_infaxis_4_demo.ipynb` -> `tutorials/lab_infinaxis_4_demo.ipynb` + +```diff +--- tutorials/lab_infinaxis_4_demo.ipynb ++++ tutorials/lab_infinaxis_4_demo.ipynb +@@ -8,13 +8,7 @@ + "outputs": [], + "source": [ + "import lab.fullcontrol.infinaxis as fci\n", +- "import fullcontrol as fc\n", +- "from math import sin, cos, tau\n", +- "\n", +- "fc.Point = fci.Point\n", +- "fc.GcodeControls = fci.GcodeControls\n", +- "fc.transform = fci.transform\n", +- "fc.Axis = fci.Axis" ++ "from math import sin, cos, tau\n" + ] + }, + { +@@ -28,9 +22,13 @@ + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", +- "gcode_controls = fc.GcodeControls(\n", +- " head_chain = [],\n", +- " bed_chain = [fc.Axis(name='C')],\n", ++ "head_chain = []\n", ++ "bed_chain = [fci.Axis(name='C')]\n", ++ "Point = fci.configure_point(head_chain, bed_chain)\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", + " initialization_data=print_settings \n", + " )" + ] +@@ -50,20 +48,20 @@ + "\n", + "\n", + "steps = []\n", +- "steps.append(fc.Printer(print_speed=2160))\n", ++ "steps.append(fci.Printer(print_speed=2160))\n", + "for i in range(layers):\n", + " for j in range(density):\n", + " angle = 360*(i+j/density)\n", +- " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", ++ " steps.append(Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, c=angle))\n", + "\n", + "for step in steps:\n", +- " if type(step).__name__ == 'Point':\n", ++ " if isinstance(step, fci.Point):\n", + " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", +- " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", ++ " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", + "\n", +- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", ++ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", +- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" +``` + +## `tutorials/lab_infaxis_5_demo.ipynb` -> `tutorials/lab_infinaxis_5_demo.ipynb` + +```diff +--- tutorials/lab_infinaxis_5_demo.ipynb ++++ tutorials/lab_infinaxis_5_demo.ipynb +@@ -8,13 +8,7 @@ + "outputs": [], + "source": [ + "import lab.fullcontrol.infinaxis as fci\n", +- "import fullcontrol as fc\n", +- "from math import sin, cos, tau\n", +- "\n", +- "fc.Point = fci.Point\n", +- "fc.GcodeControls = fci.GcodeControls\n", +- "fc.transform = fci.transform\n", +- "fc.Axis = fci.Axis" ++ "from math import sin, cos, tau\n" + ] + }, + { +@@ -28,11 +22,13 @@ + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", +- "gcode_controls = fc.GcodeControls(\n", +- " head_chain = [fc.Axis(name='B')],\n", +- " bed_chain = [fc.Axis(name='C')],\n", +- " distance_axis = True,\n", +- " verbose = True,\n", ++ "head_chain = [fci.Axis(name='B')]\n", ++ "bed_chain = [fci.Axis(name='C')]\n", ++ "Point = fci.configure_point(head_chain, bed_chain)\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", + " initialization_data=print_settings \n", + " )" + ] +@@ -53,17 +49,17 @@ + " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", + " dr = r_next - r\n", + " dz = p_next.z-p.z\n", +- " ptilt = p.axes['B']\n", +- " dtilt = p_next.axes['B']-ptilt\n", ++ " ptilt = p.b\n", ++ " dtilt = p_next.b-ptilt\n", + " \n", +- " dc = p_next.axes['C']-p.axes['C']\n", ++ " dc = p_next.c-p.c\n", + " for j in range(density):\n", +- " angle = p.axes['C'] + dc * j / density\n", ++ " angle = p.c + dc * j / density\n", + " tilt = ptilt + dtilt * j / density\n", + " r_final = r + dr * j / density\n", + " z_final = p.z + dz * j / density\n", + " \n", +- " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", ++ " steps.append(Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, b=tilt, c=angle))\n", + " \n", + " return steps\n", + "\n", +@@ -87,45 +83,85 @@ + " angle = i * 360\n", + " z = z_start + h * i\n", + "\n", +- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", ++ " trace.append(Point(x=r, y=0, z=z, b=tilt, c=angle))\n", + "\n", +- "angle_offset = fc.last_point(trace).axes['C']\n", +- "z_offset = fc.last_point(trace).z\n", ++ "angle_offset = fci.last_point(trace).c\n", ++ "z_offset = fci.last_point(trace).z\n", + "\n", + "for i in range(1,arc_layers):\n", + " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", + " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", + " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", + " angle = i * 360 + angle_offset\n", +- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", ++ " trace.append(Point(x=r, y=0, z=z, b=-tilt, c=angle))\n", + "\n", +- "angle_offset = fc.last_point(trace).axes['C']\n", +- "z_offset = fc.last_point(trace).z\n", +- "r_offset = fc.last_point(trace).x\n", ++ "angle_offset = fci.last_point(trace).c\n", ++ "z_offset = fci.last_point(trace).z\n", ++ "r_offset = fci.last_point(trace).x\n", + "\n", + "for i in range(1,layers_tilted):\n", + " tilt = tilt_end\n", + " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", + " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", + " angle = i * 360 + angle_offset\n", +- " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", ++ " trace.append(Point(x=0, y=r, z=z, b=-tilt, c=angle))\n", + "\n", + "steps = vase_from_trace(trace, density)\n", +- "steps.append(fc.last_point(trace))\n", ++ "steps.append(fci.last_point(trace))\n", + "\n", + "for step in steps:\n", +- " if type(step).__name__ == 'Point':\n", ++ " if isinstance(step, fci.Point):\n", + " # color is a gradient from A=0 (blue) to A=90 (red)\n", +- " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", ++ " step.color = [((abs(step.b))/90), 0, 1-((abs(step.b))/90)]\n", + "\n", +- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", ++ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", +- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] ++ }, ++ { ++ "cell_type": "code", ++ "execution_count": null, ++ "id": "9462651e", ++ "metadata": {}, ++ "outputs": [], ++ "source": [ ++ "# with and without 'verbose'\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", ++ " verbose = False,\n", ++ " initialization_data=print_settings \n", ++ " )\n", ++ "\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", ++ "print('___\\nwith GcodeControls(verbose=False):')\n", ++ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", ++ " verbose = True,\n", ++ " initialization_data=print_settings \n", ++ " )\n", ++ "\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", ++ "print('\\n___\\nwith GcodeControls(verbose=True):')\n", ++ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" ++ ] ++ }, ++ { ++ "cell_type": "code", ++ "execution_count": null, ++ "id": "aaf9ec1a", ++ "metadata": {}, ++ "outputs": [], ++ "source": [] + } + ], + "metadata": { +``` + +## `tutorials/colab/lab_infaxis_4_demo_colab.ipynb` -> `tutorials/colab/lab_infinaxis_4_demo_colab.ipynb` + +```diff +--- tutorials/colab/lab_infinaxis_4_demo_colab.ipynb ++++ tutorials/colab/lab_infinaxis_4_demo_colab.ipynb +@@ -8,13 +8,7 @@ + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", +- "import fullcontrol as fc\n", +- "from math import sin, cos, tau\n", +- "\n", +- "fc.Point = fci.Point\n", +- "fc.GcodeControls = fci.GcodeControls\n", +- "fc.transform = fci.transform\n", +- "fc.Axis = fci.Axis" ++ "from math import sin, cos, tau\n" + ] + }, + { +@@ -28,9 +22,13 @@ + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", +- "gcode_controls = fc.GcodeControls(\n", +- " head_chain = [],\n", +- " bed_chain = [fc.Axis(name='C')],\n", ++ "head_chain = []\n", ++ "bed_chain = [fci.Axis(name='C')]\n", ++ "Point = fci.configure_point(head_chain, bed_chain)\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", + " initialization_data=print_settings \n", + " )" + ] +@@ -50,20 +48,20 @@ + "\n", + "\n", + "steps = []\n", +- "steps.append(fc.Printer(print_speed=2160))\n", ++ "steps.append(fci.Printer(print_speed=2160))\n", + "for i in range(layers):\n", + " for j in range(density):\n", + " angle = 360*(i+j/density)\n", +- " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", ++ " steps.append(Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, c=angle))\n", + "\n", + "for step in steps:\n", +- " if type(step).__name__ == 'Point':\n", ++ " if isinstance(step, fci.Point):\n", + " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", +- " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", ++ " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", + "\n", +- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", ++ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", +- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" +``` + +## `tutorials/colab/lab_infaxis_5_demo_colab.ipynb` -> `tutorials/colab/lab_infinaxis_5_demo_colab.ipynb` + +```diff +--- tutorials/colab/lab_infinaxis_5_demo_colab.ipynb ++++ tutorials/colab/lab_infinaxis_5_demo_colab.ipynb +@@ -8,13 +8,7 @@ + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", +- "import fullcontrol as fc\n", +- "from math import sin, cos, tau\n", +- "\n", +- "fc.Point = fci.Point\n", +- "fc.GcodeControls = fci.GcodeControls\n", +- "fc.transform = fci.transform\n", +- "fc.Axis = fci.Axis" ++ "from math import sin, cos, tau\n" + ] + }, + { +@@ -28,11 +22,13 @@ + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", +- "gcode_controls = fc.GcodeControls(\n", +- " head_chain = [fc.Axis(name='B')],\n", +- " bed_chain = [fc.Axis(name='C')],\n", +- " distance_axis = True,\n", +- " verbose = True,\n", ++ "head_chain = [fci.Axis(name='B')]\n", ++ "bed_chain = [fci.Axis(name='C')]\n", ++ "Point = fci.configure_point(head_chain, bed_chain)\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", + " initialization_data=print_settings \n", + " )" + ] +@@ -53,17 +49,17 @@ + " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", + " dr = r_next - r\n", + " dz = p_next.z-p.z\n", +- " ptilt = p.axes['B']\n", +- " dtilt = p_next.axes['B']-ptilt\n", ++ " ptilt = p.b\n", ++ " dtilt = p_next.b-ptilt\n", + " \n", +- " dc = p_next.axes['C']-p.axes['C']\n", ++ " dc = p_next.c-p.c\n", + " for j in range(density):\n", +- " angle = p.axes['C'] + dc * j / density\n", ++ " angle = p.c + dc * j / density\n", + " tilt = ptilt + dtilt * j / density\n", + " r_final = r + dr * j / density\n", + " z_final = p.z + dz * j / density\n", + " \n", +- " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", ++ " steps.append(Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, b=tilt, c=angle))\n", + " \n", + " return steps\n", + "\n", +@@ -87,45 +83,85 @@ + " angle = i * 360\n", + " z = z_start + h * i\n", + "\n", +- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", ++ " trace.append(Point(x=r, y=0, z=z, b=tilt, c=angle))\n", + "\n", +- "angle_offset = fc.last_point(trace).axes['C']\n", +- "z_offset = fc.last_point(trace).z\n", ++ "angle_offset = fci.last_point(trace).c\n", ++ "z_offset = fci.last_point(trace).z\n", + "\n", + "for i in range(1,arc_layers):\n", + " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", + " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", + " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", + " angle = i * 360 + angle_offset\n", +- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", ++ " trace.append(Point(x=r, y=0, z=z, b=-tilt, c=angle))\n", + "\n", +- "angle_offset = fc.last_point(trace).axes['C']\n", +- "z_offset = fc.last_point(trace).z\n", +- "r_offset = fc.last_point(trace).x\n", ++ "angle_offset = fci.last_point(trace).c\n", ++ "z_offset = fci.last_point(trace).z\n", ++ "r_offset = fci.last_point(trace).x\n", + "\n", + "for i in range(1,layers_tilted):\n", + " tilt = tilt_end\n", + " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", + " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", + " angle = i * 360 + angle_offset\n", +- " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", ++ " trace.append(Point(x=0, y=r, z=z, b=-tilt, c=angle))\n", + "\n", + "steps = vase_from_trace(trace, density)\n", +- "steps.append(fc.last_point(trace))\n", ++ "steps.append(fci.last_point(trace))\n", + "\n", + "for step in steps:\n", +- " if type(step).__name__ == 'Point':\n", ++ " if isinstance(step, fci.Point):\n", + " # color is a gradient from A=0 (blue) to A=90 (red)\n", +- " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", ++ " step.color = [((abs(step.b))/90), 0, 1-((abs(step.b))/90)]\n", + "\n", +- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", ++ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", +- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] ++ }, ++ { ++ "cell_type": "code", ++ "execution_count": null, ++ "id": "9462651e", ++ "metadata": {}, ++ "outputs": [], ++ "source": [ ++ "# with and without 'verbose'\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", ++ " verbose = False,\n", ++ " initialization_data=print_settings \n", ++ " )\n", ++ "\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", ++ "print('___\\nwith GcodeControls(verbose=False):')\n", ++ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", ++ "\n", ++ "gcode_controls = fci.GcodeControls(\n", ++ " head_chain = head_chain,\n", ++ " bed_chain = bed_chain,\n", ++ " verbose = True,\n", ++ " initialization_data=print_settings \n", ++ " )\n", ++ "\n", ++ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", ++ "print('\\n___\\nwith GcodeControls(verbose=True):')\n", ++ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" ++ ] ++ }, ++ { ++ "cell_type": "code", ++ "execution_count": null, ++ "id": "aaf9ec1a", ++ "metadata": {}, ++ "outputs": [], ++ "source": [] + } + ], + "metadata": { +``` + +## New files without old `infaxis` equivalent + +- `lab/fullcontrol/infinaxis/_plot.py`: present +- `lab/fullcontrol/infinaxis/xyz_add_axes.py`: present + diff --git a/lab/fullcontrol/infaxis/point.py b/lab/fullcontrol/infinaxis/point.py similarity index 72% rename from lab/fullcontrol/infaxis/point.py rename to lab/fullcontrol/infinaxis/point.py index e56b2c41..c77b8265 100644 --- a/lab/fullcontrol/infaxis/point.py +++ b/lab/fullcontrol/infinaxis/point.py @@ -3,12 +3,61 @@ from copy import deepcopy import numpy as np -from infaxis.axis import Axis +from lab.fullcontrol.infinaxis.axis import Axis + + +def _model_field_names(model_class): + return set(model_class.model_fields if hasattr(model_class, "model_fields") else model_class.__fields__) + + +def _axis_names(head_chain=None, bed_chain=None): + axes = list(head_chain or []) + list(bed_chain or []) + return {axis.name for axis in axes if axis.name is not None} + + +def configure_point(head_chain=None, bed_chain=None): + """Return a Point class whose extra constructor fields map to configured axes.""" + axis_name_lookup = {name.lower(): name for name in _axis_names(head_chain, bed_chain)} + + class ConfiguredPoint(Point): + # Keep axes as the storage model. This class is only a user-facing + # convenience so designs can write Point(x=..., b=...) and point.b. + def __init__(self, **data): + fields = _model_field_names(type(self)) + field_lookup = {name.lower(): name for name in fields} + axes = dict(data.pop("axes", None) or {}) + for name in list(data): + if name not in fields and name.lower() in field_lookup: + data[field_lookup[name.lower()]] = data.pop(name) + elif name.lower() in axis_name_lookup and name not in fields: + axes[axis_name_lookup[name.lower()]] = data.pop(name) + if axes: + data["axes"] = axes + super().__init__(**data) + + def __getattr__(self, name): + axes = getattr(self, "axes", None) + axis_name = axis_name_lookup.get(name.lower()) + if axes is not None and axis_name in axes: + return axes[axis_name] + raise AttributeError(name) + + def __setattr__(self, name, value): + axis_name = axis_name_lookup.get(name.lower()) + if axis_name is not None and name not in _model_field_names(type(self)): + axes = dict(getattr(self, "axes", None) or {}) + axes[axis_name] = value + super().__setattr__("axes", axes) + else: + super().__setattr__(name, value) + + return ConfiguredPoint + class Point(BasePoint): axes: Optional[dict] = None # dictionary for assigning chages to the printer Axis based on the info in the point - def infaxis_gcode(self, self_systemXYZ,state) -> float: + def infinaxis_gcode(self, self_systemXYZ,state) -> float: 'generate XYZABC gcode string to move from a point p to this point. return XYZABC string' p = state.point_systemXYZ s = '' @@ -89,9 +138,10 @@ def chain_matrix(chain,start_point=Point(x=0,y=0,z=0)): model_point = deepcopy(state.point) model_point.update_from(self) # Update Axis from point - for axis in state.printer.head_chain + state.printer.bed_chain: - if axis.name in self.axes and self.axes[axis.name] != None: - axis.active = self.axes[axis.name] + if self.axes is not None: + for axis in state.printer.head_chain + state.printer.bed_chain: + if axis.name in self.axes and self.axes[axis.name] != None: + axis.active = self.axes[axis.name] # inverse kinematics: system_point = model2system(model_point, state) @@ -104,8 +154,8 @@ def chain_matrix(chain,start_point=Point(x=0,y=0,z=0)): def gcode(self, state): 'process this instance in a list of steps supplied by the designer to generate and return a line of gcode' self_systemXYZ, dist, dist_system = self.inverse_kinematics(state) - infaxis_str = self.infaxis_gcode(self_systemXYZ,state) - if infaxis_str != None: # only write a line of gcode if movement occurs + infinaxis_str = self.infinaxis_gcode(self_systemXYZ,state) + if infinaxis_str != None: # only write a line of gcode if movement occurs G_str = 'G1 ' if state.extruder.on else 'G0 ' E_str = state.extruder.e_gcode(self, state) @@ -125,11 +175,12 @@ def gcode(self, state): state.distance_accumulated += (dist**2-dist_system**2)**0.5 if dist - dist_system > 0 else 0 + # the following two checks for model_XYZ_gcode and distance_axis are only passed if the user flags them in GcodeControls and may be useful for give more information for motion planning if state.printer.model_XYZ_gcode: - infaxis_str = infaxis_str + f"U{round(self.x, 6):.6} V{round(self.y, 6):.6} W{round(self.z, 6):.6} " + infinaxis_str = infinaxis_str + f"U{round(self.x, 6):.6} V{round(self.y, 6):.6} W{round(self.z, 6):.6} " elif state.printer.distance_axis: - infaxis_str = infaxis_str + f"U{state.distance_accumulated:.3f} " - gcode_str = f'{G_str}{F_str}{infaxis_str}{E_str}' + infinaxis_str = infinaxis_str + f"U{state.distance_accumulated:.3f} " + gcode_str = f'{G_str}{F_str}{infinaxis_str}{E_str}' if state.printer.verbose: gcode_str += f' ; distance: {dist:.3f}, system: {dist_system:.3f}' state.printer.speed_changed = False @@ -167,4 +218,4 @@ def visualize(self, state: 'State', plot_data: 'PlotData', plot_controls: 'PlotC if change_check: state.point.update_color(state, plot_data, plot_controls) plot_data.paths[-1].add_point(state) - state.point_count_now += 1 \ No newline at end of file + state.point_count_now += 1 diff --git a/lab/fullcontrol/infaxis/printer.py b/lab/fullcontrol/infinaxis/printer.py similarity index 91% rename from lab/fullcontrol/infaxis/printer.py rename to lab/fullcontrol/infinaxis/printer.py index 31fd2e76..c29f9e10 100644 --- a/lab/fullcontrol/infaxis/printer.py +++ b/lab/fullcontrol/infinaxis/printer.py @@ -11,6 +11,7 @@ class Printer(BasePrinter): bed_chain: list = None xyz_orientation: list = None inverse_time_feedrate: bool = None # if true, F command will be output as inverse time feedrate (e.g. F2 for 30 seconds per move (1/2 minutes per move)) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. + # the following two parameters (model_XYZ_gcode and distance_axis) may be useful for give more information for motion planning distance_axis: bool = None model_XYZ_gcode: bool = None verbose: bool = None diff --git a/lab/fullcontrol/infaxis/state.py b/lab/fullcontrol/infinaxis/state.py similarity index 89% rename from lab/fullcontrol/infaxis/state.py rename to lab/fullcontrol/infinaxis/state.py index e513566a..b288cfce 100644 --- a/lab/fullcontrol/infaxis/state.py +++ b/lab/fullcontrol/infinaxis/state.py @@ -3,11 +3,10 @@ from importlib import import_module from fullcontrol.gcode.extrusion_classes import ExtrusionGeometry, Extruder -from fullcontrol.gcode.controls import GcodeControls -from infaxis.point import Point -from infaxis.printer import Printer -from infaxis.controls import GcodeControls +from lab.fullcontrol.infinaxis.point import Point +from lab.fullcontrol.infinaxis.printer import Printer +from lab.fullcontrol.infinaxis.controls import GcodeControls class State(BaseModel): @@ -32,18 +31,18 @@ def __init__(self, steps: list, gcode_controls: GcodeControls): super().__init__() # initialize state based on the named-printer default initialization_data and initialization_data over-rides passed by designer in gcode_controls - def first_infaxis_point(steps: list, fully_defined: bool = True) -> Point: + def first_infinaxis_point(steps: list, fully_defined: bool = True) -> Point: 'return first Point in list. if the parameter fully_defined is true, return first Point with x,y,z' if type(steps).__name__ == 'list': for i in range(len(steps)): - if type(steps[i]).__name__ == 'Point': + if isinstance(steps[i], Point): if fully_defined: if steps[i].x != None and steps[i].y != None and steps[i].z != None: return steps[i] else: return steps[i] if fully_defined: - raise Exception(f'No point found in steps with all five axis defined') + raise Exception(f'No point found in steps with fully defined x, y, and z') if not fully_defined: raise Exception(f'No point found in steps') @@ -96,6 +95,6 @@ def first_infaxis_point(steps: list, fully_defined: bool = True) -> Point: # primer_steps = import_module(f'fullcontrol.gcode.primer_library.travel').primer(first_XYZBC_point(steps)) primer_steps = [] primer_steps.append(Extruder(on=False)) - primer_steps.append(first_infaxis_point(steps)) # move fast to start position + primer_steps.append(first_infinaxis_point(steps)) # move fast to start position primer_steps.append(Extruder(on=True)) - self.steps = initialization_data['starting_procedure_steps'] + primer_steps + steps + initialization_data['ending_procedure_steps'] \ No newline at end of file + self.steps = initialization_data['starting_procedure_steps'] + primer_steps + steps + initialization_data['ending_procedure_steps'] diff --git a/lab/fullcontrol/infaxis/steps2gcode.py b/lab/fullcontrol/infinaxis/steps2gcode.py similarity index 85% rename from lab/fullcontrol/infaxis/steps2gcode.py rename to lab/fullcontrol/infinaxis/steps2gcode.py index b7f7e651..c4e180d0 100644 --- a/lab/fullcontrol/infaxis/steps2gcode.py +++ b/lab/fullcontrol/infinaxis/steps2gcode.py @@ -2,8 +2,8 @@ import os from datetime import datetime -from infaxis.state import State -from infaxis.controls import GcodeControls +from lab.fullcontrol.infinaxis.state import State +from lab.fullcontrol.infinaxis.controls import GcodeControls def gcode(steps: list, gcode_controls: GcodeControls = GcodeControls()): @@ -22,4 +22,4 @@ def gcode(steps: list, gcode_controls: GcodeControls = GcodeControls()): filename = gcode_controls.save_as + datetime.now().strftime("__%d-%m-%Y__%H-%M-%S.gcode") open(filename, 'w').write(gc) else: - return gc \ No newline at end of file + return gc diff --git a/lab/fullcontrol/infinaxis/xyz_add_axes.py b/lab/fullcontrol/infinaxis/xyz_add_axes.py new file mode 100644 index 00000000..b16d8262 --- /dev/null +++ b/lab/fullcontrol/infinaxis/xyz_add_axes.py @@ -0,0 +1,23 @@ +from typing import Union + +from fullcontrol import Point as XYZPoint + +from lab.fullcontrol.infinaxis.point import Point + + +def xyz_add_axes(xyz_geometry: Union[XYZPoint, list], point_class=Point) -> Union[Point, list]: + 'convert xyz geometry points to infinaxis Points' + if isinstance(xyz_geometry, XYZPoint): + pt = point_class() + pt.update_from(xyz_geometry) + return pt + + geometry_new = [] + for step in xyz_geometry: + if isinstance(step, XYZPoint): + pt = point_class() + pt.update_from(step) + geometry_new.append(pt) + else: + geometry_new.append(step) + return geometry_new diff --git a/tutorials/README.md b/tutorials/README.md index 5f426dfc..d26488a7 100644 --- a/tutorials/README.md +++ b/tutorials/README.md @@ -37,3 +37,9 @@ for use on a local system, clone this repo and open the contents notebooks (cont 1. stl-output example - [lab_stl_output.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/lab_stl_output_colab.ipynb) 1. 3mf-output example - [lab_3mf_output.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/lab_3mf_output_colab.ipynb) +### FullControl infinaxis: +1. infinaxis 4-axis example - [infinaxis_4axis_demo.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_4axis_demo_colab.ipynb) +1. infinaxis 5-axis example - [infinaxis_5axis_demo.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_5axis_demo_colab.ipynb) +1. infinaxis controls - [infinaxis_controls.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_controls_colab.ipynb) +1. infinaxis custom axes - [infinaxis_custom_axes.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_custom_axes_colab.ipynb) +1. infinaxis XYZ geometry - [infinaxis_xyz_geom.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_xyz_geom_colab.ipynb) diff --git a/tutorials/colab/contents_colab.ipynb b/tutorials/colab/contents_colab.ipynb index e22e0d50..60fe5739 100644 --- a/tutorials/colab/contents_colab.ipynb +++ b/tutorials/colab/contents_colab.ipynb @@ -42,6 +42,13 @@ "1. stl output - [lab_stl_output.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/lab_stl_output_colab.ipynb)\n", "1. 3mf output - [lab_3mf_output.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/lab_3mf_output_colab.ipynb)\n", "\n", + "#### FullControl infinaxis:\n", + "1. infinaxis 4-axis example - [infinaxis_4axis_demo.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_4axis_demo_colab.ipynb)\n", + "1. infinaxis 5-axis example - [infinaxis_5axis_demo.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_5axis_demo_colab.ipynb)\n", + "1. infinaxis controls - [infinaxis_controls.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_controls_colab.ipynb)\n", + "1. infinaxis custom axes - [infinaxis_custom_axes.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_custom_axes_colab.ipynb)\n", + "1. infinaxis XYZ geometry - [infinaxis_xyz_geom.ipynb](https://colab.research.google.com/github/FullControlXYZ/fullcontrol/blob/master/tutorials/colab/infinaxis_xyz_geom_colab.ipynb)\n", + "\n", "See the [FullControl github repository](https://github.com/FullControlXYZ/fullcontrol#readme)" ] } diff --git a/tutorials/colab/lab_infaxis_4_demo_colab.ipynb b/tutorials/colab/infinaxis_4axis_demo_colab.ipynb similarity index 66% rename from tutorials/colab/lab_infaxis_4_demo_colab.ipynb rename to tutorials/colab/infinaxis_4axis_demo_colab.ipynb index b0d319d3..e5ca75d6 100644 --- a/tutorials/colab/lab_infaxis_4_demo_colab.ipynb +++ b/tutorials/colab/infinaxis_4axis_demo_colab.ipynb @@ -7,14 +7,8 @@ "metadata": {}, "outputs": [], "source": [ - "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infaxis as fci\n", - "import fullcontrol as fc\n", - "from math import sin, cos, tau\n", - "\n", - "fc.Point = fci.Point\n", - "fc.GcodeControls = fci.GcodeControls\n", - "fc.transform = fci.transform\n", - "fc.Axis = fci.Axis" + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", + "from math import sin, cos, tau\n" ] }, { @@ -28,9 +22,13 @@ "EH = 0.3\n", "\n", "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", - "gcode_controls = fc.GcodeControls(\n", - " head_chain = [],\n", - " bed_chain = [fc.Axis(name='C')],\n", + "head_chain = []\n", + "bed_chain = [fci.Axis(name='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain = head_chain,\n", + " bed_chain = bed_chain,\n", " initialization_data=print_settings \n", " )" ] @@ -50,20 +48,20 @@ "\n", "\n", "steps = []\n", - "steps.append(fc.Printer(print_speed=2160))\n", + "steps.append(fci.Printer(print_speed=2160))\n", "for i in range(layers):\n", " for j in range(density):\n", " angle = 360*(i+j/density)\n", - " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", + " steps.append(Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, c=angle))\n", "\n", "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", + " if isinstance(step, fci.Point):\n", " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", - " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", + " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", "\n", - "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", "fig.show()\n", - "gcode = fc.transform(steps,'gcode',gcode_controls)\n", + "gcode = fci.transform(steps,'gcode',gcode_controls)\n", "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", "print('')\n", "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" @@ -72,7 +70,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "fc_dev (3.12.3)", "language": "python", "name": "python3" }, @@ -86,7 +84,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.11" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/tutorials/colab/infinaxis_5axis_demo_colab.ipynb b/tutorials/colab/infinaxis_5axis_demo_colab.ipynb new file mode 100644 index 00000000..4115d81c --- /dev/null +++ b/tutorials/colab/infinaxis_5axis_demo_colab.ipynb @@ -0,0 +1,206 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "88f67711", + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", + "from math import sin, cos, tau\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d54e7f4", + "metadata": {}, + "outputs": [], + "source": [ + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain = head_chain,\n", + " bed_chain = bed_chain,\n", + " initialization_data=print_settings \n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "ffe83b59", + "metadata": {}, + "source": [ + "### QUESITON: I've created a simpler version of the next code cell in a new code cell immediately after it. Check this is okay, then delete the more complex code cell just below this comment. The intention is that readers don't need to think about the geometry/python stuff too much and focus more on the fci-specific aspects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa20bc2b", + "metadata": {}, + "outputs": [], + "source": [ + "def vase_from_trace(trace_points,density):\n", + " steps = []\n", + " for i in range(len(trace_points)-1):\n", + " p = trace_points[i]\n", + " p_next = trace_points[i+1]\n", + " r = (p.x**2 + p.y**2)**0.5\n", + " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", + " dr = r_next - r\n", + " dz = p_next.z-p.z\n", + " ptilt = p.b\n", + " dtilt = p_next.b-ptilt\n", + " \n", + " dc = p_next.c-p.c\n", + " for j in range(density):\n", + " angle = p.c + dc * j / density\n", + " tilt = ptilt + dtilt * j / density\n", + " r_final = r + dr * j / density\n", + " z_final = p.z + dz * j / density\n", + " \n", + " steps.append(Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, b=tilt, c=angle))\n", + " \n", + " return steps\n", + "\n", + "density = 360\n", + "r_start = 10\n", + "r_tilt = 5 # 10 layers\n", + "tilt_start = 0\n", + "tilt_end = 90\n", + "h = EH # height of each layer\n", + "z_start = h*0.5 # starting z height\n", + "layers = 20 # number of layers in the z direction for first segment\n", + "arc_layers = int((r_tilt * (tilt_end - tilt_start) / 360 *tau)/h)\n", + "d_tilted = 5\n", + "layers_tilted = int(d_tilted/h)\n", + "\n", + "trace = []\n", + "\n", + "for i in range(layers):\n", + " r = r_start\n", + " tilt = tilt_start\n", + " angle = i * 360\n", + " z = z_start + h * i\n", + "\n", + " trace.append(Point(x=r, y=0, z=z, b=tilt, c=angle))\n", + "\n", + "angle_offset = fci.last_point(trace).c\n", + "z_offset = fci.last_point(trace).z\n", + "\n", + "for i in range(1,arc_layers):\n", + " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", + " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", + " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", + " angle = i * 360 + angle_offset\n", + " trace.append(Point(x=r, y=0, z=z, b=-tilt, c=angle))\n", + "\n", + "angle_offset = fci.last_point(trace).c\n", + "z_offset = fci.last_point(trace).z\n", + "r_offset = fci.last_point(trace).x\n", + "\n", + "for i in range(1,layers_tilted):\n", + " tilt = tilt_end\n", + " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", + " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", + " angle = i * 360 + angle_offset\n", + " trace.append(Point(x=0, y=r, z=z, b=-tilt, c=angle))\n", + "\n", + "steps = vase_from_trace(trace, density)\n", + "steps.append(fci.last_point(trace))\n", + "\n", + "for step in steps:\n", + " if isinstance(step, fci.Point):\n", + " # color is a gradient from A=0 (blue) to A=90 (red)\n", + " step.color = [((abs(step.b))/90), 0, 1-((abs(step.b))/90)]\n", + "\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", + "gcode = fci.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### same style of five-axis path with simpler direct construction\n", + "\n", + "This version uses one spiral turn per layer. `B` tilt increases by 1 degree per layer for 90 layers. The layer-to-layer spacing is kept constant by splitting the same step length into radial and vertical components with sine/cosine.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e73fc8bc", + "metadata": {}, + "outputs": [], + "source": [ + "# simpler direct version of the five-axis demo path\n", + "\n", + "density = 360\n", + "layers = 90\n", + "layer_gap = EH\n", + "r = 10\n", + "z = EH / 2\n", + "steps = []\n", + "\n", + "for layer in range(layers):\n", + " tilt = layer\n", + " dr = layer_gap * sin(tilt / 360 * tau)\n", + " dz = layer_gap * cos(tilt / 360 * tau)\n", + " for j in range(density):\n", + " t = j / density\n", + " angle = 360 * (layer + t)\n", + " r_now = r + dr * t\n", + " z_now = z + dz * t\n", + " steps.append(Point(x=r_now * sin(angle / 360 * tau), y=r_now * cos(angle / 360 * tau), z=z_now, b=-tilt, c=angle))\n", + " r += dr\n", + " z += dz\n", + "\n", + "for step in steps:\n", + " step.color = [abs(step.b) / 90, 0, 1 - abs(step.b) / 90]\n", + "\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual', style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", + "gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fiveaxis (3.12.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/colab/infinaxis_controls_colab.ipynb b/tutorials/colab/infinaxis_controls_colab.ipynb new file mode 100644 index 00000000..ede84618 --- /dev/null +++ b/tutorials/colab/infinaxis_controls_colab.ipynb @@ -0,0 +1,372 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# infinaxis controls demo\n", + "\n", + "This notebook shows the main `Axis` and `GcodeControls` options that affect generated gcode.\n", + "\n", + "The next cell contains reusable functions that help keep subsequent cells concise" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", + "\n", + "EW = 0.6\n", + "EH = 0.3\n", + "print_settings = {'extrusion_width': EW, 'extrusion_height': EH}\n", + "\n", + "base_steps_data = [\n", + " dict(x=0, y=10, z=EH / 2, b=0, c=0),\n", + " dict(x=4, y=10, z=EH / 2, b=10, c=20),\n", + " dict(x=8, y=12, z=EH, b=20, c=40),\n", + " dict(x=12, y=14, z=EH * 1.5, b=30, c=60),\n", + "]\n", + "\n", + "def build_steps(head_chain, bed_chain, steps_data=base_steps_data):\n", + " Point = fci.configure_point(head_chain, bed_chain)\n", + " return [Point(**step_data) for step_data in steps_data]\n", + "\n", + "def controls(head_chain, bed_chain, **kwargs):\n", + " return fci.GcodeControls(\n", + " head_chain=head_chain,\n", + " bed_chain=bed_chain,\n", + " initialization_data=print_settings,\n", + " **kwargs,\n", + " )\n", + "\n", + "def print_gcode_sample(label, steps, gcode_controls, look_for):\n", + " gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + " print('___\\n' + label)\n", + " print(f'Look for: {look_for}')\n", + " print('\\n'.join(gcode.split('\\n')[-8:]))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Baseline\n", + "\n", + "A normal two-rotary-axis example. `Axis.name` is the gcode name and, for standard `A/B/C`, also provides the default kinematic `type`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'baseline Axis(name=\"B\") + Axis(name=\"C\"):',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'B and C appear in each move; XYZ is inverse-kinematics output.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.name and Axis.type\n", + "\n", + "Use `name` for the emitted gcode axis and `type` for the kinematic behavior. This allows machine names like `CI` and `CII` to both behave as C-style rotary axes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='CII', type='C')]\n", + "bed_chain = [fci.Axis(name='CI', type='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "steps = [\n", + " Point(x=0, y=10, z=EH / 2, CI=0, CII=0),\n", + " Point(x=4, y=10, z=EH / 2, CI=20, CII=-10),\n", + " Point(x=8, y=12, z=EH, CI=40, CII=-20),\n", + "]\n", + "\n", + "print_gcode_sample(\n", + " 'custom axis names with shared C kinematics:',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'CI and CII are emitted, while type=\"C\" controls the rotation math.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.active\n", + "\n", + "`active` sets the starting position of an axis before any point explicitly changes it.\n", + "\n", + "### QUESTION: is 'active' necessary? It seems like you could do that by setting axis to have an initial value in the first Point. I assume this is actually to allow the axis to have a non-zero value when setting up subsequent axes after it in the same head or bed chain? If so, perhaps a better name than 'active' exists? Either way, give a clear description in code comments and in this demo notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = []\n", + "bed_chain = [fci.Axis(name='C', active=45)]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "steps = [\n", + " Point(x=0, y=10, z=EH / 2),\n", + " Point(x=4, y=10, z=EH / 2),\n", + " Point(x=8, y=12, z=EH, c=90),\n", + "]\n", + "\n", + "print_gcode_sample(\n", + " 'Axis(active=45) before points set c:',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'C starts at 45.0, then changes to 90.0 when the point sets c=90.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.orientation\n", + "\n", + "`orientation` flips the sign used by the kinematic calculation. The commanded axis value is still emitted with the configured `Axis.name`; the XYZ solution changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "orientation_steps_data = [\n", + " dict(x=0, y=10, z=EH / 2, c=0),\n", + " dict(x=4, y=10, z=EH / 2, c=20),\n", + " dict(x=8, y=12, z=EH, c=40),\n", + "]\n", + "\n", + "for orientation in [1, -1]:\n", + " head_chain = []\n", + " bed_chain = [fci.Axis(name='C', orientation=orientation)]\n", + " steps = build_steps(head_chain, bed_chain, orientation_steps_data)\n", + " print_gcode_sample(\n", + " f'Axis(name=\"C\", orientation={orientation}):',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'Compare XYZ values between orientation=1 and orientation=-1. C values remain commanded values.',\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.offset\n", + "\n", + "`offset` describes the location of an axis relative to the previous axis in the chain. CHECK_THIS_DESCRIPTION: confirm exact sign convention for the target machine before using this on hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for offset in [fci.Point(x=0, y=0, z=0), fci.Point(x=5, y=0, z=0)]:\n", + " head_chain = [fci.Axis(name='B', offset=offset)]\n", + " bed_chain = [fci.Axis(name='C')]\n", + " steps = build_steps(head_chain, bed_chain)\n", + " print_gcode_sample(\n", + " f'Axis(name=\"B\", offset={offset}):',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'Compare XYZ values as the head rotary axis offset changes.',\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.verbose\n", + "\n", + "`verbose=True` adds distance diagnostics as gcode comments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "for verbose in [False, True]:\n", + " print_gcode_sample(\n", + " f'GcodeControls(verbose={verbose}):',\n", + " steps,\n", + " controls(head_chain, bed_chain, verbose=verbose),\n", + " 'Verbose output adds ; distance and system comments when enabled.',\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.distance_axis\n", + "\n", + "`distance_axis=True` emits a synthetic `U` value based on accumulated model/path distance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'GcodeControls(distance_axis=True):',\n", + " steps,\n", + " controls(head_chain, bed_chain, distance_axis=True),\n", + " 'U appears after B/C and increases as accumulated distance changes.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.model_XYZ_gcode\n", + "\n", + "`model_XYZ_gcode=True` emits model-space `U/V/W` values. CHECK_THIS_DESCRIPTION: confirm target firmware/motion-planner expectations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'GcodeControls(model_XYZ_gcode=True):',\n", + " steps,\n", + " controls(head_chain, bed_chain, model_XYZ_gcode=True),\n", + " 'U/V/W show the model-space x/y/z values alongside transformed XYZ/B/C.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.inverse_time_feedrate\n", + "\n", + "`inverse_time_feedrate=True` changes the F calculation for extrusion moves. CHECK_THIS_DESCRIPTION: verify units and firmware mode before using on a machine.\n", + "\n", + "### QUESTION: Don't we need to add an M command at the start of the gcode if this is set to true? That should be automatically added (like M83)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'GcodeControls(inverse_time_feedrate=True):',\n", + " steps,\n", + " controls(head_chain, bed_chain, inverse_time_feedrate=True),\n", + " 'F values differ from the baseline calculation.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.xyz_orientation\n", + "\n", + "`xyz_orientation` flips output signs for system XYZ axes. CHECK_THIS_DESCRIPTION: confirm this is the intended way to handle machine axis direction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'Baseline output for regular orientation, GcodeControls(xyz_orientation=[1, 1, 1]):',\n", + " steps,\n", + " controls(head_chain, bed_chain, xyz_orientation=[1, 1, 1]),\n", + " '',\n", + ")\n", + "\n", + "print_gcode_sample(\n", + " 'X orientation flipped, GcodeControls(xyz_orientation=[-1, 1, 1]):',\n", + " steps,\n", + " controls(head_chain, bed_chain, xyz_orientation=[-1, 1, 1]),\n", + " 'X output changes sign compared with the baseline.',\n", + ")\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/colab/infinaxis_custom_axes_colab.ipynb b/tutorials/colab/infinaxis_custom_axes_colab.ipynb new file mode 100644 index 00000000..b2a77a7f --- /dev/null +++ b/tutorials/colab/infinaxis_custom_axes_colab.ipynb @@ -0,0 +1,73 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9cad98ab", + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", + "\n", + "head_chain = [fci.Axis(name='CI', type='C')]\n", + "bed_chain = [fci.Axis(name='CII', type='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain=head_chain,\n", + " bed_chain=bed_chain,\n", + " initialization_data={'extrusion_width': 0.6, 'extrusion_height': 0.3},\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ddc4461", + "metadata": {}, + "outputs": [], + "source": [ + "steps = [\n", + " Point(x=0, y=0, z=0.15, CI=0, CII=0),\n", + " Point(x=10, CI=30, CII=-15),\n", + " Point(y=10, CI=60, CII=-30),\n", + "]\n", + "\n", + "print(steps[1].axes)\n", + "print(steps[2].CI, steps[2].CII)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "656e27c8", + "metadata": {}, + "outputs": [], + "source": [ + "gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + "print('\\n'.join(gcode.split('\\n')[:8]))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc (3.12.3.final.0)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/colab/infinaxis_xyz_geom_colab.ipynb b/tutorials/colab/infinaxis_xyz_geom_colab.ipynb new file mode 100644 index 00000000..1377c5d2 --- /dev/null +++ b/tutorials/colab/infinaxis_xyz_geom_colab.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", + "\n", + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "head_chain = []\n", + "bed_chain = [fci.Axis(name='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain=head_chain,\n", + " bed_chain=bed_chain,\n", + " initialization_data={'extrusion_width': EW, 'extrusion_height': EH},\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "xyz_steps = fci.xyz_geom.circleXY(\n", + " fci.xyz_geom.Point(x=0, y=0, z=EH / 2),\n", + " radius=20,\n", + " start_angle=0,\n", + " segments=72,\n", + ")\n", + "\n", + "steps = fci.xyz_add_axes(xyz_steps, Point)\n", + "steps.insert(0, fci.Printer(print_speed=1200))\n", + "\n", + "for i, step in enumerate(steps):\n", + " if isinstance(step, fci.Point):\n", + " step.c = i * 5\n", + " step.color = [(step.c % 360) / 360, 0, 1 - ((step.c % 360) / 360)]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = fci.transform(\n", + " steps,\n", + " 'fig',\n", + " fci.PlotControls(color_type='manual', style='line', zoom=0.75),\n", + " show_tips=False,\n", + ")\n", + "fig.show()\n", + "\n", + "gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/colab/lab_five_axis_demo_colab.ipynb b/tutorials/colab/lab_five_axis_demo_colab.ipynb index 6cf59d32..3b7ef868 100644 --- a/tutorials/colab/lab_five_axis_demo_colab.ipynb +++ b/tutorials/colab/lab_five_axis_demo_colab.ipynb @@ -1,342 +1,349 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# lab five-axis demo\n", - "\n", - "this documentation gives a brief overview of 5-axis capabilities - it will be expanded in the future\n", - "\n", - "most of this tutorial relates to a system with B-C rotation stages, where C-axis rotations do not affect the B axis, but B-axis rotations do alter the C axis orientation (i.e. a rotating stage [C] mounted onto a tilting platform [B])\n", - "\n", - "the final section of this notebook demosntrates how to generate gcode for a system with a rotating nozzle (B) and rotating bed (C) \n", - "\n", - "the generated gcode would work on other 5-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", - "\n", - "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", - "[link](https://www.google.com/search?q=ipynb+tutorial),\n", - "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", - "[link](https://colab.research.google.com/)*>\n", - "\n", - "*run all cells in this notebook in order (keep pressing shift+enter)*" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### five axis import" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.fiveaxis as fc5\n", - "import fullcontrol as fc\n", - "import lab.fullcontrol as fclab" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### basic demo\n", - "\n", - "points are designed in the model's XYZ coordinate system\n", - "\n", - "the point x=0, y=0, z=0 in the model's coordinate system represents the intercept point of B and C axes\n", - "\n", - "FullControl translates them to the 3D printer XYZ coordinates, factoring in the effect of rotations to B and C axes\n", - "\n", - "a full explanation of this concept is out of scope for this brief tutorial notebook - google 5-axis kinematics for more info\n", - "\n", - "however, the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", - "steps.append(fc5.Point(x=1))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", - "steps.append(fc5.Point(b=45))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", - "steps.append(fc5.Point(b=90))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=90 - although x and z change, E=0 because the nozzle stays in the same point on the model'))\n", - "steps.append(fc5.Point(b=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=0'))\n", - "steps.append(fc5.Point(c=90))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set c=90 - this causes a change to x and y in system coordinates'))\n", - "steps.append(fc5.Point(y=1))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set y=1 - this causes a change to x in system coordinates since the model is rotated 90 degrees'))\n", - "print(fc5.transform(steps, 'gcode'))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### add custom color to preview axes\n", - "\n", - "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", - "\n", - "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps = []\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B0'))\n", - "steps.append(fc5.Point(x=10, y=5, z=0, b=0, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B0'))\n", - "steps.append(fc5.Point(y=10, z=0, b=-180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B-180'))\n", - "steps.append(fc5.Point(x=0, y=15, b=-180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B-180'))\n", - "steps.append(fc5.Point(y=20, b=180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B+180'))\n", - "steps.append(fc5.Point(x=10, y=25, b=180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B+180'))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", - " step.color = [((step.b+180)/360), 0, 1-((step.b+180)/360)]\n", - "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual'))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### a more complex color example\n", - "\n", - "this example shows a wavey helical print path, where the model is continuously rotating while the nozzle gradually moves away from the print platform\n", - "\n", - "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from math import sin, cos, tau\n", - "steps = []\n", - "for i in range(10001):\n", - " angle = tau*i/200\n", - " offset = (1.5*(i/10000)**2)*cos(angle*6)\n", - " steps.append(fc5.Point(x=(6+offset)*sin(angle), y=(6+offset)*cos(angle), z=((i/200)*0.1)-offset/2, b=(offset/1.5)*30, c=angle*360/tau))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=0 (blue) to B=45 (red)\n", - " step.color = [((step.b+30)/60), 0, 1-((step.b+30)/60)]\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=8.75), label='color indicates B axis (tilt)'))\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=7.5), label='-30 deg (blue) to +30 deg (red)'))\n", - "gcode = fc5.transform(steps,'gcode')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", - "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual', hide_axes=False, zoom=0.75))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### use 3-axis geometry functions from FullControl (with caution!)\n", - "\n", - "this functionality should be considered experimental at best!\n", - "\n", - "geometry functions that generate 3-axis points can be used - accessed via fc5.xyz_geom()\n", - "\n", - "but they must be translated to have 5-axis methods for gcode generation - achieved via fc5.xyz_add_bc()\n", - "\n", - "this conversion does not set any values of B or C attributes for those points - the BC values will remain at whatever values they were in the ***design*** before the list of converted points\n", - "\n", - "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 90\n", - "\n", - "therefore, the 3D printer actually prints a circle in the YZ plane since the model coordinate system has been rotated by 90 degrees about the B axis\n", - "\n", - "hence, when the ***design*** is transformed to a 'gcode' ***result***, Y and Z values vary in gcode while X is constant (of course this would not print well - it's just for simple demonstration)\n", - "\n", - "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates because 5-axis plots in the 3D-printer's coordinates system often make no sense visually" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc5.Point(x=10, y=0, z=0, b=90, c=0))\n", - "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, 16)\n", - "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", - "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc5.xyz_geom'))\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=3.5), label='but points must be converted to 5-axis variants via fc5.xyz_add_bc'))\n", - "print(fc5.transform(steps, 'gcode'))\n", - "fc5.transform(steps, 'plot')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### bc_intercept\n", - "\n", - "if the machine's coordinate system is **not** set up so that the b and c axes intercept at the point x=0, y=0, z=0, the bc_intercept data can be provided in a GcodeControls object to ensure correct gcode generation\n", - "\n", - "the GcodeControls object has slightly less functionality for 5-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n", - "\n", - "note that although the system does not need the b and c axes to intercept at the point x=0, y=0, z=0, the model coordinate system must still be implemented such that the point x=0, y=0, z=0 represents the intercept point of b and c axes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "gcode_controls = fc5.GcodeControls(bc_intercept = fc5.Point(x=10, y=0, z=0), initialization_data={'nozzle_temp': 250})\n", - "steps=[]\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point (x=0 in the model but x=10 in gcode due to the bc_intercept being at x=10)'))\n", - "steps.append(fc5.Point(x=1))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", - "steps.append(fc5.Point(b=45))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", - "print(fc5.transform(steps, 'gcode', gcode_controls))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### rotating-nozzle 5-axis system\n", - "\n", - "if the nozzle rotates about the Y axis, as opposed to the rotating bed tilting about the Y axis (as was the case for the code above), but the print bed still rotates about the Z axis, you can import `lab.fullcontrol.fiveaxisC0B1` as fc5 instead of ` lab.fullcontrol.fiveaxis`\n", - "\n", - "this is shown in the code cell below. note that the new import statement means the previous import of fc5 is nullified, so don't try to run the above code cells after running the next code cell or they won't work\n", - "\n", - "the simple instructions below show how rotation of the bed and of the nozzle independently result in necessary changes to X and Y in gcode\n", - "\n", - "in the future, the way of defining which type of multiaxis printer you change will be made more intuitive and procedural, but this works for now." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.fiveaxisC0B1 as fc5\n", - "b_offset_z = 40\n", - "steps=[]\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", - "steps.extend([fc5.Point(x=1), fc5.GcodeComment(end_of_previous_line_text='x=1')])\n", - "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", - "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", - "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", - "steps.extend([fc5.Point(x=0, y=1, c=0), fc5.GcodeComment(end_of_previous_line_text='x=0, y=1, c=0')])\n", - "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", - "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", - "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", - "steps.extend([fc5.Point(b=90), fc5.GcodeComment(end_of_previous_line_text='b=90')])\n", - "steps.extend([fc5.Point(b=-90), fc5.GcodeComment(end_of_previous_line_text='b=-90')])\n", - "print(fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "to keep the nozzle directly to the hand side of the bed (Y=0) for every point, which is useful for nozzle tilting about Y when printing a funnel for example, you need to design C to rotate for each point" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from math import degrees\n", - "\n", - "circle_segments = 16\n", - "points_per_circle = circle_segments+1\n", - "steps = []\n", - "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, circle_segments)\n", - "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", - "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", - "steps[0].b, steps[0].c = 0.0, 0.0\n", - "gcode_without_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", - "fc5.transform(steps, 'plot')\n", - "\n", - "for i in range(len(steps)): steps[i].c = -360/circle_segments*i\n", - "# instead of the above for loop, you can use the following function to constantly vary c automatically. This is good for more complex geometry, where c cannot be 'designed' easily.\n", - "# steps = fclab.constant_polar_angle_with_c(points=steps, centre=fc5.Point(x=0, y=0, z=0), initial_c=-90)\n", - "gcode_with_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", - "\n", - "print(gcode_without_c_rotation +\n", - " '\\n\\n\\ngcode with C rotation to keep nozzle directly in +X direction from bed centre:\\n\\n' + gcode_with_c_rotation)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### next steps\n", - "\n", - "this tutorial notebook gives a brief introduction to five-axis use of FullControl for interest, but it is not an expansive implementation. it is included as an initial step towards translating in-house research for 5-axis gcode generation into a more general format compatible with the overall FullControl concept" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "fc", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - }, - "vscode": { - "interpreter": { - "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# lab five-axis demo\n", + "\n", + "this documentation gives a brief overview of 5-axis capabilities - it will be expanded in the future\n", + "\n", + "most of this tutorial relates to a system with B-C rotation stages, where C-axis rotations do not affect the B axis, but B-axis rotations do alter the C axis orientation (i.e. a rotating stage [C] mounted onto a tilting platform [B])\n", + "\n", + "the final section of this notebook demosntrates how to generate gcode for a system with a rotating nozzle (B) and rotating bed (C) \n", + "\n", + "the generated gcode would work on other 5-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", + "\n", + "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", + "[link](https://www.google.com/search?q=ipynb+tutorial),\n", + "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", + "[link](https://colab.research.google.com/)*>\n", + "\n", + "*run all cells in this notebook in order (keep pressing shift+enter)*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Fixed-axis note: this tutorial is retained as a reference for the older fixed-axis lab wrapper. New work may be better served by `lab.fullcontrol.infinaxis`, which is intended to express equivalent dynamic axis setups. Exact gcode equivalence is still being checked.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### five axis import" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.fiveaxis as fc5\n", + "import fullcontrol as fc\n", + "import lab.fullcontrol as fclab" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### basic demo\n", + "\n", + "points are designed in the model's XYZ coordinate system\n", + "\n", + "the point x=0, y=0, z=0 in the model's coordinate system represents the intercept point of B and C axes\n", + "\n", + "FullControl translates them to the 3D printer XYZ coordinates, factoring in the effect of rotations to B and C axes\n", + "\n", + "a full explanation of this concept is out of scope for this brief tutorial notebook - google 5-axis kinematics for more info\n", + "\n", + "however, the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", + "steps.append(fc5.Point(x=1))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", + "steps.append(fc5.Point(b=45))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", + "steps.append(fc5.Point(b=90))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=90 - although x and z change, E=0 because the nozzle stays in the same point on the model'))\n", + "steps.append(fc5.Point(b=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=0'))\n", + "steps.append(fc5.Point(c=90))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set c=90 - this causes a change to x and y in system coordinates'))\n", + "steps.append(fc5.Point(y=1))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set y=1 - this causes a change to x in system coordinates since the model is rotated 90 degrees'))\n", + "print(fc5.transform(steps, 'gcode'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### add custom color to preview axes\n", + "\n", + "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", + "\n", + "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps = []\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B0'))\n", + "steps.append(fc5.Point(x=10, y=5, z=0, b=0, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B0'))\n", + "steps.append(fc5.Point(y=10, z=0, b=-180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B-180'))\n", + "steps.append(fc5.Point(x=0, y=15, b=-180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B-180'))\n", + "steps.append(fc5.Point(y=20, b=180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B+180'))\n", + "steps.append(fc5.Point(x=10, y=25, b=180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B+180'))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", + " step.color = [((step.b+180)/360), 0, 1-((step.b+180)/360)]\n", + "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### a more complex color example\n", + "\n", + "this example shows a wavey helical print path, where the model is continuously rotating while the nozzle gradually moves away from the print platform\n", + "\n", + "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sin, cos, tau\n", + "steps = []\n", + "for i in range(10001):\n", + " angle = tau*i/200\n", + " offset = (1.5*(i/10000)**2)*cos(angle*6)\n", + " steps.append(fc5.Point(x=(6+offset)*sin(angle), y=(6+offset)*cos(angle), z=((i/200)*0.1)-offset/2, b=(offset/1.5)*30, c=angle*360/tau))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=0 (blue) to B=45 (red)\n", + " step.color = [((step.b+30)/60), 0, 1-((step.b+30)/60)]\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=8.75), label='color indicates B axis (tilt)'))\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=7.5), label='-30 deg (blue) to +30 deg (red)'))\n", + "gcode = fc5.transform(steps,'gcode')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", + "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual', hide_axes=False, zoom=0.75))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### use 3-axis geometry functions from FullControl (with caution!)\n", + "\n", + "this functionality should be considered experimental at best!\n", + "\n", + "geometry functions that generate 3-axis points can be used - accessed via fc5.xyz_geom()\n", + "\n", + "but they must be translated to have 5-axis methods for gcode generation - achieved via fc5.xyz_add_bc()\n", + "\n", + "this conversion does not set any values of B or C attributes for those points - the BC values will remain at whatever values they were in the ***design*** before the list of converted points\n", + "\n", + "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 90\n", + "\n", + "therefore, the 3D printer actually prints a circle in the YZ plane since the model coordinate system has been rotated by 90 degrees about the B axis\n", + "\n", + "hence, when the ***design*** is transformed to a 'gcode' ***result***, Y and Z values vary in gcode while X is constant (of course this would not print well - it's just for simple demonstration)\n", + "\n", + "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates because 5-axis plots in the 3D-printer's coordinates system often make no sense visually" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc5.Point(x=10, y=0, z=0, b=90, c=0))\n", + "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, 16)\n", + "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", + "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc5.xyz_geom'))\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=3.5), label='but points must be converted to 5-axis variants via fc5.xyz_add_bc'))\n", + "print(fc5.transform(steps, 'gcode'))\n", + "fc5.transform(steps, 'plot')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### bc_intercept\n", + "\n", + "if the machine's coordinate system is **not** set up so that the b and c axes intercept at the point x=0, y=0, z=0, the bc_intercept data can be provided in a GcodeControls object to ensure correct gcode generation\n", + "\n", + "the GcodeControls object has slightly less functionality for 5-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n", + "\n", + "note that although the system does not need the b and c axes to intercept at the point x=0, y=0, z=0, the model coordinate system must still be implemented such that the point x=0, y=0, z=0 represents the intercept point of b and c axes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gcode_controls = fc5.GcodeControls(bc_intercept = fc5.Point(x=10, y=0, z=0), initialization_data={'nozzle_temp': 250})\n", + "steps=[]\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point (x=0 in the model but x=10 in gcode due to the bc_intercept being at x=10)'))\n", + "steps.append(fc5.Point(x=1))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", + "steps.append(fc5.Point(b=45))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", + "print(fc5.transform(steps, 'gcode', gcode_controls))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### rotating-nozzle 5-axis system\n", + "\n", + "if the nozzle rotates about the Y axis, as opposed to the rotating bed tilting about the Y axis (as was the case for the code above), but the print bed still rotates about the Z axis, you can import `lab.fullcontrol.fiveaxisC0B1` as fc5 instead of ` lab.fullcontrol.fiveaxis`\n", + "\n", + "this is shown in the code cell below. note that the new import statement means the previous import of fc5 is nullified, so don't try to run the above code cells after running the next code cell or they won't work\n", + "\n", + "the simple instructions below show how rotation of the bed and of the nozzle independently result in necessary changes to X and Y in gcode\n", + "\n", + "in the future, the way of defining which type of multiaxis printer you change will be made more intuitive and procedural, but this works for now." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.fiveaxisC0B1 as fc5\n", + "b_offset_z = 40\n", + "steps=[]\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", + "steps.extend([fc5.Point(x=1), fc5.GcodeComment(end_of_previous_line_text='x=1')])\n", + "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", + "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", + "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", + "steps.extend([fc5.Point(x=0, y=1, c=0), fc5.GcodeComment(end_of_previous_line_text='x=0, y=1, c=0')])\n", + "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", + "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", + "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", + "steps.extend([fc5.Point(b=90), fc5.GcodeComment(end_of_previous_line_text='b=90')])\n", + "steps.extend([fc5.Point(b=-90), fc5.GcodeComment(end_of_previous_line_text='b=-90')])\n", + "print(fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "to keep the nozzle directly to the hand side of the bed (Y=0) for every point, which is useful for nozzle tilting about Y when printing a funnel for example, you need to design C to rotate for each point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import degrees\n", + "\n", + "circle_segments = 16\n", + "points_per_circle = circle_segments+1\n", + "steps = []\n", + "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, circle_segments)\n", + "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", + "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", + "steps[0].b, steps[0].c = 0.0, 0.0\n", + "gcode_without_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", + "fc5.transform(steps, 'plot')\n", + "\n", + "for i in range(len(steps)): steps[i].c = -360/circle_segments*i\n", + "# instead of the above for loop, you can use the following function to constantly vary c automatically. This is good for more complex geometry, where c cannot be 'designed' easily.\n", + "# steps = fclab.constant_polar_angle_with_c(points=steps, centre=fc5.Point(x=0, y=0, z=0), initial_c=-90)\n", + "gcode_with_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", + "\n", + "print(gcode_without_c_rotation +\n", + " '\\n\\n\\ngcode with C rotation to keep nozzle directly in +X direction from bed centre:\\n\\n' + gcode_with_c_rotation)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### next steps\n", + "\n", + "this tutorial notebook gives a brief introduction to five-axis use of FullControl for interest, but it is not an expansive implementation. it is included as an initial step towards translating in-house research for 5-axis gcode generation into a more general format compatible with the overall FullControl concept" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + }, + "vscode": { + "interpreter": { + "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/tutorials/colab/lab_four_axis_demo_colab.ipynb b/tutorials/colab/lab_four_axis_demo_colab.ipynb index 8546478e..ccd83ee9 100644 --- a/tutorials/colab/lab_four_axis_demo_colab.ipynb +++ b/tutorials/colab/lab_four_axis_demo_colab.ipynb @@ -1,259 +1,266 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# lab four-axis demo\n", - "\n", - "this documentation gives a brief overview of 4-axis capabilities - it will be expanded in the future\n", - "\n", - "it currently works for a system with a nozzle rotating about the y axis, for which open-source documentation will be released soon\n", - "\n", - "the generated gcode would work on other 4-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", - "\n", - "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", - "[link](https://www.google.com/search?q=ipynb+tutorial),\n", - "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", - "[link](https://colab.research.google.com/)*>\n", - "\n", - "*run all cells in this notebook in order (keep pressing shift+enter)*" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### four axis import" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.fouraxis as fc4\nfrom google.colab import files" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### basic demo\n", - "\n", - "points in fullcontrol are designed in the model's XYZ coordinate system\n", - "\n", - "rotation of the b axis will cause the nozzle to move in the x and z directions, and the amount that it moves depends on how far the tip of the nozzle is away from the axis of rotation. therefore it is important to set this distance with `GcodeControls(b_offset_z=...)` to allow fullcontrol to determine the correct x z values to send to the printer\n", - "\n", - "if the nozzle is below the axis of rotation b_offset_z should be positive\n", - "\n", - "there is also the potential for the nozzle to be offset from the axis of rotation in the x direction when it is vertical (b=0). this is not currently programmed in fullcontrol but will be in the future and will be set by the user with `b_offset_x`\n", - "\n", - "the GcodeControls object has slightly less functionality for 4-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "b_offset_z = 46.0 # mm" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='start point'))\n", - "steps.append(fc4.Point(x=1))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='set x=1 - gcode for this is simple... just move in x'))\n", - "steps.append(fc4.Point(b=60))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", - "steps.append(fc4.Point(b=90))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set b=90 - although x and z change, the nozzle tip doesn't move (hence E=0)\"))\n", - "steps.append(fc4.Point(z=1))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set z=1 - just like the x-movement above, this z-movement is simple. it's only changes to nozzle angle that affect other axes\"))\n", - "steps.append(fc4.Point(b=-90))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=-90 - the print head moves to the opposite side when the nozzle rotates 180 degrees to ensure the nozzle stays at x=1'))\n", - "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z)))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### add custom color to preview axes\n", - "\n", - "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", - "\n", - "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps = []\n", - "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", - "steps.append(fc4.PlotAnnotation(label='B0'))\n", - "steps.append(fc4.Point(x=10, y=5, z=0, b=0))\n", - "steps.append(fc4.PlotAnnotation(label='B0'))\n", - "steps.append(fc4.Point(y=10, z=0, b=-180))\n", - "steps.append(fc4.PlotAnnotation(label='B-45'))\n", - "steps.append(fc4.Point(x=0, y=15, b=-180))\n", - "steps.append(fc4.PlotAnnotation(label='B-45'))\n", - "steps.append(fc4.Point(y=20, b=180))\n", - "steps.append(fc4.PlotAnnotation(label='B+45'))\n", - "steps.append(fc4.Point(x=10, y=25, b=180))\n", - "steps.append(fc4.PlotAnnotation(label='B+45'))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", - " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", - "fc4.transform(steps, 'plot', fc4.PlotControls(color_type='manual'))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### a more complex color example\n", - "\n", - "this example shows a wavey helical print path, where the tilts to easy side (oscialtes once per layer)\n", - "\n", - "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from math import sin, cos, tau\n", - "EH = 0.4\n", - "EW = 1.2\n", - "\n", - "rad = 12 # nominal radius of structure before offsets\n", - "max_offset = rad\n", - "\n", - "start_x, start_y = 75, 75\n", - "initial_z = 0.5*EH\n", - "\n", - "steps = []\n", - "segs, segs_per_layer = 10000, 200\n", - "max_z = (segs/segs_per_layer)*EH\n", - "\n", - "for i in range(segs+1):\n", - " angle = tau*i/segs_per_layer\n", - " offset = (max_offset*(i/segs)**2)*(0.5+0.5*cos(angle*2))\n", - " steps.append(fc4.Point(x=start_x+(rad+offset)*cos(angle), y=start_y+(rad+offset)*sin(angle),\n", - " z=initial_z+((i/segs_per_layer)*EH)-offset/2, b=cos(angle)*(offset/max_offset)*45))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=-45 (blue) to B=45 (red)\n", - " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", - " x=start_x, y=start_y, z=max_z*1.2), label='color indicates B axis (tilt)'))\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", - " x=start_x, y=start_y, z=max_z), label='-45 deg (blue) to +45 deg (red)'))\n", - "gcode = fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z, initialization_data={\n", - " 'print_speed': 500, 'extrusion_width': EW, 'extrusion_height': EH}))\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", - "fc4.transform(steps, 'plot', fc4.PlotControls(\n", - " color_type='manual', hide_axes=False, zoom=0.75))\n", - "\n", - "design_name = 'fouraxis'\n", - "open(f'{design_name}.gcode', 'w').write(gcode)\n", - "\n", - "# activate the next line to download the gcode if using google colab\n", - "# files.download(f'{design_name}.gcode')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### use 3-axis geometry functions from FullControl (with caution!)\n", - "\n", - "this functionality should be considered experimental at best!\n", - "\n", - "geometry functions that generate 3-axis points can be used - accessed via fc4.xyz_geom()\n", - "\n", - "but they must be translated to have 4-axis methods for gcode generation - achieved via fc4.xyz_add_b()\n", - "\n", - "this conversion does not set any values of B attributes for those points - the B values will remain at whatever values they were in the ***design*** before the list of converted points\n", - "\n", - "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 45\n", - "\n", - "hence, when the ***design*** is transformed to a 'gcode' ***result***, X and Z values vary from the design X Z values in gcode to accomodate the true required position of the printhead (to get the desired nozzle location)\n", - "\n", - "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates (e.g. Z=0) because 4-axis plots in the 3D-printer's coordinates system often make no sense visually" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc4.Point(x=10, y=0, z=0, b=45))\n", - "xyz_geometry_steps = fc4.xyz_geom.circleXY(fc4.Point(x=0, y=0, z=0), 10, 0, 16)\n", - "xyz_geometry_steps_with_bc_capabilities = fc4.xyz_add_b(xyz_geometry_steps)\n", - "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc4.xyz_geom'))\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=3.5), label='but points must be converted to 4-axis variants via fc4.xyz_add_b'))\n", - "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=30)))\n", - "fc4.transform(steps, 'plot')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "fc", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - }, - "vscode": { - "interpreter": { - "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# lab four-axis demo\n", + "\n", + "this documentation gives a brief overview of 4-axis capabilities - it will be expanded in the future\n", + "\n", + "it currently works for a system with a nozzle rotating about the y axis, for which open-source documentation will be released soon\n", + "\n", + "the generated gcode would work on other 4-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", + "\n", + "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", + "[link](https://www.google.com/search?q=ipynb+tutorial),\n", + "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", + "[link](https://colab.research.google.com/)*>\n", + "\n", + "*run all cells in this notebook in order (keep pressing shift+enter)*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Fixed-axis note: this tutorial is retained as a reference for the older fixed-axis lab wrapper. New work may be better served by `lab.fullcontrol.infinaxis`, which is intended to express equivalent dynamic axis setups. Exact gcode equivalence is still being checked.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### four axis import" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.fouraxis as fc4\nfrom google.colab import files" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### basic demo\n", + "\n", + "points in fullcontrol are designed in the model's XYZ coordinate system\n", + "\n", + "rotation of the b axis will cause the nozzle to move in the x and z directions, and the amount that it moves depends on how far the tip of the nozzle is away from the axis of rotation. therefore it is important to set this distance with `GcodeControls(b_offset_z=...)` to allow fullcontrol to determine the correct x z values to send to the printer\n", + "\n", + "if the nozzle is below the axis of rotation b_offset_z should be positive\n", + "\n", + "there is also the potential for the nozzle to be offset from the axis of rotation in the x direction when it is vertical (b=0). this is not currently programmed in fullcontrol but will be in the future and will be set by the user with `b_offset_x`\n", + "\n", + "the GcodeControls object has slightly less functionality for 4-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b_offset_z = 46.0 # mm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='start point'))\n", + "steps.append(fc4.Point(x=1))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='set x=1 - gcode for this is simple... just move in x'))\n", + "steps.append(fc4.Point(b=60))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", + "steps.append(fc4.Point(b=90))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set b=90 - although x and z change, the nozzle tip doesn't move (hence E=0)\"))\n", + "steps.append(fc4.Point(z=1))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set z=1 - just like the x-movement above, this z-movement is simple. it's only changes to nozzle angle that affect other axes\"))\n", + "steps.append(fc4.Point(b=-90))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=-90 - the print head moves to the opposite side when the nozzle rotates 180 degrees to ensure the nozzle stays at x=1'))\n", + "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z)))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### add custom color to preview axes\n", + "\n", + "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", + "\n", + "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps = []\n", + "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", + "steps.append(fc4.PlotAnnotation(label='B0'))\n", + "steps.append(fc4.Point(x=10, y=5, z=0, b=0))\n", + "steps.append(fc4.PlotAnnotation(label='B0'))\n", + "steps.append(fc4.Point(y=10, z=0, b=-180))\n", + "steps.append(fc4.PlotAnnotation(label='B-45'))\n", + "steps.append(fc4.Point(x=0, y=15, b=-180))\n", + "steps.append(fc4.PlotAnnotation(label='B-45'))\n", + "steps.append(fc4.Point(y=20, b=180))\n", + "steps.append(fc4.PlotAnnotation(label='B+45'))\n", + "steps.append(fc4.Point(x=10, y=25, b=180))\n", + "steps.append(fc4.PlotAnnotation(label='B+45'))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", + " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", + "fc4.transform(steps, 'plot', fc4.PlotControls(color_type='manual'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### a more complex color example\n", + "\n", + "this example shows a wavey helical print path, where the tilts to easy side (oscialtes once per layer)\n", + "\n", + "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sin, cos, tau\n", + "EH = 0.4\n", + "EW = 1.2\n", + "\n", + "rad = 12 # nominal radius of structure before offsets\n", + "max_offset = rad\n", + "\n", + "start_x, start_y = 75, 75\n", + "initial_z = 0.5*EH\n", + "\n", + "steps = []\n", + "segs, segs_per_layer = 10000, 200\n", + "max_z = (segs/segs_per_layer)*EH\n", + "\n", + "for i in range(segs+1):\n", + " angle = tau*i/segs_per_layer\n", + " offset = (max_offset*(i/segs)**2)*(0.5+0.5*cos(angle*2))\n", + " steps.append(fc4.Point(x=start_x+(rad+offset)*cos(angle), y=start_y+(rad+offset)*sin(angle),\n", + " z=initial_z+((i/segs_per_layer)*EH)-offset/2, b=cos(angle)*(offset/max_offset)*45))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=-45 (blue) to B=45 (red)\n", + " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", + " x=start_x, y=start_y, z=max_z*1.2), label='color indicates B axis (tilt)'))\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", + " x=start_x, y=start_y, z=max_z), label='-45 deg (blue) to +45 deg (red)'))\n", + "gcode = fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z, initialization_data={\n", + " 'print_speed': 500, 'extrusion_width': EW, 'extrusion_height': EH}))\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", + "fc4.transform(steps, 'plot', fc4.PlotControls(\n", + " color_type='manual', hide_axes=False, zoom=0.75))\n", + "\n", + "design_name = 'fouraxis'\n", + "open(f'{design_name}.gcode', 'w').write(gcode)\n", + "\n", + "# activate the next line to download the gcode if using google colab\n", + "# files.download(f'{design_name}.gcode')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### use 3-axis geometry functions from FullControl (with caution!)\n", + "\n", + "this functionality should be considered experimental at best!\n", + "\n", + "geometry functions that generate 3-axis points can be used - accessed via fc4.xyz_geom()\n", + "\n", + "but they must be translated to have 4-axis methods for gcode generation - achieved via fc4.xyz_add_b()\n", + "\n", + "this conversion does not set any values of B attributes for those points - the B values will remain at whatever values they were in the ***design*** before the list of converted points\n", + "\n", + "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 45\n", + "\n", + "hence, when the ***design*** is transformed to a 'gcode' ***result***, X and Z values vary from the design X Z values in gcode to accomodate the true required position of the printhead (to get the desired nozzle location)\n", + "\n", + "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates (e.g. Z=0) because 4-axis plots in the 3D-printer's coordinates system often make no sense visually" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc4.Point(x=10, y=0, z=0, b=45))\n", + "xyz_geometry_steps = fc4.xyz_geom.circleXY(fc4.Point(x=0, y=0, z=0), 10, 0, 16)\n", + "xyz_geometry_steps_with_bc_capabilities = fc4.xyz_add_b(xyz_geometry_steps)\n", + "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc4.xyz_geom'))\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=3.5), label='but points must be converted to 4-axis variants via fc4.xyz_add_b'))\n", + "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=30)))\n", + "fc4.transform(steps, 'plot')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + }, + "vscode": { + "interpreter": { + "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/tutorials/colab/lab_infaxis_5_demo_colab.ipynb b/tutorials/colab/lab_infaxis_5_demo_colab.ipynb deleted file mode 100644 index 25c8805e..00000000 --- a/tutorials/colab/lab_infaxis_5_demo_colab.ipynb +++ /dev/null @@ -1,152 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "88f67711", - "metadata": {}, - "outputs": [], - "source": [ - "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infaxis as fci\n", - "import fullcontrol as fc\n", - "from math import sin, cos, tau\n", - "\n", - "fc.Point = fci.Point\n", - "fc.GcodeControls = fci.GcodeControls\n", - "fc.transform = fci.transform\n", - "fc.Axis = fci.Axis" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0d54e7f4", - "metadata": {}, - "outputs": [], - "source": [ - "EW = 0.6\n", - "EH = 0.3\n", - "\n", - "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", - "gcode_controls = fc.GcodeControls(\n", - " head_chain = [fc.Axis(name='B')],\n", - " bed_chain = [fc.Axis(name='C')],\n", - " distance_axis = True,\n", - " verbose = True,\n", - " initialization_data=print_settings \n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fa20bc2b", - "metadata": {}, - "outputs": [], - "source": [ - "def vase_from_trace(trace_points,density):\n", - " steps = []\n", - " for i in range(len(trace_points)-1):\n", - " p = trace_points[i]\n", - " p_next = trace_points[i+1]\n", - " r = (p.x**2 + p.y**2)**0.5\n", - " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", - " dr = r_next - r\n", - " dz = p_next.z-p.z\n", - " ptilt = p.axes['B']\n", - " dtilt = p_next.axes['B']-ptilt\n", - " \n", - " dc = p_next.axes['C']-p.axes['C']\n", - " for j in range(density):\n", - " angle = p.axes['C'] + dc * j / density\n", - " tilt = ptilt + dtilt * j / density\n", - " r_final = r + dr * j / density\n", - " z_final = p.z + dz * j / density\n", - " \n", - " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", - " \n", - " return steps\n", - "\n", - "density = 360\n", - "r_start = 10\n", - "r_tilt = 5 # 10 layers\n", - "tilt_start = 0\n", - "tilt_end = 90\n", - "h = EH # height of each layer\n", - "z_start = h*0.5 # starting z height\n", - "layers = 20 # number of layers in the z direction for first segment\n", - "arc_layers = int((r_tilt * (tilt_end - tilt_start) / 360 *tau)/h)\n", - "d_tilted = 5\n", - "layers_tilted = int(d_tilted/h)\n", - "\n", - "trace = []\n", - "\n", - "for i in range(layers):\n", - " r = r_start\n", - " tilt = tilt_start\n", - " angle = i * 360\n", - " z = z_start + h * i\n", - "\n", - " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", - "\n", - "angle_offset = fc.last_point(trace).axes['C']\n", - "z_offset = fc.last_point(trace).z\n", - "\n", - "for i in range(1,arc_layers):\n", - " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", - " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", - " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", - " angle = i * 360 + angle_offset\n", - " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", - "\n", - "angle_offset = fc.last_point(trace).axes['C']\n", - "z_offset = fc.last_point(trace).z\n", - "r_offset = fc.last_point(trace).x\n", - "\n", - "for i in range(1,layers_tilted):\n", - " tilt = tilt_end\n", - " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", - " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", - " angle = i * 360 + angle_offset\n", - " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", - "\n", - "steps = vase_from_trace(trace, density)\n", - "steps.append(fc.last_point(trace))\n", - "\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from A=0 (blue) to A=90 (red)\n", - " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", - "\n", - "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", - "fig.show()\n", - "\n", - "gcode = fc.transform(steps,'gcode',gcode_controls)\n", - "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", - "print('')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "fiveaxis (3.12.12)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/tutorials/contents.ipynb b/tutorials/contents.ipynb index 83ba08b5..9b70140f 100644 --- a/tutorials/contents.ipynb +++ b/tutorials/contents.ipynb @@ -42,6 +42,13 @@ "1. stl output - [lab_stl_output.ipynb](lab_stl_output.ipynb)\n", "1. 3mf output - [lab_3mf_output.ipynb](lab_3mf_output.ipynb)\n", "\n", + "#### FullControl infinaxis:\n", + "1. infinaxis 4-axis example - [infinaxis_4axis_demo.ipynb](infinaxis_4axis_demo.ipynb)\n", + "1. infinaxis 5-axis example - [infinaxis_5axis_demo.ipynb](infinaxis_5axis_demo.ipynb)\n", + "1. infinaxis controls - [infinaxis_controls.ipynb](infinaxis_controls.ipynb)\n", + "1. infinaxis custom axes - [infinaxis_custom_axes.ipynb](infinaxis_custom_axes.ipynb)\n", + "1. infinaxis XYZ geometry - [infinaxis_xyz_geom.ipynb](infinaxis_xyz_geom.ipynb)\n", + "\n", "See the [FullControl github repository](https://github.com/FullControlXYZ/fullcontrol#readme)" ] } diff --git a/tutorials/lab_infaxis_4_demo.ipynb b/tutorials/infinaxis_4axis_demo.ipynb similarity index 64% rename from tutorials/lab_infaxis_4_demo.ipynb rename to tutorials/infinaxis_4axis_demo.ipynb index 1b575ecd..ef5a7944 100644 --- a/tutorials/lab_infaxis_4_demo.ipynb +++ b/tutorials/infinaxis_4axis_demo.ipynb @@ -7,14 +7,8 @@ "metadata": {}, "outputs": [], "source": [ - "import lab.fullcontrol.infaxis as fci\n", - "import fullcontrol as fc\n", - "from math import sin, cos, tau\n", - "\n", - "fc.Point = fci.Point\n", - "fc.GcodeControls = fci.GcodeControls\n", - "fc.transform = fci.transform\n", - "fc.Axis = fci.Axis" + "import lab.fullcontrol.infinaxis as fci\n", + "from math import sin, cos, tau\n" ] }, { @@ -28,9 +22,13 @@ "EH = 0.3\n", "\n", "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", - "gcode_controls = fc.GcodeControls(\n", - " head_chain = [],\n", - " bed_chain = [fc.Axis(name='C')],\n", + "head_chain = []\n", + "bed_chain = [fci.Axis(name='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain = head_chain,\n", + " bed_chain = bed_chain,\n", " initialization_data=print_settings \n", " )" ] @@ -50,20 +48,20 @@ "\n", "\n", "steps = []\n", - "steps.append(fc.Printer(print_speed=2160))\n", + "steps.append(fci.Printer(print_speed=2160))\n", "for i in range(layers):\n", " for j in range(density):\n", " angle = 360*(i+j/density)\n", - " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", + " steps.append(Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, c=angle))\n", "\n", "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", + " if isinstance(step, fci.Point):\n", " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", - " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", + " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", "\n", - "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", "fig.show()\n", - "gcode = fc.transform(steps,'gcode',gcode_controls)\n", + "gcode = fci.transform(steps,'gcode',gcode_controls)\n", "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", "print('')\n", "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" @@ -72,7 +70,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "fc_dev (3.12.3)", "language": "python", "name": "python3" }, @@ -86,7 +84,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.11" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/tutorials/infinaxis_5axis_demo.ipynb b/tutorials/infinaxis_5axis_demo.ipynb new file mode 100644 index 00000000..4f2364d8 --- /dev/null +++ b/tutorials/infinaxis_5axis_demo.ipynb @@ -0,0 +1,206 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "88f67711", + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.infinaxis as fci\n", + "from math import sin, cos, tau\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d54e7f4", + "metadata": {}, + "outputs": [], + "source": [ + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain = head_chain,\n", + " bed_chain = bed_chain,\n", + " initialization_data=print_settings \n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "ffe83b59", + "metadata": {}, + "source": [ + "### QUESITON: I've created a simpler version of the next code cell in a new code cell immediately after it. Check this is okay, then delete the more complex code cell just below this comment. The intention is that readers don't need to think about the geometry/python stuff too much and focus more on the fci-specific aspects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa20bc2b", + "metadata": {}, + "outputs": [], + "source": [ + "def vase_from_trace(trace_points,density):\n", + " steps = []\n", + " for i in range(len(trace_points)-1):\n", + " p = trace_points[i]\n", + " p_next = trace_points[i+1]\n", + " r = (p.x**2 + p.y**2)**0.5\n", + " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", + " dr = r_next - r\n", + " dz = p_next.z-p.z\n", + " ptilt = p.b\n", + " dtilt = p_next.b-ptilt\n", + " \n", + " dc = p_next.c-p.c\n", + " for j in range(density):\n", + " angle = p.c + dc * j / density\n", + " tilt = ptilt + dtilt * j / density\n", + " r_final = r + dr * j / density\n", + " z_final = p.z + dz * j / density\n", + " \n", + " steps.append(Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, b=tilt, c=angle))\n", + " \n", + " return steps\n", + "\n", + "density = 360\n", + "r_start = 10\n", + "r_tilt = 5 # 10 layers\n", + "tilt_start = 0\n", + "tilt_end = 90\n", + "h = EH # height of each layer\n", + "z_start = h*0.5 # starting z height\n", + "layers = 20 # number of layers in the z direction for first segment\n", + "arc_layers = int((r_tilt * (tilt_end - tilt_start) / 360 *tau)/h)\n", + "d_tilted = 5\n", + "layers_tilted = int(d_tilted/h)\n", + "\n", + "trace = []\n", + "\n", + "for i in range(layers):\n", + " r = r_start\n", + " tilt = tilt_start\n", + " angle = i * 360\n", + " z = z_start + h * i\n", + "\n", + " trace.append(Point(x=r, y=0, z=z, b=tilt, c=angle))\n", + "\n", + "angle_offset = fci.last_point(trace).c\n", + "z_offset = fci.last_point(trace).z\n", + "\n", + "for i in range(1,arc_layers):\n", + " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", + " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", + " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", + " angle = i * 360 + angle_offset\n", + " trace.append(Point(x=r, y=0, z=z, b=-tilt, c=angle))\n", + "\n", + "angle_offset = fci.last_point(trace).c\n", + "z_offset = fci.last_point(trace).z\n", + "r_offset = fci.last_point(trace).x\n", + "\n", + "for i in range(1,layers_tilted):\n", + " tilt = tilt_end\n", + " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", + " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", + " angle = i * 360 + angle_offset\n", + " trace.append(Point(x=0, y=r, z=z, b=-tilt, c=angle))\n", + "\n", + "steps = vase_from_trace(trace, density)\n", + "steps.append(fci.last_point(trace))\n", + "\n", + "for step in steps:\n", + " if isinstance(step, fci.Point):\n", + " # color is a gradient from A=0 (blue) to A=90 (red)\n", + " step.color = [((abs(step.b))/90), 0, 1-((abs(step.b))/90)]\n", + "\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", + "gcode = fci.transform(steps,'gcode',gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### same style of five-axis path with simpler direct construction\n", + "\n", + "This version uses one spiral turn per layer. `B` tilt increases by 1 degree per layer for 90 layers. The layer-to-layer spacing is kept constant by splitting the same step length into radial and vertical components with sine/cosine.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e73fc8bc", + "metadata": {}, + "outputs": [], + "source": [ + "# simpler direct version of the five-axis demo path\n", + "\n", + "density = 360\n", + "layers = 90\n", + "layer_gap = EH\n", + "r = 10\n", + "z = EH / 2\n", + "steps = []\n", + "\n", + "for layer in range(layers):\n", + " tilt = layer\n", + " dr = layer_gap * sin(tilt / 360 * tau)\n", + " dz = layer_gap * cos(tilt / 360 * tau)\n", + " for j in range(density):\n", + " t = j / density\n", + " angle = 360 * (layer + t)\n", + " r_now = r + dr * t\n", + " z_now = z + dz * t\n", + " steps.append(Point(x=r_now * sin(angle / 360 * tau), y=r_now * cos(angle / 360 * tau), z=z_now, b=-tilt, c=angle))\n", + " r += dr\n", + " z += dz\n", + "\n", + "for step in steps:\n", + " step.color = [abs(step.b) / 90, 0, 1 - abs(step.b) / 90]\n", + "\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual', style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\n", + "\n", + "gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", + "print('')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fiveaxis (3.12.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/infinaxis_controls.ipynb b/tutorials/infinaxis_controls.ipynb new file mode 100644 index 00000000..1cea74d5 --- /dev/null +++ b/tutorials/infinaxis_controls.ipynb @@ -0,0 +1,372 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# infinaxis controls demo\n", + "\n", + "This notebook shows the main `Axis` and `GcodeControls` options that affect generated gcode.\n", + "\n", + "The next cell contains reusable functions that help keep subsequent cells concise" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.infinaxis as fci\n", + "\n", + "EW = 0.6\n", + "EH = 0.3\n", + "print_settings = {'extrusion_width': EW, 'extrusion_height': EH}\n", + "\n", + "base_steps_data = [\n", + " dict(x=0, y=10, z=EH / 2, b=0, c=0),\n", + " dict(x=4, y=10, z=EH / 2, b=10, c=20),\n", + " dict(x=8, y=12, z=EH, b=20, c=40),\n", + " dict(x=12, y=14, z=EH * 1.5, b=30, c=60),\n", + "]\n", + "\n", + "def build_steps(head_chain, bed_chain, steps_data=base_steps_data):\n", + " Point = fci.configure_point(head_chain, bed_chain)\n", + " return [Point(**step_data) for step_data in steps_data]\n", + "\n", + "def controls(head_chain, bed_chain, **kwargs):\n", + " return fci.GcodeControls(\n", + " head_chain=head_chain,\n", + " bed_chain=bed_chain,\n", + " initialization_data=print_settings,\n", + " **kwargs,\n", + " )\n", + "\n", + "def print_gcode_sample(label, steps, gcode_controls, look_for):\n", + " gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + " print('___\\n' + label)\n", + " print(f'Look for: {look_for}')\n", + " print('\\n'.join(gcode.split('\\n')[-8:]))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Baseline\n", + "\n", + "A normal two-rotary-axis example. `Axis.name` is the gcode name and, for standard `A/B/C`, also provides the default kinematic `type`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'baseline Axis(name=\"B\") + Axis(name=\"C\"):',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'B and C appear in each move; XYZ is inverse-kinematics output.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.name and Axis.type\n", + "\n", + "Use `name` for the emitted gcode axis and `type` for the kinematic behavior. This allows machine names like `CI` and `CII` to both behave as C-style rotary axes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='CII', type='C')]\n", + "bed_chain = [fci.Axis(name='CI', type='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "steps = [\n", + " Point(x=0, y=10, z=EH / 2, CI=0, CII=0),\n", + " Point(x=4, y=10, z=EH / 2, CI=20, CII=-10),\n", + " Point(x=8, y=12, z=EH, CI=40, CII=-20),\n", + "]\n", + "\n", + "print_gcode_sample(\n", + " 'custom axis names with shared C kinematics:',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'CI and CII are emitted, while type=\"C\" controls the rotation math.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.active\n", + "\n", + "`active` sets the starting position of an axis before any point explicitly changes it.\n", + "\n", + "### QUESTION: is 'active' necessary? It seems like you could do that by setting axis to have an initial value in the first Point. I assume this is actually to allow the axis to have a non-zero value when setting up subsequent axes after it in the same head or bed chain? If so, perhaps a better name than 'active' exists? Either way, give a clear description in code comments and in this demo notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = []\n", + "bed_chain = [fci.Axis(name='C', active=45)]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "steps = [\n", + " Point(x=0, y=10, z=EH / 2),\n", + " Point(x=4, y=10, z=EH / 2),\n", + " Point(x=8, y=12, z=EH, c=90),\n", + "]\n", + "\n", + "print_gcode_sample(\n", + " 'Axis(active=45) before points set c:',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'C starts at 45.0, then changes to 90.0 when the point sets c=90.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.orientation\n", + "\n", + "`orientation` flips the sign used by the kinematic calculation. The commanded axis value is still emitted with the configured `Axis.name`; the XYZ solution changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "orientation_steps_data = [\n", + " dict(x=0, y=10, z=EH / 2, c=0),\n", + " dict(x=4, y=10, z=EH / 2, c=20),\n", + " dict(x=8, y=12, z=EH, c=40),\n", + "]\n", + "\n", + "for orientation in [1, -1]:\n", + " head_chain = []\n", + " bed_chain = [fci.Axis(name='C', orientation=orientation)]\n", + " steps = build_steps(head_chain, bed_chain, orientation_steps_data)\n", + " print_gcode_sample(\n", + " f'Axis(name=\"C\", orientation={orientation}):',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'Compare XYZ values between orientation=1 and orientation=-1. C values remain commanded values.',\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Axis.offset\n", + "\n", + "`offset` describes the location of an axis relative to the previous axis in the chain. CHECK_THIS_DESCRIPTION: confirm exact sign convention for the target machine before using this on hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for offset in [fci.Point(x=0, y=0, z=0), fci.Point(x=5, y=0, z=0)]:\n", + " head_chain = [fci.Axis(name='B', offset=offset)]\n", + " bed_chain = [fci.Axis(name='C')]\n", + " steps = build_steps(head_chain, bed_chain)\n", + " print_gcode_sample(\n", + " f'Axis(name=\"B\", offset={offset}):',\n", + " steps,\n", + " controls(head_chain, bed_chain),\n", + " 'Compare XYZ values as the head rotary axis offset changes.',\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.verbose\n", + "\n", + "`verbose=True` adds distance diagnostics as gcode comments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "for verbose in [False, True]:\n", + " print_gcode_sample(\n", + " f'GcodeControls(verbose={verbose}):',\n", + " steps,\n", + " controls(head_chain, bed_chain, verbose=verbose),\n", + " 'Verbose output adds ; distance and system comments when enabled.',\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.distance_axis\n", + "\n", + "`distance_axis=True` emits a synthetic `U` value based on accumulated model/path distance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'GcodeControls(distance_axis=True):',\n", + " steps,\n", + " controls(head_chain, bed_chain, distance_axis=True),\n", + " 'U appears after B/C and increases as accumulated distance changes.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.model_XYZ_gcode\n", + "\n", + "`model_XYZ_gcode=True` emits model-space `U/V/W` values. CHECK_THIS_DESCRIPTION: confirm target firmware/motion-planner expectations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'GcodeControls(model_XYZ_gcode=True):',\n", + " steps,\n", + " controls(head_chain, bed_chain, model_XYZ_gcode=True),\n", + " 'U/V/W show the model-space x/y/z values alongside transformed XYZ/B/C.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.inverse_time_feedrate\n", + "\n", + "`inverse_time_feedrate=True` changes the F calculation for extrusion moves. CHECK_THIS_DESCRIPTION: verify units and firmware mode before using on a machine.\n", + "\n", + "### QUESTION: Don't we need to add an M command at the start of the gcode if this is set to true? That should be automatically added (like M83)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'GcodeControls(inverse_time_feedrate=True):',\n", + " steps,\n", + " controls(head_chain, bed_chain, inverse_time_feedrate=True),\n", + " 'F values differ from the baseline calculation.',\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GcodeControls.xyz_orientation\n", + "\n", + "`xyz_orientation` flips output signs for system XYZ axes. CHECK_THIS_DESCRIPTION: confirm this is the intended way to handle machine axis direction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "head_chain = [fci.Axis(name='B')]\n", + "bed_chain = [fci.Axis(name='C')]\n", + "steps = build_steps(head_chain, bed_chain)\n", + "\n", + "print_gcode_sample(\n", + " 'Baseline output for regular orientation, GcodeControls(xyz_orientation=[1, 1, 1]):',\n", + " steps,\n", + " controls(head_chain, bed_chain, xyz_orientation=[1, 1, 1]),\n", + " '',\n", + ")\n", + "\n", + "print_gcode_sample(\n", + " 'X orientation flipped, GcodeControls(xyz_orientation=[-1, 1, 1]):',\n", + " steps,\n", + " controls(head_chain, bed_chain, xyz_orientation=[-1, 1, 1]),\n", + " 'X output changes sign compared with the baseline.',\n", + ")\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/infinaxis_custom_axes.ipynb b/tutorials/infinaxis_custom_axes.ipynb new file mode 100644 index 00000000..4a4d3cf1 --- /dev/null +++ b/tutorials/infinaxis_custom_axes.ipynb @@ -0,0 +1,73 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9cad98ab", + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.infinaxis as fci\n", + "\n", + "head_chain = [fci.Axis(name='CI', type='C')]\n", + "bed_chain = [fci.Axis(name='CII', type='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain=head_chain,\n", + " bed_chain=bed_chain,\n", + " initialization_data={'extrusion_width': 0.6, 'extrusion_height': 0.3},\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ddc4461", + "metadata": {}, + "outputs": [], + "source": [ + "steps = [\n", + " Point(x=0, y=0, z=0.15, CI=0, CII=0),\n", + " Point(x=10, CI=30, CII=-15),\n", + " Point(y=10, CI=60, CII=-30),\n", + "]\n", + "\n", + "print(steps[1].axes)\n", + "print(steps[2].CI, steps[2].CII)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "656e27c8", + "metadata": {}, + "outputs": [], + "source": [ + "gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + "print('\\n'.join(gcode.split('\\n')[:8]))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc (3.12.3.final.0)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/infinaxis_xyz_geom.ipynb b/tutorials/infinaxis_xyz_geom.ipynb new file mode 100644 index 00000000..b5a2aba9 --- /dev/null +++ b/tutorials/infinaxis_xyz_geom.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.infinaxis as fci\n", + "\n", + "EW = 0.6\n", + "EH = 0.3\n", + "\n", + "head_chain = []\n", + "bed_chain = [fci.Axis(name='C')]\n", + "Point = fci.configure_point(head_chain, bed_chain)\n", + "\n", + "gcode_controls = fci.GcodeControls(\n", + " head_chain=head_chain,\n", + " bed_chain=bed_chain,\n", + " initialization_data={'extrusion_width': EW, 'extrusion_height': EH},\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "xyz_steps = fci.xyz_geom.circleXY(\n", + " fci.xyz_geom.Point(x=0, y=0, z=EH / 2),\n", + " radius=20,\n", + " start_angle=0,\n", + " segments=72,\n", + ")\n", + "\n", + "steps = fci.xyz_add_axes(xyz_steps, Point)\n", + "steps.insert(0, fci.Printer(print_speed=1200))\n", + "\n", + "for i, step in enumerate(steps):\n", + " if isinstance(step, fci.Point):\n", + " step.c = i * 5\n", + " step.color = [(step.c % 360) / 360, 0, 1 - ((step.c % 360) / 360)]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = fci.transform(\n", + " steps,\n", + " 'fig',\n", + " fci.PlotControls(color_type='manual', style='line', zoom=0.75),\n", + " show_tips=False,\n", + ")\n", + "fig.show()\n", + "\n", + "gcode = fci.transform(steps, 'gcode', gcode_controls)\n", + "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/lab_five_axis_demo.ipynb b/tutorials/lab_five_axis_demo.ipynb index 712975c5..2d99894e 100644 --- a/tutorials/lab_five_axis_demo.ipynb +++ b/tutorials/lab_five_axis_demo.ipynb @@ -1,342 +1,349 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# lab five-axis demo\n", - "\n", - "this documentation gives a brief overview of 5-axis capabilities - it will be expanded in the future\n", - "\n", - "most of this tutorial relates to a system with B-C rotation stages, where C-axis rotations do not affect the B axis, but B-axis rotations do alter the C axis orientation (i.e. a rotating stage [C] mounted onto a tilting platform [B])\n", - "\n", - "the final section of this notebook demosntrates how to generate gcode for a system with a rotating nozzle (B) and rotating bed (C) \n", - "\n", - "the generated gcode would work on other 5-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", - "\n", - "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", - "[link](https://www.google.com/search?q=ipynb+tutorial),\n", - "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", - "[link](https://colab.research.google.com/)*>\n", - "\n", - "*run all cells in this notebook in order (keep pressing shift+enter)*" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### five axis import" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import lab.fullcontrol.fiveaxis as fc5\n", - "import fullcontrol as fc\n", - "import lab.fullcontrol as fclab" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### basic demo\n", - "\n", - "points are designed in the model's XYZ coordinate system\n", - "\n", - "the point x=0, y=0, z=0 in the model's coordinate system represents the intercept point of B and C axes\n", - "\n", - "FullControl translates them to the 3D printer XYZ coordinates, factoring in the effect of rotations to B and C axes\n", - "\n", - "a full explanation of this concept is out of scope for this brief tutorial notebook - google 5-axis kinematics for more info\n", - "\n", - "however, the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", - "steps.append(fc5.Point(x=1))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", - "steps.append(fc5.Point(b=45))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", - "steps.append(fc5.Point(b=90))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=90 - although x and z change, E=0 because the nozzle stays in the same point on the model'))\n", - "steps.append(fc5.Point(b=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=0'))\n", - "steps.append(fc5.Point(c=90))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set c=90 - this causes a change to x and y in system coordinates'))\n", - "steps.append(fc5.Point(y=1))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set y=1 - this causes a change to x in system coordinates since the model is rotated 90 degrees'))\n", - "print(fc5.transform(steps, 'gcode'))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### add custom color to preview axes\n", - "\n", - "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", - "\n", - "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps = []\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B0'))\n", - "steps.append(fc5.Point(x=10, y=5, z=0, b=0, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B0'))\n", - "steps.append(fc5.Point(y=10, z=0, b=-180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B-180'))\n", - "steps.append(fc5.Point(x=0, y=15, b=-180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B-180'))\n", - "steps.append(fc5.Point(y=20, b=180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B+180'))\n", - "steps.append(fc5.Point(x=10, y=25, b=180, c=0))\n", - "steps.append(fc5.PlotAnnotation(label='B+180'))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", - " step.color = [((step.b+180)/360), 0, 1-((step.b+180)/360)]\n", - "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual'))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### a more complex color example\n", - "\n", - "this example shows a wavey helical print path, where the model is continuously rotating while the nozzle gradually moves away from the print platform\n", - "\n", - "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from math import sin, cos, tau\n", - "steps = []\n", - "for i in range(10001):\n", - " angle = tau*i/200\n", - " offset = (1.5*(i/10000)**2)*cos(angle*6)\n", - " steps.append(fc5.Point(x=(6+offset)*sin(angle), y=(6+offset)*cos(angle), z=((i/200)*0.1)-offset/2, b=(offset/1.5)*30, c=angle*360/tau))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=0 (blue) to B=45 (red)\n", - " step.color = [((step.b+30)/60), 0, 1-((step.b+30)/60)]\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=8.75), label='color indicates B axis (tilt)'))\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=7.5), label='-30 deg (blue) to +30 deg (red)'))\n", - "gcode = fc5.transform(steps,'gcode')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", - "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual', hide_axes=False, zoom=0.75))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### use 3-axis geometry functions from FullControl (with caution!)\n", - "\n", - "this functionality should be considered experimental at best!\n", - "\n", - "geometry functions that generate 3-axis points can be used - accessed via fc5.xyz_geom()\n", - "\n", - "but they must be translated to have 5-axis methods for gcode generation - achieved via fc5.xyz_add_bc()\n", - "\n", - "this conversion does not set any values of B or C attributes for those points - the BC values will remain at whatever values they were in the ***design*** before the list of converted points\n", - "\n", - "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 90\n", - "\n", - "therefore, the 3D printer actually prints a circle in the YZ plane since the model coordinate system has been rotated by 90 degrees about the B axis\n", - "\n", - "hence, when the ***design*** is transformed to a 'gcode' ***result***, Y and Z values vary in gcode while X is constant (of course this would not print well - it's just for simple demonstration)\n", - "\n", - "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates because 5-axis plots in the 3D-printer's coordinates system often make no sense visually" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc5.Point(x=10, y=0, z=0, b=90, c=0))\n", - "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, 16)\n", - "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", - "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc5.xyz_geom'))\n", - "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=3.5), label='but points must be converted to 5-axis variants via fc5.xyz_add_bc'))\n", - "print(fc5.transform(steps, 'gcode'))\n", - "fc5.transform(steps, 'plot')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### bc_intercept\n", - "\n", - "if the machine's coordinate system is **not** set up so that the b and c axes intercept at the point x=0, y=0, z=0, the bc_intercept data can be provided in a GcodeControls object to ensure correct gcode generation\n", - "\n", - "the GcodeControls object has slightly less functionality for 5-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n", - "\n", - "note that although the system does not need the b and c axes to intercept at the point x=0, y=0, z=0, the model coordinate system must still be implemented such that the point x=0, y=0, z=0 represents the intercept point of b and c axes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "gcode_controls = fc5.GcodeControls(bc_intercept = fc5.Point(x=10, y=0, z=0), initialization_data={'nozzle_temp': 250})\n", - "steps=[]\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point (x=0 in the model but x=10 in gcode due to the bc_intercept being at x=10)'))\n", - "steps.append(fc5.Point(x=1))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", - "steps.append(fc5.Point(b=45))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", - "print(fc5.transform(steps, 'gcode', gcode_controls))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### rotating-nozzle 5-axis system\n", - "\n", - "if the nozzle rotates about the Y axis, as opposed to the rotating bed tilting about the Y axis (as was the case for the code above), but the print bed still rotates about the Z axis, you can import `lab.fullcontrol.fiveaxisC0B1` as fc5 instead of ` lab.fullcontrol.fiveaxis`\n", - "\n", - "this is shown in the code cell below. note that the new import statement means the previous import of fc5 is nullified, so don't try to run the above code cells after running the next code cell or they won't work\n", - "\n", - "the simple instructions below show how rotation of the bed and of the nozzle independently result in necessary changes to X and Y in gcode\n", - "\n", - "in the future, the way of defining which type of multiaxis printer you change will be made more intuitive and procedural, but this works for now." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import lab.fullcontrol.fiveaxisC0B1 as fc5\n", - "b_offset_z = 40\n", - "steps=[]\n", - "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", - "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", - "steps.extend([fc5.Point(x=1), fc5.GcodeComment(end_of_previous_line_text='x=1')])\n", - "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", - "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", - "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", - "steps.extend([fc5.Point(x=0, y=1, c=0), fc5.GcodeComment(end_of_previous_line_text='x=0, y=1, c=0')])\n", - "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", - "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", - "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", - "steps.extend([fc5.Point(b=90), fc5.GcodeComment(end_of_previous_line_text='b=90')])\n", - "steps.extend([fc5.Point(b=-90), fc5.GcodeComment(end_of_previous_line_text='b=-90')])\n", - "print(fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "to keep the nozzle directly to the hand side of the bed (Y=0) for every point, which is useful for nozzle tilting about Y when printing a funnel for example, you need to design C to rotate for each point" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from math import degrees\n", - "\n", - "circle_segments = 16\n", - "points_per_circle = circle_segments+1\n", - "steps = []\n", - "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, circle_segments)\n", - "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", - "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", - "steps[0].b, steps[0].c = 0.0, 0.0\n", - "gcode_without_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", - "fc5.transform(steps, 'plot')\n", - "\n", - "for i in range(len(steps)): steps[i].c = -360/circle_segments*i\n", - "# instead of the above for loop, you can use the following function to constantly vary c automatically. This is good for more complex geometry, where c cannot be 'designed' easily.\n", - "# steps = fclab.constant_polar_angle_with_c(points=steps, centre=fc5.Point(x=0, y=0, z=0), initial_c=-90)\n", - "gcode_with_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", - "\n", - "print(gcode_without_c_rotation +\n", - " '\\n\\n\\ngcode with C rotation to keep nozzle directly in +X direction from bed centre:\\n\\n' + gcode_with_c_rotation)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### next steps\n", - "\n", - "this tutorial notebook gives a brief introduction to five-axis use of FullControl for interest, but it is not an expansive implementation. it is included as an initial step towards translating in-house research for 5-axis gcode generation into a more general format compatible with the overall FullControl concept" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "fc", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - }, - "vscode": { - "interpreter": { - "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# lab five-axis demo\n", + "\n", + "this documentation gives a brief overview of 5-axis capabilities - it will be expanded in the future\n", + "\n", + "most of this tutorial relates to a system with B-C rotation stages, where C-axis rotations do not affect the B axis, but B-axis rotations do alter the C axis orientation (i.e. a rotating stage [C] mounted onto a tilting platform [B])\n", + "\n", + "the final section of this notebook demosntrates how to generate gcode for a system with a rotating nozzle (B) and rotating bed (C) \n", + "\n", + "the generated gcode would work on other 5-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", + "\n", + "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", + "[link](https://www.google.com/search?q=ipynb+tutorial),\n", + "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", + "[link](https://colab.research.google.com/)*>\n", + "\n", + "*run all cells in this notebook in order (keep pressing shift+enter)*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Fixed-axis note: this tutorial is retained as a reference for the older fixed-axis lab wrapper. New work may be better served by `lab.fullcontrol.infinaxis`, which is intended to express equivalent dynamic axis setups. Exact gcode equivalence is still being checked.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### five axis import" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.fiveaxis as fc5\n", + "import fullcontrol as fc\n", + "import lab.fullcontrol as fclab" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### basic demo\n", + "\n", + "points are designed in the model's XYZ coordinate system\n", + "\n", + "the point x=0, y=0, z=0 in the model's coordinate system represents the intercept point of B and C axes\n", + "\n", + "FullControl translates them to the 3D printer XYZ coordinates, factoring in the effect of rotations to B and C axes\n", + "\n", + "a full explanation of this concept is out of scope for this brief tutorial notebook - google 5-axis kinematics for more info\n", + "\n", + "however, the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", + "steps.append(fc5.Point(x=1))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", + "steps.append(fc5.Point(b=45))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", + "steps.append(fc5.Point(b=90))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=90 - although x and z change, E=0 because the nozzle stays in the same point on the model'))\n", + "steps.append(fc5.Point(b=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=0'))\n", + "steps.append(fc5.Point(c=90))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set c=90 - this causes a change to x and y in system coordinates'))\n", + "steps.append(fc5.Point(y=1))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set y=1 - this causes a change to x in system coordinates since the model is rotated 90 degrees'))\n", + "print(fc5.transform(steps, 'gcode'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### add custom color to preview axes\n", + "\n", + "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", + "\n", + "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps = []\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B0'))\n", + "steps.append(fc5.Point(x=10, y=5, z=0, b=0, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B0'))\n", + "steps.append(fc5.Point(y=10, z=0, b=-180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B-180'))\n", + "steps.append(fc5.Point(x=0, y=15, b=-180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B-180'))\n", + "steps.append(fc5.Point(y=20, b=180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B+180'))\n", + "steps.append(fc5.Point(x=10, y=25, b=180, c=0))\n", + "steps.append(fc5.PlotAnnotation(label='B+180'))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", + " step.color = [((step.b+180)/360), 0, 1-((step.b+180)/360)]\n", + "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### a more complex color example\n", + "\n", + "this example shows a wavey helical print path, where the model is continuously rotating while the nozzle gradually moves away from the print platform\n", + "\n", + "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sin, cos, tau\n", + "steps = []\n", + "for i in range(10001):\n", + " angle = tau*i/200\n", + " offset = (1.5*(i/10000)**2)*cos(angle*6)\n", + " steps.append(fc5.Point(x=(6+offset)*sin(angle), y=(6+offset)*cos(angle), z=((i/200)*0.1)-offset/2, b=(offset/1.5)*30, c=angle*360/tau))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=0 (blue) to B=45 (red)\n", + " step.color = [((step.b+30)/60), 0, 1-((step.b+30)/60)]\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=8.75), label='color indicates B axis (tilt)'))\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=7.5), label='-30 deg (blue) to +30 deg (red)'))\n", + "gcode = fc5.transform(steps,'gcode')\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", + "fc5.transform(steps, 'plot', fc5.PlotControls(color_type='manual', hide_axes=False, zoom=0.75))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### use 3-axis geometry functions from FullControl (with caution!)\n", + "\n", + "this functionality should be considered experimental at best!\n", + "\n", + "geometry functions that generate 3-axis points can be used - accessed via fc5.xyz_geom()\n", + "\n", + "but they must be translated to have 5-axis methods for gcode generation - achieved via fc5.xyz_add_bc()\n", + "\n", + "this conversion does not set any values of B or C attributes for those points - the BC values will remain at whatever values they were in the ***design*** before the list of converted points\n", + "\n", + "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 90\n", + "\n", + "therefore, the 3D printer actually prints a circle in the YZ plane since the model coordinate system has been rotated by 90 degrees about the B axis\n", + "\n", + "hence, when the ***design*** is transformed to a 'gcode' ***result***, Y and Z values vary in gcode while X is constant (of course this would not print well - it's just for simple demonstration)\n", + "\n", + "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates because 5-axis plots in the 3D-printer's coordinates system often make no sense visually" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc5.Point(x=10, y=0, z=0, b=90, c=0))\n", + "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, 16)\n", + "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", + "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc5.xyz_geom'))\n", + "steps.append(fc5.PlotAnnotation(point=fc5.Point(x=0, y=0, z=3.5), label='but points must be converted to 5-axis variants via fc5.xyz_add_bc'))\n", + "print(fc5.transform(steps, 'gcode'))\n", + "fc5.transform(steps, 'plot')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### bc_intercept\n", + "\n", + "if the machine's coordinate system is **not** set up so that the b and c axes intercept at the point x=0, y=0, z=0, the bc_intercept data can be provided in a GcodeControls object to ensure correct gcode generation\n", + "\n", + "the GcodeControls object has slightly less functionality for 5-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n", + "\n", + "note that although the system does not need the b and c axes to intercept at the point x=0, y=0, z=0, the model coordinate system must still be implemented such that the point x=0, y=0, z=0 represents the intercept point of b and c axes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gcode_controls = fc5.GcodeControls(bc_intercept = fc5.Point(x=10, y=0, z=0), initialization_data={'nozzle_temp': 250})\n", + "steps=[]\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point (x=0 in the model but x=10 in gcode due to the bc_intercept being at x=10)'))\n", + "steps.append(fc5.Point(x=1))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set x=1 - since b=0 and c=0, the model x-axis is oriented the same as the system x-axis'))\n", + "steps.append(fc5.Point(b=45))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", + "print(fc5.transform(steps, 'gcode', gcode_controls))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### rotating-nozzle 5-axis system\n", + "\n", + "if the nozzle rotates about the Y axis, as opposed to the rotating bed tilting about the Y axis (as was the case for the code above), but the print bed still rotates about the Z axis, you can import `lab.fullcontrol.fiveaxisC0B1` as fc5 instead of ` lab.fullcontrol.fiveaxis`\n", + "\n", + "this is shown in the code cell below. note that the new import statement means the previous import of fc5 is nullified, so don't try to run the above code cells after running the next code cell or they won't work\n", + "\n", + "the simple instructions below show how rotation of the bed and of the nozzle independently result in necessary changes to X and Y in gcode\n", + "\n", + "in the future, the way of defining which type of multiaxis printer you change will be made more intuitive and procedural, but this works for now." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.fiveaxisC0B1 as fc5\n", + "b_offset_z = 40\n", + "steps=[]\n", + "steps.append(fc5.Point(x=0, y=0, z=0, b=0, c=0))\n", + "steps.append(fc5.GcodeComment(end_of_previous_line_text='start point'))\n", + "steps.extend([fc5.Point(x=1), fc5.GcodeComment(end_of_previous_line_text='x=1')])\n", + "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", + "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", + "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", + "steps.extend([fc5.Point(x=0, y=1, c=0), fc5.GcodeComment(end_of_previous_line_text='x=0, y=1, c=0')])\n", + "steps.extend([fc5.Point(c=90), fc5.GcodeComment(end_of_previous_line_text='c=90')])\n", + "steps.extend([fc5.Point(c=180), fc5.GcodeComment(end_of_previous_line_text='c=180')])\n", + "steps.extend([fc5.Point(c=270), fc5.GcodeComment(end_of_previous_line_text='c=270')])\n", + "steps.extend([fc5.Point(b=90), fc5.GcodeComment(end_of_previous_line_text='b=90')])\n", + "steps.extend([fc5.Point(b=-90), fc5.GcodeComment(end_of_previous_line_text='b=-90')])\n", + "print(fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "to keep the nozzle directly to the hand side of the bed (Y=0) for every point, which is useful for nozzle tilting about Y when printing a funnel for example, you need to design C to rotate for each point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import degrees\n", + "\n", + "circle_segments = 16\n", + "points_per_circle = circle_segments+1\n", + "steps = []\n", + "xyz_geometry_steps = fc5.xyz_geom.circleXY(fc5.Point(x=0, y=0, z=0), 10, 0, circle_segments)\n", + "xyz_geometry_steps_with_bc_capabilities = fc5.xyz_add_bc(xyz_geometry_steps)\n", + "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", + "steps[0].b, steps[0].c = 0.0, 0.0\n", + "gcode_without_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", + "fc5.transform(steps, 'plot')\n", + "\n", + "for i in range(len(steps)): steps[i].c = -360/circle_segments*i\n", + "# instead of the above for loop, you can use the following function to constantly vary c automatically. This is good for more complex geometry, where c cannot be 'designed' easily.\n", + "# steps = fclab.constant_polar_angle_with_c(points=steps, centre=fc5.Point(x=0, y=0, z=0), initial_c=-90)\n", + "gcode_with_c_rotation = fc5.transform(steps, 'gcode', fc5.GcodeControls(b_offset_z=b_offset_z))\n", + "\n", + "print(gcode_without_c_rotation +\n", + " '\\n\\n\\ngcode with C rotation to keep nozzle directly in +X direction from bed centre:\\n\\n' + gcode_with_c_rotation)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### next steps\n", + "\n", + "this tutorial notebook gives a brief introduction to five-axis use of FullControl for interest, but it is not an expansive implementation. it is included as an initial step towards translating in-house research for 5-axis gcode generation into a more general format compatible with the overall FullControl concept" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + }, + "vscode": { + "interpreter": { + "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/tutorials/lab_four_axis_demo.ipynb b/tutorials/lab_four_axis_demo.ipynb index 8239cc84..9d2a6762 100644 --- a/tutorials/lab_four_axis_demo.ipynb +++ b/tutorials/lab_four_axis_demo.ipynb @@ -1,259 +1,266 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# lab four-axis demo\n", - "\n", - "this documentation gives a brief overview of 4-axis capabilities - it will be expanded in the future\n", - "\n", - "it currently works for a system with a nozzle rotating about the y axis, for which open-source documentation will be released soon\n", - "\n", - "the generated gcode would work on other 4-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", - "\n", - "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", - "[link](https://www.google.com/search?q=ipynb+tutorial),\n", - "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", - "[link](https://colab.research.google.com/)*>\n", - "\n", - "*run all cells in this notebook in order (keep pressing shift+enter)*" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### four axis import" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import lab.fullcontrol.fouraxis as fc4" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### basic demo\n", - "\n", - "points in fullcontrol are designed in the model's XYZ coordinate system\n", - "\n", - "rotation of the b axis will cause the nozzle to move in the x and z directions, and the amount that it moves depends on how far the tip of the nozzle is away from the axis of rotation. therefore it is important to set this distance with `GcodeControls(b_offset_z=...)` to allow fullcontrol to determine the correct x z values to send to the printer\n", - "\n", - "if the nozzle is below the axis of rotation b_offset_z should be positive\n", - "\n", - "there is also the potential for the nozzle to be offset from the axis of rotation in the x direction when it is vertical (b=0). this is not currently programmed in fullcontrol but will be in the future and will be set by the user with `b_offset_x`\n", - "\n", - "the GcodeControls object has slightly less functionality for 4-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "b_offset_z = 46.0 # mm" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='start point'))\n", - "steps.append(fc4.Point(x=1))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='set x=1 - gcode for this is simple... just move in x'))\n", - "steps.append(fc4.Point(b=60))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", - "steps.append(fc4.Point(b=90))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set b=90 - although x and z change, the nozzle tip doesn't move (hence E=0)\"))\n", - "steps.append(fc4.Point(z=1))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set z=1 - just like the x-movement above, this z-movement is simple. it's only changes to nozzle angle that affect other axes\"))\n", - "steps.append(fc4.Point(b=-90))\n", - "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=-90 - the print head moves to the opposite side when the nozzle rotates 180 degrees to ensure the nozzle stays at x=1'))\n", - "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z)))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### add custom color to preview axes\n", - "\n", - "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", - "\n", - "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps = []\n", - "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", - "steps.append(fc4.PlotAnnotation(label='B0'))\n", - "steps.append(fc4.Point(x=10, y=5, z=0, b=0))\n", - "steps.append(fc4.PlotAnnotation(label='B0'))\n", - "steps.append(fc4.Point(y=10, z=0, b=-180))\n", - "steps.append(fc4.PlotAnnotation(label='B-45'))\n", - "steps.append(fc4.Point(x=0, y=15, b=-180))\n", - "steps.append(fc4.PlotAnnotation(label='B-45'))\n", - "steps.append(fc4.Point(y=20, b=180))\n", - "steps.append(fc4.PlotAnnotation(label='B+45'))\n", - "steps.append(fc4.Point(x=10, y=25, b=180))\n", - "steps.append(fc4.PlotAnnotation(label='B+45'))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", - " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", - "fc4.transform(steps, 'plot', fc4.PlotControls(color_type='manual'))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### a more complex color example\n", - "\n", - "this example shows a wavey helical print path, where the tilts to easy side (oscialtes once per layer)\n", - "\n", - "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from math import sin, cos, tau\n", - "EH = 0.4\n", - "EW = 1.2\n", - "\n", - "rad = 12 # nominal radius of structure before offsets\n", - "max_offset = rad\n", - "\n", - "start_x, start_y = 75, 75\n", - "initial_z = 0.5*EH\n", - "\n", - "steps = []\n", - "segs, segs_per_layer = 10000, 200\n", - "max_z = (segs/segs_per_layer)*EH\n", - "\n", - "for i in range(segs+1):\n", - " angle = tau*i/segs_per_layer\n", - " offset = (max_offset*(i/segs)**2)*(0.5+0.5*cos(angle*2))\n", - " steps.append(fc4.Point(x=start_x+(rad+offset)*cos(angle), y=start_y+(rad+offset)*sin(angle),\n", - " z=initial_z+((i/segs_per_layer)*EH)-offset/2, b=cos(angle)*(offset/max_offset)*45))\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from B=-45 (blue) to B=45 (red)\n", - " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", - " x=start_x, y=start_y, z=max_z*1.2), label='color indicates B axis (tilt)'))\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", - " x=start_x, y=start_y, z=max_z), label='-45 deg (blue) to +45 deg (red)'))\n", - "gcode = fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z, initialization_data={\n", - " 'print_speed': 500, 'extrusion_width': EW, 'extrusion_height': EH}))\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", - "fc4.transform(steps, 'plot', fc4.PlotControls(\n", - " color_type='manual', hide_axes=False, zoom=0.75))\n", - "\n", - "design_name = 'fouraxis'\n", - "open(f'{design_name}.gcode', 'w').write(gcode)\n", - "\n", - "# activate the next line to download the gcode if using google colab\n", - "# files.download(f'{design_name}.gcode')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### use 3-axis geometry functions from FullControl (with caution!)\n", - "\n", - "this functionality should be considered experimental at best!\n", - "\n", - "geometry functions that generate 3-axis points can be used - accessed via fc4.xyz_geom()\n", - "\n", - "but they must be translated to have 4-axis methods for gcode generation - achieved via fc4.xyz_add_b()\n", - "\n", - "this conversion does not set any values of B attributes for those points - the B values will remain at whatever values they were in the ***design*** before the list of converted points\n", - "\n", - "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 45\n", - "\n", - "hence, when the ***design*** is transformed to a 'gcode' ***result***, X and Z values vary from the design X Z values in gcode to accomodate the true required position of the printhead (to get the desired nozzle location)\n", - "\n", - "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates (e.g. Z=0) because 4-axis plots in the 3D-printer's coordinates system often make no sense visually" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "steps=[]\n", - "steps.append(fc4.Point(x=10, y=0, z=0, b=45))\n", - "xyz_geometry_steps = fc4.xyz_geom.circleXY(fc4.Point(x=0, y=0, z=0), 10, 0, 16)\n", - "xyz_geometry_steps_with_bc_capabilities = fc4.xyz_add_b(xyz_geometry_steps)\n", - "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc4.xyz_geom'))\n", - "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=3.5), label='but points must be converted to 4-axis variants via fc4.xyz_add_b'))\n", - "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=30)))\n", - "fc4.transform(steps, 'plot')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "fc", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - }, - "vscode": { - "interpreter": { - "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# lab four-axis demo\n", + "\n", + "this documentation gives a brief overview of 4-axis capabilities - it will be expanded in the future\n", + "\n", + "it currently works for a system with a nozzle rotating about the y axis, for which open-source documentation will be released soon\n", + "\n", + "the generated gcode would work on other 4-axis systems but this would likely require minor tweaks and a good understanding of the gcode requirements\n", + "\n", + "<*this document is a jupyter notebook - if they're new to you, check out how they work:\n", + "[link](https://www.google.com/search?q=ipynb+tutorial),\n", + "[link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb),\n", + "[link](https://colab.research.google.com/)*>\n", + "\n", + "*run all cells in this notebook in order (keep pressing shift+enter)*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Fixed-axis note: this tutorial is retained as a reference for the older fixed-axis lab wrapper. New work may be better served by `lab.fullcontrol.infinaxis`, which is intended to express equivalent dynamic axis setups. Exact gcode equivalence is still being checked.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### four axis import" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.fouraxis as fc4" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### basic demo\n", + "\n", + "points in fullcontrol are designed in the model's XYZ coordinate system\n", + "\n", + "rotation of the b axis will cause the nozzle to move in the x and z directions, and the amount that it moves depends on how far the tip of the nozzle is away from the axis of rotation. therefore it is important to set this distance with `GcodeControls(b_offset_z=...)` to allow fullcontrol to determine the correct x z values to send to the printer\n", + "\n", + "if the nozzle is below the axis of rotation b_offset_z should be positive\n", + "\n", + "there is also the potential for the nozzle to be offset from the axis of rotation in the x direction when it is vertical (b=0). this is not currently programmed in fullcontrol but will be in the future and will be set by the user with `b_offset_x`\n", + "\n", + "the GcodeControls object has slightly less functionality for 4-axis FullControl compared to 3-axis FullControl - there are no printer options to choose from at present (the 'generic' printer is always used) and no built-in primer can be used\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b_offset_z = 46.0 # mm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "the following code cell briefly demonstrates how changes to the model coordinates and orientation affect the machine coordinates in interesting ways " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='start point'))\n", + "steps.append(fc4.Point(x=1))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='set x=1 - gcode for this is simple... just move in x'))\n", + "steps.append(fc4.Point(b=60))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=45 - this causes a change to x and z in system coordinates'))\n", + "steps.append(fc4.Point(b=90))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set b=90 - although x and z change, the nozzle tip doesn't move (hence E=0)\"))\n", + "steps.append(fc4.Point(z=1))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text=\"set z=1 - just like the x-movement above, this z-movement is simple. it's only changes to nozzle angle that affect other axes\"))\n", + "steps.append(fc4.Point(b=-90))\n", + "steps.append(fc4.GcodeComment(end_of_previous_line_text='set b=-90 - the print head moves to the opposite side when the nozzle rotates 180 degrees to ensure the nozzle stays at x=1'))\n", + "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z)))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### add custom color to preview axes\n", + "\n", + "this code cell demonstrates a convenient way to add color for previews - it is not supposed to be a useful print path, it's just for demonstration\n", + "\n", + "after creating all the steps in the design, color is added to each Point object based on the Point's orientation in B" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps = []\n", + "steps.append(fc4.Point(x=0, y=0, z=0, b=0))\n", + "steps.append(fc4.PlotAnnotation(label='B0'))\n", + "steps.append(fc4.Point(x=10, y=5, z=0, b=0))\n", + "steps.append(fc4.PlotAnnotation(label='B0'))\n", + "steps.append(fc4.Point(y=10, z=0, b=-180))\n", + "steps.append(fc4.PlotAnnotation(label='B-45'))\n", + "steps.append(fc4.Point(x=0, y=15, b=-180))\n", + "steps.append(fc4.PlotAnnotation(label='B-45'))\n", + "steps.append(fc4.Point(y=20, b=180))\n", + "steps.append(fc4.PlotAnnotation(label='B+45'))\n", + "steps.append(fc4.Point(x=10, y=25, b=180))\n", + "steps.append(fc4.PlotAnnotation(label='B+45'))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=-180 (blue) to B=+180 (red)\n", + " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", + "fc4.transform(steps, 'plot', fc4.PlotControls(color_type='manual'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### a more complex color example\n", + "\n", + "this example shows a wavey helical print path, where the tilts to easy side (oscialtes once per layer)\n", + "\n", + "the part is tilted to orient the nozzle perpendicular(ish) to the wavey walls at all points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sin, cos, tau\n", + "EH = 0.4\n", + "EW = 1.2\n", + "\n", + "rad = 12 # nominal radius of structure before offsets\n", + "max_offset = rad\n", + "\n", + "start_x, start_y = 75, 75\n", + "initial_z = 0.5*EH\n", + "\n", + "steps = []\n", + "segs, segs_per_layer = 10000, 200\n", + "max_z = (segs/segs_per_layer)*EH\n", + "\n", + "for i in range(segs+1):\n", + " angle = tau*i/segs_per_layer\n", + " offset = (max_offset*(i/segs)**2)*(0.5+0.5*cos(angle*2))\n", + " steps.append(fc4.Point(x=start_x+(rad+offset)*cos(angle), y=start_y+(rad+offset)*sin(angle),\n", + " z=initial_z+((i/segs_per_layer)*EH)-offset/2, b=cos(angle)*(offset/max_offset)*45))\n", + "for step in steps:\n", + " if type(step).__name__ == 'Point':\n", + " # color is a gradient from B=-45 (blue) to B=45 (red)\n", + " step.color = [((step.b+45)/90), 0, 1-((step.b+45)/90)]\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", + " x=start_x, y=start_y, z=max_z*1.2), label='color indicates B axis (tilt)'))\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(\n", + " x=start_x, y=start_y, z=max_z), label='-45 deg (blue) to +45 deg (red)'))\n", + "gcode = fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=b_offset_z, initialization_data={\n", + " 'print_speed': 500, 'extrusion_width': EW, 'extrusion_height': EH}))\n", + "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", + "fc4.transform(steps, 'plot', fc4.PlotControls(\n", + " color_type='manual', hide_axes=False, zoom=0.75))\n", + "\n", + "design_name = 'fouraxis'\n", + "open(f'{design_name}.gcode', 'w').write(gcode)\n", + "\n", + "# activate the next line to download the gcode if using google colab\n", + "# files.download(f'{design_name}.gcode')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### use 3-axis geometry functions from FullControl (with caution!)\n", + "\n", + "this functionality should be considered experimental at best!\n", + "\n", + "geometry functions that generate 3-axis points can be used - accessed via fc4.xyz_geom()\n", + "\n", + "but they must be translated to have 4-axis methods for gcode generation - achieved via fc4.xyz_add_b()\n", + "\n", + "this conversion does not set any values of B attributes for those points - the B values will remain at whatever values they were in the ***design*** before the list of converted points\n", + "\n", + "in the example below, a circle is created in the XY plane in the model's coordinate system, but the b-axis is set to 45\n", + "\n", + "hence, when the ***design*** is transformed to a 'gcode' ***result***, X and Z values vary from the design X Z values in gcode to accomodate the true required position of the printhead (to get the desired nozzle location)\n", + "\n", + "in contrast, when the ***design*** is transformed to a 'plot' ***result***, the plot shows model coordinates (e.g. Z=0) because 4-axis plots in the 3D-printer's coordinates system often make no sense visually" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps=[]\n", + "steps.append(fc4.Point(x=10, y=0, z=0, b=45))\n", + "xyz_geometry_steps = fc4.xyz_geom.circleXY(fc4.Point(x=0, y=0, z=0), 10, 0, 16)\n", + "xyz_geometry_steps_with_bc_capabilities = fc4.xyz_add_b(xyz_geometry_steps)\n", + "steps.extend(xyz_geometry_steps_with_bc_capabilities)\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=5), label='normal FullControl geometry functions can be used via fc4.xyz_geom'))\n", + "steps.append(fc4.PlotAnnotation(point=fc4.Point(x=0, y=0, z=3.5), label='but points must be converted to 4-axis variants via fc4.xyz_add_b'))\n", + "print(fc4.transform(steps, 'gcode', fc4.GcodeControls(b_offset_z=30)))\n", + "fc4.transform(steps, 'plot')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + }, + "vscode": { + "interpreter": { + "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/tutorials/lab_infaxis_5_demo.ipynb b/tutorials/lab_infaxis_5_demo.ipynb deleted file mode 100644 index d1743815..00000000 --- a/tutorials/lab_infaxis_5_demo.ipynb +++ /dev/null @@ -1,152 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "88f67711", - "metadata": {}, - "outputs": [], - "source": [ - "import lab.fullcontrol.infaxis as fci\n", - "import fullcontrol as fc\n", - "from math import sin, cos, tau\n", - "\n", - "fc.Point = fci.Point\n", - "fc.GcodeControls = fci.GcodeControls\n", - "fc.transform = fci.transform\n", - "fc.Axis = fci.Axis" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0d54e7f4", - "metadata": {}, - "outputs": [], - "source": [ - "EW = 0.6\n", - "EH = 0.3\n", - "\n", - "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", - "gcode_controls = fc.GcodeControls(\n", - " head_chain = [fc.Axis(name='B')],\n", - " bed_chain = [fc.Axis(name='C')],\n", - " distance_axis = True,\n", - " verbose = True,\n", - " initialization_data=print_settings \n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fa20bc2b", - "metadata": {}, - "outputs": [], - "source": [ - "def vase_from_trace(trace_points,density):\n", - " steps = []\n", - " for i in range(len(trace_points)-1):\n", - " p = trace_points[i]\n", - " p_next = trace_points[i+1]\n", - " r = (p.x**2 + p.y**2)**0.5\n", - " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", - " dr = r_next - r\n", - " dz = p_next.z-p.z\n", - " ptilt = p.axes['B']\n", - " dtilt = p_next.axes['B']-ptilt\n", - " \n", - " dc = p_next.axes['C']-p.axes['C']\n", - " for j in range(density):\n", - " angle = p.axes['C'] + dc * j / density\n", - " tilt = ptilt + dtilt * j / density\n", - " r_final = r + dr * j / density\n", - " z_final = p.z + dz * j / density\n", - " \n", - " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", - " \n", - " return steps\n", - "\n", - "density = 360\n", - "r_start = 10\n", - "r_tilt = 5 # 10 layers\n", - "tilt_start = 0\n", - "tilt_end = 90\n", - "h = EH # height of each layer\n", - "z_start = h*0.5 # starting z height\n", - "layers = 20 # number of layers in the z direction for first segment\n", - "arc_layers = int((r_tilt * (tilt_end - tilt_start) / 360 *tau)/h)\n", - "d_tilted = 5\n", - "layers_tilted = int(d_tilted/h)\n", - "\n", - "trace = []\n", - "\n", - "for i in range(layers):\n", - " r = r_start\n", - " tilt = tilt_start\n", - " angle = i * 360\n", - " z = z_start + h * i\n", - "\n", - " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", - "\n", - "angle_offset = fc.last_point(trace).axes['C']\n", - "z_offset = fc.last_point(trace).z\n", - "\n", - "for i in range(1,arc_layers):\n", - " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", - " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", - " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", - " angle = i * 360 + angle_offset\n", - " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", - "\n", - "angle_offset = fc.last_point(trace).axes['C']\n", - "z_offset = fc.last_point(trace).z\n", - "r_offset = fc.last_point(trace).x\n", - "\n", - "for i in range(1,layers_tilted):\n", - " tilt = tilt_end\n", - " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", - " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", - " angle = i * 360 + angle_offset\n", - " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", - "\n", - "steps = vase_from_trace(trace, density)\n", - "steps.append(fc.last_point(trace))\n", - "\n", - "for step in steps:\n", - " if type(step).__name__ == 'Point':\n", - " # color is a gradient from A=0 (blue) to A=90 (red)\n", - " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", - "\n", - "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", - "fig.show()\n", - "\n", - "gcode = fc.transform(steps,'gcode',gcode_controls)\n", - "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", - "print('')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "fiveaxis (3.12.12)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From d38fe920573a7af057abd8736c4ae283a3301f7b Mon Sep 17 00:00:00 2001 From: andy g Date: Tue, 16 Jun 2026 18:26:05 +0100 Subject: [PATCH 4/4] Remove temporary infinaxis changelog --- .../infinaxis_changes_after_initial_pr.md | 1218 ----------------- 1 file changed, 1218 deletions(-) delete mode 100644 lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md diff --git a/lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md b/lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md deleted file mode 100644 index 95598fdf..00000000 --- a/lab/fullcontrol/infinaxis/infinaxis_changes_after_initial_pr.md +++ /dev/null @@ -1,1218 +0,0 @@ -# Changes implemented to first pr submission - -- Renamed `infaxis` to `infinaxis` across package files, demos, tutorials, Colab notebooks, and the Colab generator. -- `lab.fullcontrol.infinaxis` now follows the root package flow: `__init__.py` imports from `common.py`. -- `common.py` imports the base `fullcontrol` namespace for normal user access, then overrides the infinaxis-specific public names: `Point`, `Printer`, `GcodeControls`, `Axis`, `gcode`, and `transform`. -- Demos/tutorials now use only `import lab.fullcontrol.infinaxis as fci`; no `fullcontrol` monkey-patching or extra `import fullcontrol as fc`. -- Helper-only plot functions moved out of public `common.py` into private `_plot.py`. -- Internal package imports now point at `lab.fullcontrol.infinaxis...`. -- `Point.axes` remains optional, and inverse kinematics only reads it when it is present, so `fci.Point(x=10)` works naturally. -- Added `fci.configure_point(head_chain, bed_chain)` to return a configured `Point` class. - - It keeps `axes={...}` as the internal storage model, but allows dot-style axis input/access like `Point(x=10, b=20, c=30)` or `Point(X=10, B=20, C=30)`. - - Axis matching is case-insensitive for user input, while stored axis keys preserve the configured `Axis.name` for gcode output. - - Gcode state now recognizes configured `Point` subclasses. -- Updated the two infinaxis demos, tutorial notebooks, and Colab copies to use `Point = fci.configure_point(head_chain, bed_chain)` with dot-style axis values instead of `axes={...}`. -- Added `fci.xyz_geom` and `fci.xyz_add_axes(...)`, matching the existing multiaxis pattern for using normal xyz geometry functions and converting their Points to infinaxis Points. -- Clarified optional helper gcode outputs: `distance_axis` can emit accumulated-distance `U`, while `model_XYZ_gcode` can emit model-space `U/V/W` for motion-planning support. -- Updated the root 5-axis demo to show gcode output with `verbose=False` and `verbose=True`. -- `Axis(name="X")`, `Axis(name="Y")`, and `Axis(name="Z")` now raise an exception; linear helper axes should use another name with `type="X"`, `type="Y"`, or `type="Z"`. -- Added a root custom-axis-names demo showing `CI` / `CII` with shared `type="C"` kinematics. -- Added a root controls demo showing `Axis.name/type/active/orientation/offset` plus `GcodeControls.verbose`, `distance_axis`, `model_XYZ_gcode`, `inverse_time_feedrate`, and `xyz_orientation`; uncertain descriptions are marked with `CHECK_THIS_DESCRIPTION`. -- Added cautious fixed-axis notes to `lab.fullcontrol.fouraxis`, `lab.fullcontrol.fiveaxis`, `lab.fullcontrol.fiveaxisC0B1`, and the four/five-axis tutorial and Colab notebooks. - - -Due to rename from infaxis to infinaxis, git tracking is not that valuable (lots of untracked/deleted files rather than comparisions of modification). A python script was written to do a comparison and results of that script are copied below. - -### python script: - -``` python -from __future__ import annotations - -import difflib -import json -import subprocess -from pathlib import Path - - -ROOT = Path(__file__).resolve().parent -OUTPUT = ROOT / "infinaxis_rename_comparison.md" - -PAIRS = [ - ("lab/fullcontrol/infaxis/__init__.py", "lab/fullcontrol/infinaxis/__init__.py"), - ("lab/fullcontrol/infaxis/axis.py", "lab/fullcontrol/infinaxis/axis.py"), - ("lab/fullcontrol/infaxis/common.py", "lab/fullcontrol/infinaxis/common.py"), - ("lab/fullcontrol/infaxis/controls.py", "lab/fullcontrol/infinaxis/controls.py"), - ("lab/fullcontrol/infaxis/point.py", "lab/fullcontrol/infinaxis/point.py"), - ("lab/fullcontrol/infaxis/printer.py", "lab/fullcontrol/infinaxis/printer.py"), - ("lab/fullcontrol/infaxis/state.py", "lab/fullcontrol/infinaxis/state.py"), - ("lab/fullcontrol/infaxis/steps2gcode.py", "lab/fullcontrol/infinaxis/steps2gcode.py"), - ("tutorials/lab_infaxis_4_demo.ipynb", "tutorials/lab_infinaxis_4_demo.ipynb"), - ("tutorials/lab_infaxis_5_demo.ipynb", "tutorials/lab_infinaxis_5_demo.ipynb"), - ("tutorials/colab/lab_infaxis_4_demo_colab.ipynb", "tutorials/colab/lab_infinaxis_4_demo_colab.ipynb"), - ("tutorials/colab/lab_infaxis_5_demo_colab.ipynb", "tutorials/colab/lab_infinaxis_5_demo_colab.ipynb"), -] - -NEW_ONLY = [ - "lab/fullcontrol/infinaxis/_plot.py", - "lab/fullcontrol/infinaxis/xyz_add_axes.py", -] - - -def git_show(path: str) -> str: - return subprocess.check_output(["git", "show", f"HEAD:{path}"], cwd=ROOT, text=True) - - -def normalize_text(text: str) -> str: - return ( - text.replace("infaxis", "infinaxis") - .replace("Infaxis", "Infinaxis") - .replace("INFAXIS", "INFINAXIS") - .replace("fc_infinaxis", "lab.fullcontrol.infinaxis") - ) - - -def normalize_notebook(text: str) -> str: - notebook = json.loads(text) - for cell in notebook.get("cells", []): - if cell.get("cell_type") == "code": - cell["execution_count"] = None - cell["outputs"] = [] - return json.dumps(notebook, indent=1, sort_keys=True) + "\n" - - -def normalized(path: str, text: str) -> list[str]: - text = normalize_text(text) - if path.endswith(".ipynb"): - text = normalize_notebook(text) - return text.splitlines(keepends=True) - - -def main() -> None: - sections: list[str] = [ - "# Infinaxis Rename Comparison\n\n", - "This file compares old tracked `infaxis` files from `HEAD` with the current working-tree `infinaxis` files.\n\n", - "Normalization applied before diffing:\n\n", - "- `infaxis`/`Infaxis`/`INFAXIS` text is treated as `infinaxis`/`Infinaxis`/`INFINAXIS`.\n", - "- notebook outputs and execution counts are cleared.\n", - "- notebook JSON is formatted consistently.\n\n", - "Use this as a temporary commit-description aid; it can be deleted after the rename/edit commit is prepared.\n\n", - ] - - changed = 0 - unchanged = 0 - missing = 0 - - for old_path, new_path in PAIRS: - sections.append(f"## `{old_path}` -> `{new_path}`\n\n") - new_file = ROOT / new_path - if not new_file.exists(): - sections.append(f"Missing new file: `{new_path}`\n\n") - missing += 1 - continue - - old_lines = normalized(old_path, git_show(old_path)) - new_lines = normalized(new_path, new_file.read_text()) - diff = list( - difflib.unified_diff( - old_lines, - new_lines, - fromfile=old_path.replace("infaxis", "infinaxis"), - tofile=new_path, - ) - ) - if diff: - changed += 1 - sections.append("```diff\n") - sections.extend(diff) - sections.append("```\n\n") - else: - unchanged += 1 - sections.append("No content changes after rename normalization.\n\n") - - if NEW_ONLY: - sections.append("## New files without old `infaxis` equivalent\n\n") - for path in NEW_ONLY: - exists = (ROOT / path).exists() - status = "present" if exists else "missing" - sections.append(f"- `{path}`: {status}\n") - sections.append("\n") - - summary = ( - "## Summary\n\n" - f"- compared pairs: {len(PAIRS)}\n" - f"- changed after normalization: {changed}\n" - f"- unchanged after normalization: {unchanged}\n" - f"- missing new files: {missing}\n\n" - ) - sections.insert(1, summary) - OUTPUT.write_text("".join(sections)) - print(OUTPUT) - - -if __name__ == "__main__": - main() -``` - -### python script output: - - -_____ -_____ -_____ - -### NOTE THIS WAS RUN BEFORE RENAMING SOME OF THE TUTORIALS AND CREATING THE README AT lab/fullcontrol/infinaxis/README.md - -# Infinaxis Rename Comparison - -## Summary - -- compared pairs: 12 -- changed after normalization: 12 -- unchanged after normalization: 0 -- missing new files: 0 - -This file compares old tracked `infaxis` files from `HEAD` with the current working-tree `infinaxis` files. - -Normalization applied before diffing: - -- `infaxis`/`Infaxis`/`INFAXIS` text is treated as `infinaxis`/`Infinaxis`/`INFINAXIS`. -- notebook outputs and execution counts are cleared. -- notebook JSON is formatted consistently. - -Use this as a temporary commit-description aid; it can be deleted after the rename/edit commit is prepared. - -## `lab/fullcontrol/infaxis/__init__.py` -> `lab/fullcontrol/infinaxis/__init__.py` - -```diff ---- lab/fullcontrol/infinaxis/__init__.py -+++ lab/fullcontrol/infinaxis/__init__.py -@@ -1,7 +1 @@ --from .point import Point --from .controls import GcodeControls --from .printer import Printer --from .state import State --from .steps2gcode import gcode --from .common import transform --from .axis import Axis+from lab.fullcontrol.infinaxis.common import * -``` - -## `lab/fullcontrol/infaxis/axis.py` -> `lab/fullcontrol/infinaxis/axis.py` - -```diff ---- lab/fullcontrol/infinaxis/axis.py -+++ lab/fullcontrol/infinaxis/axis.py -@@ -12,6 +12,9 @@ - def __init__(self, **data): - super().__init__(**data) - -+ if self.name is not None and self.name.upper() in ["X", "Y", "Z"]: -+ raise ValueError('Axis.name cannot be "X", "Y", or "Z"; use Axis.type for linear kinematics with a different name') -+ - if self.type is None: - if self.name is not None and self.name in ["A", "B", "C"]: - self.type = self.name -``` - -## `lab/fullcontrol/infaxis/common.py` -> `lab/fullcontrol/infinaxis/common.py` - -```diff ---- lab/fullcontrol/infinaxis/common.py -+++ lab/fullcontrol/infinaxis/common.py -@@ -1,307 +1,14 @@ - from typing import Union - --import numpy as np --import plotly.graph_objects as go --from fullcontrol.visualize.tube_mesh import CylindersMesh, FlowTubeMesh, MeshExporter --from fullcontrol.visualize.controls import PlotControls --from fullcontrol.visualize.plot_data import PlotData --from fullcontrol.visualize.state import State --from fullcontrol.visualize.plotly import generate_mesh --# # see comment in __init__.py about why this module exists -- --# # import functions and classes that will be accessible to the user --from fullcontrol.common import check --from fullcontrol.geometry import move, move_polar, travel_to # don't import all geometry functions since they are not designed for multiaxis Points --from fullcontrol.combinations.gcode_and_visualize.classes import * -+from fullcontrol import * - import fullcontrol.geometry as xyz_geom -- --def generate_single_path_vase_lod_surface_arrays( -- path, -- center_xy=None, -- points_per_turn: int = 128, -- turn_stride: int = 2, -- color_mode: str = "none", --): -- points = np.asarray([path.xvals, path.yvals, path.zvals], dtype=np.float32).T -- -- if len(points) < 4: -- return None, None, None, None -- -- good = np.ones(len(points), dtype=bool) -- dups = np.all(np.diff(points, axis=0) == 0, axis=1) -- good[1:] = ~dups -- points = points[good] -- -- if len(points) < 4: -- return None, None, None, None -- -- source_color_scalar = None -- -- if color_mode == "path": -- if getattr(path, "colors", None) is not None and len(path.colors) > 0: -- colors_arr = np.asarray(path.colors, dtype=np.float32) -- -- if len(colors_arr) == len(good): -- colors_arr = colors_arr[good] -- -- if len(colors_arr) == len(points): -- source_color_scalar = ( -- 0.299 * colors_arr[:, 0] -- + 0.587 * colors_arr[:, 1] -- + 0.114 * colors_arr[:, 2] -- ).astype(np.float32) -- -- if center_xy is None: -- cx = np.float32(0.5 * (np.min(points[:, 0]) + np.max(points[:, 0]))) -- cy = np.float32(0.5 * (np.min(points[:, 1]) + np.max(points[:, 1]))) -- else: -- cx, cy = center_xy -- -- theta = np.unwrap(np.arctan2(points[:, 1] - cy, points[:, 0] - cx)).astype(np.float32) -- -- if theta[-1] < theta[0]: -- theta = -theta -- -- turn_coord = ((theta - theta[0]) / np.float32(2.0 * np.pi)).astype(np.float32) -- -- monotonic_good = np.concatenate([[True], np.diff(turn_coord) > 1e-10]) -- turn_coord = turn_coord[monotonic_good] -- points = points[monotonic_good] -- -- if source_color_scalar is not None: -- source_color_scalar = source_color_scalar[monotonic_good] -- -- if len(points) < 4: -- return None, None, None, None -- -- first_complete_turn = int(np.ceil(turn_coord[0])) -- last_complete_turn = int(np.floor(turn_coord[-1])) - 1 -- -- if last_complete_turn <= first_complete_turn: -- return None, None, None, None -- -- all_turns = np.arange(first_complete_turn, last_complete_turn + 1, dtype=np.float32) -- -- selected_turns = all_turns[::max(1, int(turn_stride))] -- -- if selected_turns[-1] != all_turns[-1]: -- selected_turns = np.append(selected_turns, all_turns[-1]).astype(np.float32) -- -- if len(selected_turns) < 2: -- return None, None, None, None -- -- phases = np.linspace( -- 0.0, -- 1.0, -- points_per_turn + 1, -- endpoint=True, -- dtype=np.float32, -- ) -- -- targets = selected_turns[:, None] + phases[None, :] -- targets_flat = targets.ravel() -- -- x_grid = np.interp(targets_flat, turn_coord, points[:, 0]).reshape(targets.shape).astype(np.float32) -- y_grid = np.interp(targets_flat, turn_coord, points[:, 1]).reshape(targets.shape).astype(np.float32) -- z_grid = np.interp(targets_flat, turn_coord, points[:, 2]).reshape(targets.shape).astype(np.float32) -- -- if color_mode == "path" and source_color_scalar is not None: -- color_grid = np.interp( -- targets_flat, -- turn_coord, -- source_color_scalar, -- ).reshape(targets.shape).astype(np.float32) -- -- elif color_mode == "height": -- color_grid = z_grid -- -- elif color_mode == "turn": -- color_grid = targets.astype(np.float32) -- -- else: -- color_grid = np.zeros_like(z_grid, dtype=np.float32) -- -- return x_grid, y_grid, z_grid, color_grid -- --def generate_single_path_vase_lod_surface_trace( -- path, -- center_xy=None, -- points_per_turn: int = 128, -- turn_stride: int = 2, -- color_mode: str = "none", -- colorscale="Viridis", -- showscale: bool = False, -- opacity: float = 1.0, --): -- """ -- Drop-in Plotly Surface trace for fast vase preview. -- -- Returns: -- go.Surface or None -- """ -- -- x, y, z, surfacecolor = generate_single_path_vase_lod_surface_arrays( -- path, -- center_xy=center_xy, -- points_per_turn=points_per_turn, -- turn_stride=turn_stride, -- color_mode=color_mode, -- ) -- -- if x is None: -- return None -- -- return go.Surface( -- x=x, -- y=y, -- z=z, -- surfacecolor=surfacecolor, -- colorscale=colorscale, -- showscale=showscale, -- opacity=opacity, -- hoverinfo="skip", -- contours=dict( -- x=dict(show=False), -- y=dict(show=False), -- z=dict(show=False), -- ), -- # lighting=dict( -- # ambient=0.65, -- # diffuse=0.7, -- # specular=0.05, -- # roughness=1.0, -- # ), -- showlegend=False, -- ) -- -- -- --def fig_plot(data: PlotData, controls: PlotControls): -- ''' -- Plot data for x y z lines with RGB colors and annotations. -- The style of the plot is governed by the controls. -- -- Args: -- data (PlotData): The data to be plotted. -- controls (PlotControls): The controls for customizing the plot. -- -- Returns: -- None -- ''' -- -- fig = go.Figure() -- -- if controls.tube_type is not None: -- Mesh = {'flow': FlowTubeMesh, 'cylinders': CylindersMesh}[controls.tube_type] -- else: # Fall back to FlowTubeMesh if no tube_type is explicitly specified -- Mesh = FlowTubeMesh -- -- # generate line plots -- max_width = 0 -- -- for path in data.paths: -- colors_now = [f'rgb({color[0]*255:.2f}, {color[1]*255:.2f}, {color[2]*255:.2f})' for color in path.colors] -- -- linewidth_now = controls.line_width * 2 if path.extruder.on == True else controls.line_width * 0.5 -- -- ## Generate mesh now imported from main fullcontrol visualization module, the only reason it was here was for the global local_max variable, I've just changed the plot function to derive a similar variable from the path width instead. -- if path.widths: -- max_width = max(max_width, max(path.widths)) -- -- if path.extruder.on and controls.style == "vase_surface": -- surface_trace = generate_single_path_vase_lod_surface_trace( -- path, -- points_per_turn=96, -- turn_stride=3, -- color_mode="height", -- colorscale="viridis", -- showscale=False, -- opacity=1.0, -- ) -- -- if surface_trace is not None: -- fig.add_trace(surface_trace) -- -- -- elif path.extruder.on and controls.style == 'tube': -- sides, rounding_strength, flat_sides = controls.tube_sides, 0.4, False -- mesh = generate_mesh(path, linewidth_now, Mesh, sides, rounding_strength, flat_sides, colors_now) -- fig.add_trace(mesh.to_Mesh3d(colors=colors_now)) -- -- elif not controls.hide_travel or path.extruder.on: -- fig.add_trace(go.Scatter3d( -- mode='lines', -- x=path.xvals, -- y=path.yvals, -- z=path.zvals, -- showlegend=False, -- line=dict(width=linewidth_now, color=colors_now), -- )) -- -- -- # find a bounding box, to create a plot with equally proportioned X Y Z scales (so a cuboid looks like a cuboid, not a cube) -- bounding_box_size = max(data.bounding_box.maxx-data.bounding_box.minx, data.bounding_box.maxy - -- data.bounding_box.miny, data.bounding_box.maxz-min(0, data.bounding_box.minz)) -- bounding_box_size += 0.002 -- bounding_box_size += max_width -- -- # generate annotations -- annotations_pts = [] -- annotations = [] -- if controls.hide_annotations == False and not controls.neat_for_publishing: -- # if controls.hide_annotations == False: # and not controls.neat_for_publishing: -- for annotation in data.annotations: -- x, y, z = (annotation[axis] for axis in 'xyz') -- annotations_pts.append([x, y, z]) -- annotations.append(dict( -- showarrow=False, -- x=x, y=y, z=z, -- text=annotation['label'], -- yshift=10)) -- xs, ys, zs = zip(*annotations_pts) if annotations_pts else [[]]*3 -- fig.add_trace(go.Scatter3d(mode='markers', x=xs, y=ys, z=zs, showlegend=False, marker=dict(size=2, color='red'))) -- -- # make sure the bounding box is big enough for the annotations -- # the 0.001 is to make sure the annotations don't lie on the boundary -- midx, midy, midz = (getattr(data.bounding_box, f'mid{axis}') for axis in 'xyz') -- range -- offset = 0.001 -- offset_both_sides = 2 * offset -- for (x, y, z) in annotations_pts: -- if x < midx - bounding_box_size / 2 + offset: -- bounding_box_size = 2 * (midx - x) + offset_both_sides -- if x > midx + bounding_box_size / 2 - offset: -- bounding_box_size = 2 * (x - midx) + offset_both_sides -- if y < midy - bounding_box_size / 2 + offset: -- bounding_box_size = 2 * (midy - y) + offset_both_sides -- if y > midy + bounding_box_size / 2 - offset: -- bounding_box_size = 2 * (y - midy) + offset_both_sides -- if z < midz - bounding_box_size / 2 + offset: -- bounding_box_size = 2 * (midz - z) + offset_both_sides -- if z > midz + bounding_box_size / 2 - offset: -- bounding_box_size = 2 * (z - midz) + offset_both_sides -- -- relative_centre_z = 0.5*data.bounding_box.rangez/bounding_box_size -- camera_centre_z = -0.5 + relative_centre_z -- camera = dict(eye=dict(x=-0.5/controls.zoom, y=-1/controls.zoom, z=-0.5+0.5/controls.zoom), -- center=dict(x=0, y=0, z=camera_centre_z)) -- fig.update_layout(template='plotly_dark', paper_bgcolor="black", scene_aspectmode='cube', -- scene=dict(annotations=annotations, -- xaxis=dict(backgroundcolor="black", nticks=10, -- range=[data.bounding_box.midx-bounding_box_size/2, data.bounding_box.midx+bounding_box_size/2],), -- yaxis=dict(backgroundcolor="black", nticks=10, -- range=[data.bounding_box.midy-bounding_box_size/2, data.bounding_box.midy+bounding_box_size/2],), -- zaxis=dict(backgroundcolor="black", nticks=10, range=[min(0, data.bounding_box.minz), bounding_box_size],), -- ), scene_camera=camera, width=800, height=500, margin=dict(l=10, r=10, b=10, t=10, pad=4)) -- if controls.hide_axes or controls.neat_for_publishing: -- for axis in ['xaxis', 'yaxis', 'zaxis']: -- fig.update_layout( -- scene={axis: dict(showgrid=False, zeroline=False, visible=False)}) -- if controls.neat_for_publishing: -- fig.update_layout(width=500, height=500) -- -- # cicd_testing is a flag set by the CICD testing script (as a temporary environmental variable) to save the plot as a .png file -- return fig -+# base fc namespace for user access; infinaxis replacements override matching names -+from lab.fullcontrol.infinaxis.axis import Axis -+from lab.fullcontrol.infinaxis.controls import GcodeControls -+from lab.fullcontrol.infinaxis.point import Point, configure_point -+from lab.fullcontrol.infinaxis.printer import Printer -+from lab.fullcontrol.infinaxis.steps2gcode import gcode -+from lab.fullcontrol.infinaxis.xyz_add_axes import xyz_add_axes - - - def transform(steps: list, result_type: str, controls: Union[GcodeControls, PlotControls] = None, show_tips: bool = True): -@@ -311,7 +18,6 @@ - ''' - - if result_type == 'gcode': -- from lab.fullcontrol.infinaxis.steps2gcode import gcode - if controls is None: controls = GcodeControls() - return gcode(steps, controls) - -@@ -321,16 +27,18 @@ - return visualize(steps, controls, show_tips) - - elif result_type == 'fig': -- from fullcontrol.visualize.steps2visualization import visualize -+ from fullcontrol.visualize.plot_data import PlotData -+ from fullcontrol.visualize.state import State as VisualizeState -+ from lab.fullcontrol.infinaxis._plot import fig_plot - - if controls is None: controls = PlotControls() - plot_controls = controls - plot_controls.initialize() - -- state = State(steps, plot_controls) -+ state = VisualizeState(steps, plot_controls) - plot_data = PlotData(steps, state) - for step in steps: - step.visualize(state, plot_data, plot_controls) - plot_data.cleanup() - -- return fig_plot(plot_data, plot_controls)+ return fig_plot(plot_data, plot_controls) -``` - -## `lab/fullcontrol/infaxis/controls.py` -> `lab/fullcontrol/infinaxis/controls.py` - -```diff ---- lab/fullcontrol/infinaxis/controls.py -+++ lab/fullcontrol/infinaxis/controls.py -@@ -11,6 +11,7 @@ - bed_chain: list = [] # Ordered list of axes in the bed chain, used for the IK loop (back to front!) - xyz_orientation: Optional[list] = [1,1,1] # orientation of XYZ axes, 1 means the axis follows the right hand rule. - inverse_time_feedrate: Optional[bool] = False # if true, F command will be output as inverse time feedrate (e.g. F0.5 for 2 seconds per move) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. Note that when this is true, the print_speed and travel_speed attributes will be interpreted as seconds per move instead of mm/s. Also note that acceleration and deceleration will not be handled correctly when using inverse time feedrate, so it is recommended to use constant speed moves (G1 F...) when this is true. -+ # the following two parameters (model_XYZ_gcode and distance_axis) may be useful for give more information for motion planning - distance_axis: Optional[bool] = False - model_XYZ_gcode: Optional[bool] = False # if true, the gcode output will have a UVW axis which shows the the XYZ movement in the model (Alternative to distance_axis). Intended to be used with the system axis (XYZAC for instance) all be treated as rotational in firmware and the UVW being linear aixs for motion planning. - verbose: Optional[bool] = False``` - -## `lab/fullcontrol/infaxis/point.py` -> `lab/fullcontrol/infinaxis/point.py` - -```diff ---- lab/fullcontrol/infinaxis/point.py -+++ lab/fullcontrol/infinaxis/point.py -@@ -3,7 +3,56 @@ - from copy import deepcopy - import numpy as np - --from infinaxis.axis import Axis -+from lab.fullcontrol.infinaxis.axis import Axis -+ -+ -+def _model_field_names(model_class): -+ return set(model_class.model_fields if hasattr(model_class, "model_fields") else model_class.__fields__) -+ -+ -+def _axis_names(head_chain=None, bed_chain=None): -+ axes = list(head_chain or []) + list(bed_chain or []) -+ return {axis.name for axis in axes if axis.name is not None} -+ -+ -+def configure_point(head_chain=None, bed_chain=None): -+ """Return a Point class whose extra constructor fields map to configured axes.""" -+ axis_name_lookup = {name.lower(): name for name in _axis_names(head_chain, bed_chain)} -+ -+ class ConfiguredPoint(Point): -+ # Keep axes as the storage model. This class is only a user-facing -+ # convenience so designs can write Point(x=..., b=...) and point.b. -+ def __init__(self, **data): -+ fields = _model_field_names(type(self)) -+ field_lookup = {name.lower(): name for name in fields} -+ axes = dict(data.pop("axes", None) or {}) -+ for name in list(data): -+ if name not in fields and name.lower() in field_lookup: -+ data[field_lookup[name.lower()]] = data.pop(name) -+ elif name.lower() in axis_name_lookup and name not in fields: -+ axes[axis_name_lookup[name.lower()]] = data.pop(name) -+ if axes: -+ data["axes"] = axes -+ super().__init__(**data) -+ -+ def __getattr__(self, name): -+ axes = getattr(self, "axes", None) -+ axis_name = axis_name_lookup.get(name.lower()) -+ if axes is not None and axis_name in axes: -+ return axes[axis_name] -+ raise AttributeError(name) -+ -+ def __setattr__(self, name, value): -+ axis_name = axis_name_lookup.get(name.lower()) -+ if axis_name is not None and name not in _model_field_names(type(self)): -+ axes = dict(getattr(self, "axes", None) or {}) -+ axes[axis_name] = value -+ super().__setattr__("axes", axes) -+ else: -+ super().__setattr__(name, value) -+ -+ return ConfiguredPoint -+ - - class Point(BasePoint): - axes: Optional[dict] = None # dictionary for assigning chages to the printer Axis based on the info in the point -@@ -89,9 +138,10 @@ - model_point = deepcopy(state.point) - model_point.update_from(self) - # Update Axis from point -- for axis in state.printer.head_chain + state.printer.bed_chain: -- if axis.name in self.axes and self.axes[axis.name] != None: -- axis.active = self.axes[axis.name] -+ if self.axes is not None: -+ for axis in state.printer.head_chain + state.printer.bed_chain: -+ if axis.name in self.axes and self.axes[axis.name] != None: -+ axis.active = self.axes[axis.name] - - # inverse kinematics: - system_point = model2system(model_point, state) -@@ -125,6 +175,7 @@ - - - state.distance_accumulated += (dist**2-dist_system**2)**0.5 if dist - dist_system > 0 else 0 -+ # the following two checks for model_XYZ_gcode and distance_axis are only passed if the user flags them in GcodeControls and may be useful for give more information for motion planning - if state.printer.model_XYZ_gcode: - infinaxis_str = infinaxis_str + f"U{round(self.x, 6):.6} V{round(self.y, 6):.6} W{round(self.z, 6):.6} " - elif state.printer.distance_axis: -@@ -167,4 +218,4 @@ - if change_check: - state.point.update_color(state, plot_data, plot_controls) - plot_data.paths[-1].add_point(state) -- state.point_count_now += 1+ state.point_count_now += 1 -``` - -## `lab/fullcontrol/infaxis/printer.py` -> `lab/fullcontrol/infinaxis/printer.py` - -```diff ---- lab/fullcontrol/infinaxis/printer.py -+++ lab/fullcontrol/infinaxis/printer.py -@@ -11,6 +11,7 @@ - bed_chain: list = None - xyz_orientation: list = None - inverse_time_feedrate: bool = None # if true, F command will be output as inverse time feedrate (e.g. F2 for 30 seconds per move (1/2 minutes per move)) instead of speed (e.g. F300 for 300 mm/s). This is useful for some multiaxis machines that use inverse time feedrate to control speed. -+ # the following two parameters (model_XYZ_gcode and distance_axis) may be useful for give more information for motion planning - distance_axis: bool = None - model_XYZ_gcode: bool = None - verbose: bool = None -``` - -## `lab/fullcontrol/infaxis/state.py` -> `lab/fullcontrol/infinaxis/state.py` - -```diff ---- lab/fullcontrol/infinaxis/state.py -+++ lab/fullcontrol/infinaxis/state.py -@@ -3,11 +3,10 @@ - from importlib import import_module - - from fullcontrol.gcode.extrusion_classes import ExtrusionGeometry, Extruder --from fullcontrol.gcode.controls import GcodeControls - --from infinaxis.point import Point --from infinaxis.printer import Printer --from infinaxis.controls import GcodeControls -+from lab.fullcontrol.infinaxis.point import Point -+from lab.fullcontrol.infinaxis.printer import Printer -+from lab.fullcontrol.infinaxis.controls import GcodeControls - - - class State(BaseModel): -@@ -36,14 +35,14 @@ - 'return first Point in list. if the parameter fully_defined is true, return first Point with x,y,z' - if type(steps).__name__ == 'list': - for i in range(len(steps)): -- if type(steps[i]).__name__ == 'Point': -+ if isinstance(steps[i], Point): - if fully_defined: - if steps[i].x != None and steps[i].y != None and steps[i].z != None: - return steps[i] - else: - return steps[i] - if fully_defined: -- raise Exception(f'No point found in steps with all five axis defined') -+ raise Exception(f'No point found in steps with fully defined x, y, and z') - if not fully_defined: - raise Exception(f'No point found in steps') - -@@ -98,4 +97,4 @@ - primer_steps.append(Extruder(on=False)) - primer_steps.append(first_infinaxis_point(steps)) # move fast to start position - primer_steps.append(Extruder(on=True)) -- self.steps = initialization_data['starting_procedure_steps'] + primer_steps + steps + initialization_data['ending_procedure_steps']+ self.steps = initialization_data['starting_procedure_steps'] + primer_steps + steps + initialization_data['ending_procedure_steps'] -``` - -## `lab/fullcontrol/infaxis/steps2gcode.py` -> `lab/fullcontrol/infinaxis/steps2gcode.py` - -```diff ---- lab/fullcontrol/infinaxis/steps2gcode.py -+++ lab/fullcontrol/infinaxis/steps2gcode.py -@@ -2,8 +2,8 @@ - import os - from datetime import datetime - --from infinaxis.state import State --from infinaxis.controls import GcodeControls -+from lab.fullcontrol.infinaxis.state import State -+from lab.fullcontrol.infinaxis.controls import GcodeControls - - - def gcode(steps: list, gcode_controls: GcodeControls = GcodeControls()): -@@ -22,4 +22,4 @@ - filename = gcode_controls.save_as + datetime.now().strftime("__%d-%m-%Y__%H-%M-%S.gcode") - open(filename, 'w').write(gc) - else: -- return gc+ return gc -``` - -## `tutorials/lab_infaxis_4_demo.ipynb` -> `tutorials/lab_infinaxis_4_demo.ipynb` - -```diff ---- tutorials/lab_infinaxis_4_demo.ipynb -+++ tutorials/lab_infinaxis_4_demo.ipynb -@@ -8,13 +8,7 @@ - "outputs": [], - "source": [ - "import lab.fullcontrol.infinaxis as fci\n", -- "import fullcontrol as fc\n", -- "from math import sin, cos, tau\n", -- "\n", -- "fc.Point = fci.Point\n", -- "fc.GcodeControls = fci.GcodeControls\n", -- "fc.transform = fci.transform\n", -- "fc.Axis = fci.Axis" -+ "from math import sin, cos, tau\n" - ] - }, - { -@@ -28,9 +22,13 @@ - "EH = 0.3\n", - "\n", - "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", -- "gcode_controls = fc.GcodeControls(\n", -- " head_chain = [],\n", -- " bed_chain = [fc.Axis(name='C')],\n", -+ "head_chain = []\n", -+ "bed_chain = [fci.Axis(name='C')]\n", -+ "Point = fci.configure_point(head_chain, bed_chain)\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", - " initialization_data=print_settings \n", - " )" - ] -@@ -50,20 +48,20 @@ - "\n", - "\n", - "steps = []\n", -- "steps.append(fc.Printer(print_speed=2160))\n", -+ "steps.append(fci.Printer(print_speed=2160))\n", - "for i in range(layers):\n", - " for j in range(density):\n", - " angle = 360*(i+j/density)\n", -- " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", -+ " steps.append(Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, c=angle))\n", - "\n", - "for step in steps:\n", -- " if type(step).__name__ == 'Point':\n", -+ " if isinstance(step, fci.Point):\n", - " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", -- " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", -+ " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", - "\n", -- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", -+ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", - "fig.show()\n", -- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", - "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", - "print('')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" -``` - -## `tutorials/lab_infaxis_5_demo.ipynb` -> `tutorials/lab_infinaxis_5_demo.ipynb` - -```diff ---- tutorials/lab_infinaxis_5_demo.ipynb -+++ tutorials/lab_infinaxis_5_demo.ipynb -@@ -8,13 +8,7 @@ - "outputs": [], - "source": [ - "import lab.fullcontrol.infinaxis as fci\n", -- "import fullcontrol as fc\n", -- "from math import sin, cos, tau\n", -- "\n", -- "fc.Point = fci.Point\n", -- "fc.GcodeControls = fci.GcodeControls\n", -- "fc.transform = fci.transform\n", -- "fc.Axis = fci.Axis" -+ "from math import sin, cos, tau\n" - ] - }, - { -@@ -28,11 +22,13 @@ - "EH = 0.3\n", - "\n", - "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", -- "gcode_controls = fc.GcodeControls(\n", -- " head_chain = [fc.Axis(name='B')],\n", -- " bed_chain = [fc.Axis(name='C')],\n", -- " distance_axis = True,\n", -- " verbose = True,\n", -+ "head_chain = [fci.Axis(name='B')]\n", -+ "bed_chain = [fci.Axis(name='C')]\n", -+ "Point = fci.configure_point(head_chain, bed_chain)\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", - " initialization_data=print_settings \n", - " )" - ] -@@ -53,17 +49,17 @@ - " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", - " dr = r_next - r\n", - " dz = p_next.z-p.z\n", -- " ptilt = p.axes['B']\n", -- " dtilt = p_next.axes['B']-ptilt\n", -+ " ptilt = p.b\n", -+ " dtilt = p_next.b-ptilt\n", - " \n", -- " dc = p_next.axes['C']-p.axes['C']\n", -+ " dc = p_next.c-p.c\n", - " for j in range(density):\n", -- " angle = p.axes['C'] + dc * j / density\n", -+ " angle = p.c + dc * j / density\n", - " tilt = ptilt + dtilt * j / density\n", - " r_final = r + dr * j / density\n", - " z_final = p.z + dz * j / density\n", - " \n", -- " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", -+ " steps.append(Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, b=tilt, c=angle))\n", - " \n", - " return steps\n", - "\n", -@@ -87,45 +83,85 @@ - " angle = i * 360\n", - " z = z_start + h * i\n", - "\n", -- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", -+ " trace.append(Point(x=r, y=0, z=z, b=tilt, c=angle))\n", - "\n", -- "angle_offset = fc.last_point(trace).axes['C']\n", -- "z_offset = fc.last_point(trace).z\n", -+ "angle_offset = fci.last_point(trace).c\n", -+ "z_offset = fci.last_point(trace).z\n", - "\n", - "for i in range(1,arc_layers):\n", - " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", - " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", - " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", - " angle = i * 360 + angle_offset\n", -- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", -+ " trace.append(Point(x=r, y=0, z=z, b=-tilt, c=angle))\n", - "\n", -- "angle_offset = fc.last_point(trace).axes['C']\n", -- "z_offset = fc.last_point(trace).z\n", -- "r_offset = fc.last_point(trace).x\n", -+ "angle_offset = fci.last_point(trace).c\n", -+ "z_offset = fci.last_point(trace).z\n", -+ "r_offset = fci.last_point(trace).x\n", - "\n", - "for i in range(1,layers_tilted):\n", - " tilt = tilt_end\n", - " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", - " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", - " angle = i * 360 + angle_offset\n", -- " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", -+ " trace.append(Point(x=0, y=r, z=z, b=-tilt, c=angle))\n", - "\n", - "steps = vase_from_trace(trace, density)\n", -- "steps.append(fc.last_point(trace))\n", -+ "steps.append(fci.last_point(trace))\n", - "\n", - "for step in steps:\n", -- " if type(step).__name__ == 'Point':\n", -+ " if isinstance(step, fci.Point):\n", - " # color is a gradient from A=0 (blue) to A=90 (red)\n", -- " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", -+ " step.color = [((abs(step.b))/90), 0, 1-((abs(step.b))/90)]\n", - "\n", -- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", -+ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", - "fig.show()\n", - "\n", -- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", - "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", - "print('')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" - ] -+ }, -+ { -+ "cell_type": "code", -+ "execution_count": null, -+ "id": "9462651e", -+ "metadata": {}, -+ "outputs": [], -+ "source": [ -+ "# with and without 'verbose'\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", -+ " verbose = False,\n", -+ " initialization_data=print_settings \n", -+ " )\n", -+ "\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", -+ "print('___\\nwith GcodeControls(verbose=False):')\n", -+ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", -+ " verbose = True,\n", -+ " initialization_data=print_settings \n", -+ " )\n", -+ "\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", -+ "print('\\n___\\nwith GcodeControls(verbose=True):')\n", -+ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" -+ ] -+ }, -+ { -+ "cell_type": "code", -+ "execution_count": null, -+ "id": "aaf9ec1a", -+ "metadata": {}, -+ "outputs": [], -+ "source": [] - } - ], - "metadata": { -``` - -## `tutorials/colab/lab_infaxis_4_demo_colab.ipynb` -> `tutorials/colab/lab_infinaxis_4_demo_colab.ipynb` - -```diff ---- tutorials/colab/lab_infinaxis_4_demo_colab.ipynb -+++ tutorials/colab/lab_infinaxis_4_demo_colab.ipynb -@@ -8,13 +8,7 @@ - "outputs": [], - "source": [ - "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", -- "import fullcontrol as fc\n", -- "from math import sin, cos, tau\n", -- "\n", -- "fc.Point = fci.Point\n", -- "fc.GcodeControls = fci.GcodeControls\n", -- "fc.transform = fci.transform\n", -- "fc.Axis = fci.Axis" -+ "from math import sin, cos, tau\n" - ] - }, - { -@@ -28,9 +22,13 @@ - "EH = 0.3\n", - "\n", - "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", -- "gcode_controls = fc.GcodeControls(\n", -- " head_chain = [],\n", -- " bed_chain = [fc.Axis(name='C')],\n", -+ "head_chain = []\n", -+ "bed_chain = [fci.Axis(name='C')]\n", -+ "Point = fci.configure_point(head_chain, bed_chain)\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", - " initialization_data=print_settings \n", - " )" - ] -@@ -50,20 +48,20 @@ - "\n", - "\n", - "steps = []\n", -- "steps.append(fc.Printer(print_speed=2160))\n", -+ "steps.append(fci.Printer(print_speed=2160))\n", - "for i in range(layers):\n", - " for j in range(density):\n", - " angle = 360*(i+j/density)\n", -- " steps.append(fc.Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, axes={'C':angle}))\n", -+ " steps.append(Point(x=r*sin(angle/360*tau), y=r*cos(angle/360*tau), z=((i+j/density)*h)+z_start, c=angle))\n", - "\n", - "for step in steps:\n", -- " if type(step).__name__ == 'Point':\n", -+ " if isinstance(step, fci.Point):\n", - " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", -- " step.color = [((step.axes['C']%360)/360), 0, 1-((step.axes['C']%360)/360)]\n", -+ " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", - "\n", -- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", -+ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", - "fig.show()\n", -- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", - "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", - "print('')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" -``` - -## `tutorials/colab/lab_infaxis_5_demo_colab.ipynb` -> `tutorials/colab/lab_infinaxis_5_demo_colab.ipynb` - -```diff ---- tutorials/colab/lab_infinaxis_5_demo_colab.ipynb -+++ tutorials/colab/lab_infinaxis_5_demo_colab.ipynb -@@ -8,13 +8,7 @@ - "outputs": [], - "source": [ - "if 'google.colab' in str(get_ipython()):\n !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport lab.fullcontrol.infinaxis as fci\n", -- "import fullcontrol as fc\n", -- "from math import sin, cos, tau\n", -- "\n", -- "fc.Point = fci.Point\n", -- "fc.GcodeControls = fci.GcodeControls\n", -- "fc.transform = fci.transform\n", -- "fc.Axis = fci.Axis" -+ "from math import sin, cos, tau\n" - ] - }, - { -@@ -28,11 +22,13 @@ - "EH = 0.3\n", - "\n", - "print_settings = {'extrusion_width': EW,'extrusion_height': EH}\n", -- "gcode_controls = fc.GcodeControls(\n", -- " head_chain = [fc.Axis(name='B')],\n", -- " bed_chain = [fc.Axis(name='C')],\n", -- " distance_axis = True,\n", -- " verbose = True,\n", -+ "head_chain = [fci.Axis(name='B')]\n", -+ "bed_chain = [fci.Axis(name='C')]\n", -+ "Point = fci.configure_point(head_chain, bed_chain)\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", - " initialization_data=print_settings \n", - " )" - ] -@@ -53,17 +49,17 @@ - " r_next = (p_next.x**2 + p_next.y**2)**0.5\n", - " dr = r_next - r\n", - " dz = p_next.z-p.z\n", -- " ptilt = p.axes['B']\n", -- " dtilt = p_next.axes['B']-ptilt\n", -+ " ptilt = p.b\n", -+ " dtilt = p_next.b-ptilt\n", - " \n", -- " dc = p_next.axes['C']-p.axes['C']\n", -+ " dc = p_next.c-p.c\n", - " for j in range(density):\n", -- " angle = p.axes['C'] + dc * j / density\n", -+ " angle = p.c + dc * j / density\n", - " tilt = ptilt + dtilt * j / density\n", - " r_final = r + dr * j / density\n", - " z_final = p.z + dz * j / density\n", - " \n", -- " steps.append(fc.Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, axes={'B':tilt,'C':angle}))\n", -+ " steps.append(Point(x=r_final*sin(angle/360*tau), y=r_final*cos(angle/360*tau), z=z_final, b=tilt, c=angle))\n", - " \n", - " return steps\n", - "\n", -@@ -87,45 +83,85 @@ - " angle = i * 360\n", - " z = z_start + h * i\n", - "\n", -- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':tilt,'C':angle}))\n", -+ " trace.append(Point(x=r, y=0, z=z, b=tilt, c=angle))\n", - "\n", -- "angle_offset = fc.last_point(trace).axes['C']\n", -- "z_offset = fc.last_point(trace).z\n", -+ "angle_offset = fci.last_point(trace).c\n", -+ "z_offset = fci.last_point(trace).z\n", - "\n", - "for i in range(1,arc_layers):\n", - " tilt = tilt_start + (tilt_end - tilt_start) * i / (arc_layers - 1)\n", - " r = r_start+r_tilt*(1-cos(tilt*tau/360))\n", - " z = z_offset+r_tilt*(sin(tilt*tau/360))\n", - " angle = i * 360 + angle_offset\n", -- " trace.append(fc.Point(x=r, y=0, z=z, axes={'B':-tilt,'C':angle}))\n", -+ " trace.append(Point(x=r, y=0, z=z, b=-tilt, c=angle))\n", - "\n", -- "angle_offset = fc.last_point(trace).axes['C']\n", -- "z_offset = fc.last_point(trace).z\n", -- "r_offset = fc.last_point(trace).x\n", -+ "angle_offset = fci.last_point(trace).c\n", -+ "z_offset = fci.last_point(trace).z\n", -+ "r_offset = fci.last_point(trace).x\n", - "\n", - "for i in range(1,layers_tilted):\n", - " tilt = tilt_end\n", - " r = r_offset + d_tilted * i / (layers_tilted - 1) * (sin(tilt*tau/360))\n", - " z = z_offset + d_tilted * i / (layers_tilted - 1) * cos(tilt*tau/360)\n", - " angle = i * 360 + angle_offset\n", -- " trace.append(fc.Point(x=0, y=r, z=z, axes={'B':-tilt,'C':angle}))\n", -+ " trace.append(Point(x=0, y=r, z=z, b=-tilt, c=angle))\n", - "\n", - "steps = vase_from_trace(trace, density)\n", -- "steps.append(fc.last_point(trace))\n", -+ "steps.append(fci.last_point(trace))\n", - "\n", - "for step in steps:\n", -- " if type(step).__name__ == 'Point':\n", -+ " if isinstance(step, fci.Point):\n", - " # color is a gradient from A=0 (blue) to A=90 (red)\n", -- " step.color = [((abs(step.axes['B']))/90), 0, 1-((abs(step.axes['B']))/90)]\n", -+ " step.color = [((abs(step.b))/90), 0, 1-((abs(step.b))/90)]\n", - "\n", -- "fig = fc.transform(steps, 'fig', fc.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", -+ "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", - "fig.show()\n", - "\n", -- "gcode = fc.transform(steps,'gcode',gcode_controls)\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", - "print('first ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[:10]))\n", - "print('')\n", - "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" - ] -+ }, -+ { -+ "cell_type": "code", -+ "execution_count": null, -+ "id": "9462651e", -+ "metadata": {}, -+ "outputs": [], -+ "source": [ -+ "# with and without 'verbose'\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", -+ " verbose = False,\n", -+ " initialization_data=print_settings \n", -+ " )\n", -+ "\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", -+ "print('___\\nwith GcodeControls(verbose=False):')\n", -+ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))\n", -+ "\n", -+ "gcode_controls = fci.GcodeControls(\n", -+ " head_chain = head_chain,\n", -+ " bed_chain = bed_chain,\n", -+ " verbose = True,\n", -+ " initialization_data=print_settings \n", -+ " )\n", -+ "\n", -+ "gcode = fci.transform(steps,'gcode',gcode_controls)\n", -+ "print('\\n___\\nwith GcodeControls(verbose=True):')\n", -+ "print('final ten gcode lines:\\n' + '\\n'.join(gcode.split('\\n')[-10:]))" -+ ] -+ }, -+ { -+ "cell_type": "code", -+ "execution_count": null, -+ "id": "aaf9ec1a", -+ "metadata": {}, -+ "outputs": [], -+ "source": [] - } - ], - "metadata": { -``` - -## New files without old `infaxis` equivalent - -- `lab/fullcontrol/infinaxis/_plot.py`: present -- `lab/fullcontrol/infinaxis/xyz_add_axes.py`: present -