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 5711bac8..7e5c8952 100644 --- a/bin/colab_tutorials.py +++ b/bin/colab_tutorials.py @@ -17,7 +17,13 @@ "lab_four_axis_demo.ipynb", "lab_five_axis_demo.ipynb", "lab_stl_output.ipynb", - "lab_3mf_output.ipynb"] + "lab_3mf_output.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] @@ -34,6 +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_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' @@ -55,6 +63,7 @@ content_string = content_string.replace(old_import_5ax2, new_import_5ax2) 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/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/infinaxis/_plot.py b/lab/fullcontrol/infinaxis/_plot.py new file mode 100644 index 00000000..c36fcd78 --- /dev/null +++ b/lab/fullcontrol/infinaxis/_plot.py @@ -0,0 +1,279 @@ +import numpy as np +import plotly.graph_objects as go + +from fullcontrol.visualize.plotly import generate_mesh +from fullcontrol.visualize.tube_mesh import CylindersMesh, FlowTubeMesh + + +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, +): + 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), + ), + showlegend=False, + ) + + +def fig_plot(data, controls): + fig = go.Figure() + + if controls.tube_type is not None: + Mesh = {"flow": FlowTubeMesh, "cylinders": CylindersMesh}[controls.tube_type] + else: + Mesh = FlowTubeMesh + + 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 + + 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), + )) + + 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 + + annotations_pts = [] + annotations = [] + 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"))) + + 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: + 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) + + return fig diff --git a/lab/fullcontrol/infinaxis/axis.py b/lab/fullcontrol/infinaxis/axis.py new file mode 100644 index 00000000..7cfdb95f --- /dev/null +++ b/lab/fullcontrol/infinaxis/axis.py @@ -0,0 +1,25 @@ +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.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 + 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/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/infinaxis/controls.py b/lab/fullcontrol/infinaxis/controls.py new file mode 100644 index 00000000..2cee587a --- /dev/null +++ b/lab/fullcontrol/infinaxis/controls.py @@ -0,0 +1,17 @@ +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. + # 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/point.py b/lab/fullcontrol/infinaxis/point.py new file mode 100644 index 00000000..c77b8265 --- /dev/null +++ b/lab/fullcontrol/infinaxis/point.py @@ -0,0 +1,221 @@ +from typing import Optional +from fullcontrol import Point as BasePoint +from copy import deepcopy +import numpy as np + +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 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 = '' + 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 + 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) + + #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) + 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) + + 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 + # 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: + 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 + 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 diff --git a/lab/fullcontrol/infinaxis/printer.py b/lab/fullcontrol/infinaxis/printer.py new file mode 100644 index 00000000..c29f9e10 --- /dev/null +++ b/lab/fullcontrol/infinaxis/printer.py @@ -0,0 +1,33 @@ +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. + # 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 + + 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/infinaxis/state.py b/lab/fullcontrol/infinaxis/state.py new file mode 100644 index 00000000..b288cfce --- /dev/null +++ b/lab/fullcontrol/infinaxis/state.py @@ -0,0 +1,100 @@ +from typing import Optional +from pydantic import BaseModel +from importlib import import_module + +from fullcontrol.gcode.extrusion_classes import ExtrusionGeometry, Extruder + +from lab.fullcontrol.infinaxis.point import Point +from lab.fullcontrol.infinaxis.printer import Printer +from lab.fullcontrol.infinaxis.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_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 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 fully defined x, y, and z') + 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_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'] diff --git a/lab/fullcontrol/infinaxis/steps2gcode.py b/lab/fullcontrol/infinaxis/steps2gcode.py new file mode 100644 index 00000000..c4e180d0 --- /dev/null +++ b/lab/fullcontrol/infinaxis/steps2gcode.py @@ -0,0 +1,25 @@ + +import os +from datetime import datetime + +from lab.fullcontrol.infinaxis.state import State +from lab.fullcontrol.infinaxis.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 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/infinaxis_4axis_demo_colab.ipynb b/tutorials/colab/infinaxis_4axis_demo_colab.ipynb new file mode 100644 index 00000000..e5ca75d6 --- /dev/null +++ b/tutorials/colab/infinaxis_4axis_demo_colab.ipynb @@ -0,0 +1,92 @@ +{ + "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", + "from math import sin, cos, tau\n" + ] + }, + { + "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", + "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", + " )" + ] + }, + { + "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(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(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 isinstance(step, fci.Point):\n", + " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", + " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", + "\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\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:]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc_dev (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_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/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/infinaxis_4axis_demo.ipynb b/tutorials/infinaxis_4axis_demo.ipynb new file mode 100644 index 00000000..ef5a7944 --- /dev/null +++ b/tutorials/infinaxis_4axis_demo.ipynb @@ -0,0 +1,92 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9cad98ab", + "metadata": {}, + "outputs": [], + "source": [ + "import lab.fullcontrol.infinaxis as fci\n", + "from math import sin, cos, tau\n" + ] + }, + { + "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", + "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", + " )" + ] + }, + { + "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(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(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 isinstance(step, fci.Point):\n", + " # color is a gradient from C=0 deg (blue) to C=360 (red)\n", + " step.color = [((step.c%360)/360), 0, 1-((step.c%360)/360)]\n", + "\n", + "fig = fci.transform(steps, 'fig', fci.PlotControls(color_type='manual',style='tube', zoom=0.75), show_tips=False)\n", + "fig.show()\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:]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fc_dev (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_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 }