Skip to content

Print Organization

Fabrication parameter assignment and G-code generation.

print_organization

Print organization for embedding fabrication parameters into toolpaths.

BasePrintOrganizer

BasePrintOrganizer(slicer)

Base class for organizing the printing process.

This class is meant to be extended for implementing various print organizers. Do not use this class directly. Use PlanarPrintOrganizer or InterpolationPrintOrganizer.

Attributes:

Name Type Description
slicer BaseSlicer

An instance of a slicer class.

printpoints PrintPointsCollection

Collection of printpoints organized by layer and path.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def __init__(self, slicer: BaseSlicer) -> None:
    if not isinstance(slicer, BaseSlicer):
        raise TypeError(f"slicer must be BaseSlicer, not {type(slicer)}")
    logger.info("Print Organizer")
    self.slicer = slicer
    self.printpoints = PrintPointsCollection()

number_of_printpoints property

number_of_printpoints

Total number of printpoints.

number_of_paths property

number_of_paths

Total number of paths.

number_of_layers property

number_of_layers

Number of layers.

total_length_of_paths property

total_length_of_paths

Total length of all paths (ignores extruder toggle).

total_print_time property

total_print_time

Total print time if velocity is defined, else None.

printpoints_dict property

printpoints_dict

Legacy accessor for the old dict format. Prefer using self.printpoints directly.

create_printpoints abstractmethod

create_printpoints()

To be implemented by inheriting classes.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
@abstractmethod
def create_printpoints(self) -> None:
    """To be implemented by inheriting classes."""
    pass

printpoints_iterator

printpoints_iterator()

Iterate over all printpoints.

Yields:

Type Description
PrintPoint

Each printpoint in the organizer.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def printpoints_iterator(self) -> Generator[PrintPoint, None, None]:
    """Iterate over all printpoints.

    Yields
    ------
    PrintPoint
        Each printpoint in the organizer.

    """
    if not self.printpoints.layers:
        raise ValueError("No printpoints have been created.")
    yield from self.printpoints.iter_printpoints()

printpoints_indices_iterator

printpoints_indices_iterator()

Iterate over printpoints with their indices.

Yields:

Type Description
tuple[PrintPoint, int, int, int]

Printpoint, layer index, path index, printpoint index.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def printpoints_indices_iterator(self) -> Iterator[tuple[PrintPoint, int, int, int]]:
    """Iterate over printpoints with their indices.

    Yields
    ------
    tuple[PrintPoint, int, int, int]
        Printpoint, layer index, path index, printpoint index.

    """
    if not self.printpoints.layers:
        raise ValueError("No printpoints have been created.")
    yield from self.printpoints.iter_with_indices()

number_of_paths_on_layer

number_of_paths_on_layer(layer_index)

Number of paths within a layer.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def number_of_paths_on_layer(self, layer_index: int) -> int:
    """Number of paths within a layer."""
    return len(self.printpoints[layer_index])

remove_duplicate_points_in_path

remove_duplicate_points_in_path(layer_idx, path_idx, tolerance=0.0001)

Remove subsequent points within a threshold distance.

Parameters:

Name Type Description Default
layer_idx int

The layer index.

required
path_idx int

The path index.

required
tolerance float

Distance threshold for duplicate detection.

0.0001
Source code in src/compas_slicer/print_organization/base_print_organizer.py
def remove_duplicate_points_in_path(
    self, layer_idx: int, path_idx: int, tolerance: float = 0.0001
) -> None:
    """Remove subsequent points within a threshold distance.

    Parameters
    ----------
    layer_idx : int
        The layer index.
    path_idx : int
        The path index.
    tolerance : float
        Distance threshold for duplicate detection.

    """
    dup_index = []
    duplicate_ppts = []

    path = self.printpoints[layer_idx][path_idx]
    for i, printpoint in enumerate(path.printpoints[:-1]):
        next_ppt = path.printpoints[i + 1]
        if np.linalg.norm(np.array(printpoint.pt) - np.array(next_ppt.pt)) < tolerance:
            dup_index.append(i)
            duplicate_ppts.append(printpoint)

    if duplicate_ppts:
        logger.warning(
            f"Attention! {len(duplicate_ppts)} Duplicate printpoint(s) on "
            f"layer {layer_idx}, path {path_idx}, indices: {dup_index}. They will be removed."
        )
        for ppt in duplicate_ppts:
            path.printpoints.remove(ppt)

get_printpoint_neighboring_items

get_printpoint_neighboring_items(layer_idx, path_idx, i)

Get neighboring printpoints.

Parameters:

Name Type Description Default
layer_idx int

The layer index.

required
path_idx int

The path index.

required
i int

Index of current printpoint.

required

Returns:

Type Description
list[PrintPoint | None]

Previous and next printpoints (None if at boundary).

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def get_printpoint_neighboring_items(
    self, layer_idx: int, path_idx: int, i: int
) -> list[PrintPoint | None]:
    """Get neighboring printpoints.

    Parameters
    ----------
    layer_idx : int
        The layer index.
    path_idx : int
        The path index.
    i : int
        Index of current printpoint.

    Returns
    -------
    list[PrintPoint | None]
        Previous and next printpoints (None if at boundary).

    """
    path = self.printpoints[layer_idx][path_idx]
    prev_pt = path[i - 1] if i > 0 else None
    next_pt = path[i + 1] if i < len(path) - 1 else None
    return [prev_pt, next_pt]

printout_info

printout_info()

Print information about the PrintOrganizer.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def printout_info(self) -> None:
    """Print information about the PrintOrganizer."""
    ppts_attributes = {
        key: str(type(val))
        for key, val in self.printpoints[0][0][0].attributes.items()
    }

    logger.info("---- PrintOrganizer Info ----")
    logger.info(f"Number of layers: {self.number_of_layers}")
    logger.info(f"Number of paths: {self.number_of_paths}")
    logger.info(f"Number of PrintPoints: {self.number_of_printpoints}")
    logger.info("PrintPoints attributes: ")
    for key, val in ppts_attributes.items():
        logger.info(f"     {key} : {val}")
    logger.info(f"Toolpath length: {self.total_length_of_paths:.0f} mm")

    print_time = self.total_print_time
    if print_time:
        minutes, sec = divmod(print_time, 60)
        hour, minutes = divmod(minutes, 60)
        logger.info(f"Total print time: {int(hour)} hours, {int(minutes)} minutes, {int(sec)} seconds")
    else:
        logger.info("Print Velocity has not been assigned, thus print time is not calculated.")

get_printpoint_up_vector

get_printpoint_up_vector(path, k, normal)

Get printpoint up-vector orthogonal to path direction and normal.

Parameters:

Name Type Description Default
path Path

The path containing the point.

required
k int

Index of the point in path.points.

required
normal Vector

The normal vector.

required

Returns:

Type Description
Vector

The up vector.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def get_printpoint_up_vector(self, path: Path, k: int, normal: Vector) -> Vector:
    """Get printpoint up-vector orthogonal to path direction and normal.

    Parameters
    ----------
    path : Path
        The path containing the point.
    k : int
        Index of the point in path.points.
    normal : Vector
        The normal vector.

    Returns
    -------
    Vector
        The up vector.

    """
    p = path.points[k]
    if k < len(path.points) - 1:
        negative = False
        other_pt = path.points[k + 1]
    else:
        negative = True
        other_pt = path.points[k - 1]

    diff = normalize_vector(subtract_vectors(p, other_pt))
    up_vec = normalize_vector(cross_vectors(normal, diff))

    if negative:
        up_vec = scale_vector(up_vec, -1.0)
    if norm_vector(up_vec) == 0:
        up_vec = Vector(0, 0, 1)

    return Vector(*up_vec)

output_printpoints_dict

output_printpoints_dict()

Create a flattened printpoints dictionary.

Returns:

Type Description
dict

Flattened printpoints data for JSON serialization.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def output_printpoints_dict(self) -> dict[int, dict[str, Any]]:
    """Create a flattened printpoints dictionary.

    Returns
    -------
    dict
        Flattened printpoints data for JSON serialization.

    """
    data = {}
    count = 0

    for i, layer in enumerate(self.printpoints):
        for j, path in enumerate(layer):
            self.remove_duplicate_points_in_path(i, j)
            for printpoint in path:
                data[count] = printpoint.to_data()
                count += 1

    logger.info(f"Generated {count} print points")
    return data

output_nested_printpoints_dict

output_nested_printpoints_dict()

Create a nested printpoints dictionary.

Returns:

Type Description
dict

Nested printpoints data for JSON serialization.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def output_nested_printpoints_dict(self) -> dict[str, dict[str, dict[int, dict[str, Any]]]]:
    """Create a nested printpoints dictionary.

    Returns
    -------
    dict
        Nested printpoints data for JSON serialization.

    """
    data: dict[str, dict[str, dict[int, dict[str, Any]]]] = {}
    count = 0

    for i, layer in enumerate(self.printpoints):
        layer_key = f"layer_{i}"
        data[layer_key] = {}
        for j, path in enumerate(layer):
            path_key = f"path_{j}"
            data[layer_key][path_key] = {}
            self.remove_duplicate_points_in_path(i, j)
            for k, printpoint in enumerate(path):
                data[layer_key][path_key][k] = printpoint.to_data()
                count += 1

    logger.info(f"Generated {count} print points")
    return data

output_gcode

output_gcode(config=None)

Generate G-code text.

Parameters:

Name Type Description Default
config GcodeConfig | None

G-code configuration. If None, uses defaults.

None

Returns:

Type Description
str

G-code text.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def output_gcode(self, config: GcodeConfig | None = None) -> str:
    """Generate G-code text.

    Parameters
    ----------
    config : GcodeConfig | None
        G-code configuration. If None, uses defaults.

    Returns
    -------
    str
        G-code text.

    """
    return create_gcode_text(self, config)

get_printpoints_attribute

get_printpoints_attribute(attr_name)

Get a list of attribute values from all printpoints.

Parameters:

Name Type Description Default
attr_name str

Name of the attribute.

required

Returns:

Type Description
list

Attribute values from all printpoints.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def get_printpoints_attribute(self, attr_name: str) -> list[Any]:
    """Get a list of attribute values from all printpoints.

    Parameters
    ----------
    attr_name : str
        Name of the attribute.

    Returns
    -------
    list
        Attribute values from all printpoints.

    """
    attr_values = []
    for pp in self.printpoints.iter_printpoints():
        if attr_name not in pp.attributes:
            raise KeyError(f"Attribute '{attr_name}' not in printpoint.attributes")
        attr_values.append(pp.attributes[attr_name])
    return attr_values

InterpolationPrintOrganizer

InterpolationPrintOrganizer(slicer, config=None, DATA_PATH='.')

Bases: BasePrintOrganizer

Organize the printing process for non-planar contours.

Attributes:

Name Type Description
slicer InterpolationSlicer

An instance of InterpolationSlicer.

config InterpolationConfig

Interpolation configuration.

DATA_PATH str | Path

Data directory path.

vertical_layers list[VerticalLayer]

Vertical layers from slicer.

horizontal_layers list[Layer]

Horizontal layers from slicer.

base_boundaries list[BaseBoundary]

Base boundaries for each vertical layer.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def __init__(
    self,
    slicer: InterpolationSlicer,
    config: InterpolationConfig | None = None,
    DATA_PATH: str | FilePath = ".",
) -> None:
    from compas_slicer.slicers import InterpolationSlicer

    if not isinstance(slicer, InterpolationSlicer):
        raise TypeError('Please provide an InterpolationSlicer')
    BasePrintOrganizer.__init__(self, slicer)
    self.DATA_PATH = DATA_PATH
    self.OUTPUT_PATH = utils.get_output_directory(DATA_PATH)
    self.config = config if config else InterpolationConfig()

    self.vertical_layers = slicer.vertical_layers
    self.horizontal_layers = slicer.horizontal_layers
    if len(self.vertical_layers) + len(self.horizontal_layers) != len(slicer.layers):
        raise ValueError(
            f"Layer count mismatch: {len(self.vertical_layers)} vertical + "
            f"{len(self.horizontal_layers)} horizontal != {len(slicer.layers)} total"
        )

    if len(self.horizontal_layers) > 0:
        if len(self.horizontal_layers) != 1:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        if not self.horizontal_layers[0].is_brim:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        logger.info('Slicer has one horizontal brim layer.')

    # topological sorting of vertical layers depending on their connectivity
    self.topo_sort_graph: topo_sort.SegmentsDirectedGraph | None = None
    if len(self.vertical_layers) > 1:
        try:
            self.topological_sorting()
        except AssertionError:
            logger.exception("topology sorting failed\n")
            logger.critical("integrity of the output data ")
            # TODO: perhaps its better to be even more explicit and add a
            #  FAILED-timestamp.txt file?
    self.selected_order: list[int] | None = None

    # creation of one base boundary per vertical_layer
    self.base_boundaries: list[BaseBoundary] = self.create_base_boundaries()

topological_sorting

topological_sorting()

Create directed graph of parts with connectivity.

Creates a directed graph where each part's connectivity reflects which other parts it lies on and which other parts lie on it.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def topological_sorting(self) -> None:
    """Create directed graph of parts with connectivity.

    Creates a directed graph where each part's connectivity reflects which
    other parts it lies on and which other parts lie on it.

    """
    avg_layer_height = self.config.avg_layer_height
    self.topo_sort_graph = topo_sort.SegmentsDirectedGraph(self.slicer.mesh, self.vertical_layers,
                                                           4 * avg_layer_height, DATA_PATH=self.DATA_PATH)

create_base_boundaries

create_base_boundaries()

Create one BaseBoundary per vertical_layer.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def create_base_boundaries(self) -> list[BaseBoundary]:
    """Create one BaseBoundary per vertical_layer."""
    bs: list[BaseBoundary] = []
    root_vs = utils.get_mesh_vertex_coords_with_attribute(self.slicer.mesh, 'boundary', 1)
    root_boundary = BaseBoundary(self.slicer.mesh, [Point(*v) for v in root_vs])

    if len(self.vertical_layers) > 1 and self.topo_sort_graph is not None:
        for i, _vertical_layer in enumerate(self.vertical_layers):
            parents_of_current_node = self.topo_sort_graph.get_parents_of_node(i)
            if len(parents_of_current_node) == 0:
                boundary = root_boundary
            else:
                boundary_pts = []
                for parent_index in parents_of_current_node:
                    parent = self.vertical_layers[parent_index]
                    boundary_pts.extend(parent.paths[-1].points)
                boundary = BaseBoundary(self.slicer.mesh, boundary_pts)
            bs.append(boundary)
    else:
        bs.append(root_boundary)

    # save intermediary outputs
    b_data = {i: b.to_data() for i, b in enumerate(bs)}
    utils.save_to_json(b_data, self.OUTPUT_PATH, 'boundaries.json')

    return bs

create_printpoints

create_printpoints()

Create the print points of the fabrication process.

Based on the directed graph, select one topological order. From each path collection in that order, copy PrintPoints in the correct order.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def create_printpoints(self) -> None:
    """Create the print points of the fabrication process.

    Based on the directed graph, select one topological order.
    From each path collection in that order, copy PrintPoints in the correct order.

    """
    current_layer_index = 0

    # (1) --- First add the printpoints of the horizontal brim layer (first layer of print)
    if len(self.horizontal_layers) > 0:  # first add horizontal brim layers
        print_layer = PrintLayer()
        paths = self.horizontal_layers[0].paths
        for _j, path in enumerate(paths):
            print_path = PrintPath(printpoints=[
                PrintPoint(pt=point, layer_height=self.config.avg_layer_height,
                           mesh_normal=utils.get_normal_of_path_on_xy_plane(k, point, path, self.slicer.mesh))
                for k, point in enumerate(path.points)
            ])
            print_layer.paths.append(print_path)
        self.printpoints.layers.append(print_layer)
        current_layer_index += 1
    else:
        # Add empty first layer placeholder if no horizontal layers
        pass

    # (2) --- Select order of vertical layers
    if len(self.vertical_layers) > 1:  # then you need to select one topological order

        if not self.topo_sort_graph:
            logger.error("no topology graph found, cannnot set the order of vertical layers")
            self.selected_order = [0]
        else:
            all_orders = self.topo_sort_graph.get_all_topological_orders()
            self.selected_order = all_orders[0]  # TODO: add more elaborate selection strategy
    else:
        self.selected_order = [0]  # there is only one segment, only this option

    # (3) --- Then create the printpoints of all the vertical layers in the selected order
    if self.selected_order is None:
        raise RuntimeError("selected_order must be set before creating printpoints")
    for _index, i in enumerate(self.selected_order):
        layer = self.vertical_layers[i]
        print_layer = self.get_layer_ppts(layer, self.base_boundaries[i])
        self.printpoints.layers.append(print_layer)
        current_layer_index += 1

get_layer_ppts

get_layer_ppts(layer, base_boundary)

Create the PrintPoints of a single layer.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def get_layer_ppts(self, layer: VerticalLayer, base_boundary: BaseBoundary) -> PrintLayer:
    """Create the PrintPoints of a single layer."""
    max_layer_height = self.config.max_layer_height
    min_layer_height = self.config.min_layer_height
    avg_layer_height = self.config.avg_layer_height

    all_pts = [pt for path in layer.paths for pt in path.points]
    closest_fks, projected_pts = utils.pull_pts_to_mesh_faces(self.slicer.mesh, all_pts)
    normals = [Vector(*self.slicer.mesh.face_normal(fkey)) for fkey in closest_fks]

    count = 0
    support_polyline_pts = base_boundary.points  # Start with base boundary

    print_layer = PrintLayer()
    for _i, path in enumerate(layer.paths):
        # Batch query: find closest points for all points in this path at once
        closest_pts, distances = _batch_closest_points_on_polyline(
            path.points, support_polyline_pts
        )

        print_path = PrintPath()
        for k, p in enumerate(path.points):
            cp = closest_pts[k]
            d = distances[k]

            normal = normals[count]
            ppt = PrintPoint(pt=p, layer_height=avg_layer_height, mesh_normal=normal)

            ppt.closest_support_pt = Point(cp[0], cp[1], cp[2])
            ppt.distance_to_support = d
            ppt.layer_height = max(min(d, max_layer_height), min_layer_height)
            ppt.up_vector = self.get_printpoint_up_vector(path, k, normal)
            if dot_vectors(subtract_vectors(p, ppt.closest_support_pt), ppt.up_vector) < 0:
                ppt.up_vector = Vector(*scale_vector(ppt.up_vector, -1))
            ppt.frame = ppt.get_frame()

            print_path.printpoints.append(ppt)
            count += 1

        print_layer.paths.append(print_path)
        support_polyline_pts = path.points  # Next path checks against this one

    return print_layer

PlanarPrintOrganizer

PlanarPrintOrganizer(slicer)

Bases: BasePrintOrganizer

Organize the printing process for planar contours.

Attributes:

Name Type Description
slicer PlanarSlicer

An instance of PlanarSlicer.

Source code in src/compas_slicer/print_organization/planar_print_organizer.py
def __init__(self, slicer: PlanarSlicer) -> None:
    from compas_slicer.slicers import PlanarSlicer

    if not isinstance(slicer, PlanarSlicer):
        raise TypeError('Please provide a PlanarSlicer')
    BasePrintOrganizer.__init__(self, slicer)

create_printpoints

create_printpoints(generate_mesh_normals=True)

Create the print points of the fabrication process.

Parameters:

Name Type Description Default
generate_mesh_normals bool

If True, compute mesh normals. If False, use Vector(0, 1, 0).

True
Source code in src/compas_slicer/print_organization/planar_print_organizer.py
def create_printpoints(self, generate_mesh_normals: bool = True) -> None:
    """Create the print points of the fabrication process.

    Parameters
    ----------
    generate_mesh_normals : bool
        If True, compute mesh normals. If False, use Vector(0, 1, 0).

    """

    count = 0
    logger.info('Creating print points ...')
    with progressbar.ProgressBar(max_value=self.slicer.number_of_points) as bar:

        if generate_mesh_normals:
            logger.info('Generating mesh normals ...')
            # fast method for getting the closest mesh normals to all the printpoints
            all_pts = [pt for layer in self.slicer.layers for path in layer.paths for pt in path.points]
            closest_fks, projected_pts = utils.pull_pts_to_mesh_faces(self.slicer.mesh, all_pts)
            normals = [Vector(*self.slicer.mesh.face_normal(fkey)) for fkey in closest_fks]

        for _i, layer in enumerate(self.slicer.layers):
            print_layer = PrintLayer()

            for _j, path in enumerate(layer.paths):
                print_path = PrintPath()

                for k, point in enumerate(path.points):

                    n = normals[count] if generate_mesh_normals else Vector(0, 1, 0)
                    layer_h = self.slicer.layer_height if self.slicer.layer_height else 2.0
                    printpoint = PrintPoint(pt=point, layer_height=layer_h, mesh_normal=n)

                    if layer.is_brim or layer.is_raft:
                        printpoint.up_vector = Vector(0, 0, 1)
                    else:
                        printpoint.up_vector = self.get_printpoint_up_vector(path, k, n)

                    print_path.printpoints.append(printpoint)
                    bar.update(count)
                    count += 1

                print_layer.paths.append(print_path)

            self.printpoints.layers.append(print_layer)

ScalarFieldPrintOrganizer

ScalarFieldPrintOrganizer(slicer, config=None, DATA_PATH='.')

Bases: BasePrintOrganizer

Organize the printing process for scalar field contours.

Attributes:

Name Type Description
slicer ScalarFieldSlicer

An instance of ScalarFieldSlicer.

config InterpolationConfig

Configuration parameters.

DATA_PATH str | Path

Data directory path.

vertical_layers list[VerticalLayer]

Vertical layers from slicer.

horizontal_layers list[Layer]

Horizontal layers from slicer.

g_evaluation GradientEvaluation

Gradient evaluation object.

Source code in src/compas_slicer/print_organization/scalar_field_print_organizer.py
def __init__(
    self,
    slicer: ScalarFieldSlicer,
    config: InterpolationConfig | None = None,
    DATA_PATH: str | FilePath = ".",
) -> None:
    from compas_slicer.slicers import ScalarFieldSlicer

    if not isinstance(slicer, ScalarFieldSlicer):
        raise TypeError('Please provide a ScalarFieldSlicer')
    BasePrintOrganizer.__init__(self, slicer)
    self.DATA_PATH = DATA_PATH
    self.OUTPUT_PATH = utils.get_output_directory(DATA_PATH)
    self.config = config if config else InterpolationConfig()

    self.vertical_layers = slicer.vertical_layers
    self.horizontal_layers = slicer.horizontal_layers
    if len(self.vertical_layers) + len(self.horizontal_layers) != len(slicer.layers):
        raise ValueError(
            f"Layer count mismatch: {len(self.vertical_layers)} vertical + "
            f"{len(self.horizontal_layers)} horizontal != {len(slicer.layers)} total"
        )

    if len(self.horizontal_layers) > 0:
        if len(self.horizontal_layers) != 1:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        if not self.horizontal_layers[0].is_brim:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        logger.info('Slicer has one horizontal brim layer.')

    self.g_evaluation: GradientEvaluation = self.add_gradient_to_vertices()

create_printpoints

create_printpoints()

Create the print points of the fabrication process.

Source code in src/compas_slicer/print_organization/scalar_field_print_organizer.py
def create_printpoints(self) -> None:
    """Create the print points of the fabrication process."""
    count = 0
    logger.info('Creating print points ...')
    with progressbar.ProgressBar(max_value=self.slicer.number_of_points) as bar:

        for _i, layer in enumerate(self.slicer.layers):
            print_layer = PrintLayer()

            for _j, path in enumerate(layer.paths):
                print_path = PrintPath()

                for k, point in enumerate(path.points):
                    normal = utils.get_normal_of_path_on_xy_plane(k, point, path, self.slicer.mesh)

                    h = self.config.avg_layer_height
                    printpoint = PrintPoint(pt=point, layer_height=h, mesh_normal=normal)

                    print_path.printpoints.append(printpoint)
                    bar.update(count)
                    count += 1

                print_layer.paths.append(print_path)

            self.printpoints.layers.append(print_layer)

    # transfer gradient information to printpoints
    transfer_mesh_attributes_to_printpoints(self.slicer.mesh, self.printpoints)

    # add non-planar print data to printpoints
    for layer in self.printpoints:
        for path in layer:
            for pp in path:
                grad_norm = pp.attributes['gradient_norm']
                grad = pp.attributes['gradient']
                pp.distance_to_support = grad_norm
                pp.layer_height = grad_norm
                pp.up_vector = Vector(*normalize_vector(grad))
                pp.frame = pp.get_frame()

base_print_organizer

BasePrintOrganizer

BasePrintOrganizer(slicer)

Base class for organizing the printing process.

This class is meant to be extended for implementing various print organizers. Do not use this class directly. Use PlanarPrintOrganizer or InterpolationPrintOrganizer.

Attributes:

Name Type Description
slicer BaseSlicer

An instance of a slicer class.

printpoints PrintPointsCollection

Collection of printpoints organized by layer and path.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def __init__(self, slicer: BaseSlicer) -> None:
    if not isinstance(slicer, BaseSlicer):
        raise TypeError(f"slicer must be BaseSlicer, not {type(slicer)}")
    logger.info("Print Organizer")
    self.slicer = slicer
    self.printpoints = PrintPointsCollection()
number_of_printpoints property
number_of_printpoints

Total number of printpoints.

number_of_paths property
number_of_paths

Total number of paths.

number_of_layers property
number_of_layers

Number of layers.

total_length_of_paths property
total_length_of_paths

Total length of all paths (ignores extruder toggle).

total_print_time property
total_print_time

Total print time if velocity is defined, else None.

printpoints_dict property
printpoints_dict

Legacy accessor for the old dict format. Prefer using self.printpoints directly.

create_printpoints abstractmethod
create_printpoints()

To be implemented by inheriting classes.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
@abstractmethod
def create_printpoints(self) -> None:
    """To be implemented by inheriting classes."""
    pass
printpoints_iterator
printpoints_iterator()

Iterate over all printpoints.

Yields:

Type Description
PrintPoint

Each printpoint in the organizer.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def printpoints_iterator(self) -> Generator[PrintPoint, None, None]:
    """Iterate over all printpoints.

    Yields
    ------
    PrintPoint
        Each printpoint in the organizer.

    """
    if not self.printpoints.layers:
        raise ValueError("No printpoints have been created.")
    yield from self.printpoints.iter_printpoints()
printpoints_indices_iterator
printpoints_indices_iterator()

Iterate over printpoints with their indices.

Yields:

Type Description
tuple[PrintPoint, int, int, int]

Printpoint, layer index, path index, printpoint index.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def printpoints_indices_iterator(self) -> Iterator[tuple[PrintPoint, int, int, int]]:
    """Iterate over printpoints with their indices.

    Yields
    ------
    tuple[PrintPoint, int, int, int]
        Printpoint, layer index, path index, printpoint index.

    """
    if not self.printpoints.layers:
        raise ValueError("No printpoints have been created.")
    yield from self.printpoints.iter_with_indices()
number_of_paths_on_layer
number_of_paths_on_layer(layer_index)

Number of paths within a layer.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def number_of_paths_on_layer(self, layer_index: int) -> int:
    """Number of paths within a layer."""
    return len(self.printpoints[layer_index])
remove_duplicate_points_in_path
remove_duplicate_points_in_path(layer_idx, path_idx, tolerance=0.0001)

Remove subsequent points within a threshold distance.

Parameters:

Name Type Description Default
layer_idx int

The layer index.

required
path_idx int

The path index.

required
tolerance float

Distance threshold for duplicate detection.

0.0001
Source code in src/compas_slicer/print_organization/base_print_organizer.py
def remove_duplicate_points_in_path(
    self, layer_idx: int, path_idx: int, tolerance: float = 0.0001
) -> None:
    """Remove subsequent points within a threshold distance.

    Parameters
    ----------
    layer_idx : int
        The layer index.
    path_idx : int
        The path index.
    tolerance : float
        Distance threshold for duplicate detection.

    """
    dup_index = []
    duplicate_ppts = []

    path = self.printpoints[layer_idx][path_idx]
    for i, printpoint in enumerate(path.printpoints[:-1]):
        next_ppt = path.printpoints[i + 1]
        if np.linalg.norm(np.array(printpoint.pt) - np.array(next_ppt.pt)) < tolerance:
            dup_index.append(i)
            duplicate_ppts.append(printpoint)

    if duplicate_ppts:
        logger.warning(
            f"Attention! {len(duplicate_ppts)} Duplicate printpoint(s) on "
            f"layer {layer_idx}, path {path_idx}, indices: {dup_index}. They will be removed."
        )
        for ppt in duplicate_ppts:
            path.printpoints.remove(ppt)
get_printpoint_neighboring_items
get_printpoint_neighboring_items(layer_idx, path_idx, i)

Get neighboring printpoints.

Parameters:

Name Type Description Default
layer_idx int

The layer index.

required
path_idx int

The path index.

required
i int

Index of current printpoint.

required

Returns:

Type Description
list[PrintPoint | None]

Previous and next printpoints (None if at boundary).

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def get_printpoint_neighboring_items(
    self, layer_idx: int, path_idx: int, i: int
) -> list[PrintPoint | None]:
    """Get neighboring printpoints.

    Parameters
    ----------
    layer_idx : int
        The layer index.
    path_idx : int
        The path index.
    i : int
        Index of current printpoint.

    Returns
    -------
    list[PrintPoint | None]
        Previous and next printpoints (None if at boundary).

    """
    path = self.printpoints[layer_idx][path_idx]
    prev_pt = path[i - 1] if i > 0 else None
    next_pt = path[i + 1] if i < len(path) - 1 else None
    return [prev_pt, next_pt]
printout_info
printout_info()

Print information about the PrintOrganizer.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def printout_info(self) -> None:
    """Print information about the PrintOrganizer."""
    ppts_attributes = {
        key: str(type(val))
        for key, val in self.printpoints[0][0][0].attributes.items()
    }

    logger.info("---- PrintOrganizer Info ----")
    logger.info(f"Number of layers: {self.number_of_layers}")
    logger.info(f"Number of paths: {self.number_of_paths}")
    logger.info(f"Number of PrintPoints: {self.number_of_printpoints}")
    logger.info("PrintPoints attributes: ")
    for key, val in ppts_attributes.items():
        logger.info(f"     {key} : {val}")
    logger.info(f"Toolpath length: {self.total_length_of_paths:.0f} mm")

    print_time = self.total_print_time
    if print_time:
        minutes, sec = divmod(print_time, 60)
        hour, minutes = divmod(minutes, 60)
        logger.info(f"Total print time: {int(hour)} hours, {int(minutes)} minutes, {int(sec)} seconds")
    else:
        logger.info("Print Velocity has not been assigned, thus print time is not calculated.")
get_printpoint_up_vector
get_printpoint_up_vector(path, k, normal)

Get printpoint up-vector orthogonal to path direction and normal.

Parameters:

Name Type Description Default
path Path

The path containing the point.

required
k int

Index of the point in path.points.

required
normal Vector

The normal vector.

required

Returns:

Type Description
Vector

The up vector.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def get_printpoint_up_vector(self, path: Path, k: int, normal: Vector) -> Vector:
    """Get printpoint up-vector orthogonal to path direction and normal.

    Parameters
    ----------
    path : Path
        The path containing the point.
    k : int
        Index of the point in path.points.
    normal : Vector
        The normal vector.

    Returns
    -------
    Vector
        The up vector.

    """
    p = path.points[k]
    if k < len(path.points) - 1:
        negative = False
        other_pt = path.points[k + 1]
    else:
        negative = True
        other_pt = path.points[k - 1]

    diff = normalize_vector(subtract_vectors(p, other_pt))
    up_vec = normalize_vector(cross_vectors(normal, diff))

    if negative:
        up_vec = scale_vector(up_vec, -1.0)
    if norm_vector(up_vec) == 0:
        up_vec = Vector(0, 0, 1)

    return Vector(*up_vec)
output_printpoints_dict
output_printpoints_dict()

Create a flattened printpoints dictionary.

Returns:

Type Description
dict

Flattened printpoints data for JSON serialization.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def output_printpoints_dict(self) -> dict[int, dict[str, Any]]:
    """Create a flattened printpoints dictionary.

    Returns
    -------
    dict
        Flattened printpoints data for JSON serialization.

    """
    data = {}
    count = 0

    for i, layer in enumerate(self.printpoints):
        for j, path in enumerate(layer):
            self.remove_duplicate_points_in_path(i, j)
            for printpoint in path:
                data[count] = printpoint.to_data()
                count += 1

    logger.info(f"Generated {count} print points")
    return data
output_nested_printpoints_dict
output_nested_printpoints_dict()

Create a nested printpoints dictionary.

Returns:

Type Description
dict

Nested printpoints data for JSON serialization.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def output_nested_printpoints_dict(self) -> dict[str, dict[str, dict[int, dict[str, Any]]]]:
    """Create a nested printpoints dictionary.

    Returns
    -------
    dict
        Nested printpoints data for JSON serialization.

    """
    data: dict[str, dict[str, dict[int, dict[str, Any]]]] = {}
    count = 0

    for i, layer in enumerate(self.printpoints):
        layer_key = f"layer_{i}"
        data[layer_key] = {}
        for j, path in enumerate(layer):
            path_key = f"path_{j}"
            data[layer_key][path_key] = {}
            self.remove_duplicate_points_in_path(i, j)
            for k, printpoint in enumerate(path):
                data[layer_key][path_key][k] = printpoint.to_data()
                count += 1

    logger.info(f"Generated {count} print points")
    return data
output_gcode
output_gcode(config=None)

Generate G-code text.

Parameters:

Name Type Description Default
config GcodeConfig | None

G-code configuration. If None, uses defaults.

None

Returns:

Type Description
str

G-code text.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def output_gcode(self, config: GcodeConfig | None = None) -> str:
    """Generate G-code text.

    Parameters
    ----------
    config : GcodeConfig | None
        G-code configuration. If None, uses defaults.

    Returns
    -------
    str
        G-code text.

    """
    return create_gcode_text(self, config)
get_printpoints_attribute
get_printpoints_attribute(attr_name)

Get a list of attribute values from all printpoints.

Parameters:

Name Type Description Default
attr_name str

Name of the attribute.

required

Returns:

Type Description
list

Attribute values from all printpoints.

Source code in src/compas_slicer/print_organization/base_print_organizer.py
def get_printpoints_attribute(self, attr_name: str) -> list[Any]:
    """Get a list of attribute values from all printpoints.

    Parameters
    ----------
    attr_name : str
        Name of the attribute.

    Returns
    -------
    list
        Attribute values from all printpoints.

    """
    attr_values = []
    for pp in self.printpoints.iter_printpoints():
        if attr_name not in pp.attributes:
            raise KeyError(f"Attribute '{attr_name}' not in printpoint.attributes")
        attr_values.append(pp.attributes[attr_name])
    return attr_values

curved_print_organization

BaseBoundary

BaseBoundary(mesh, points, override_vector=None)

The BaseBoundary is like a fake initial layer that supports the first path of the segment. This is useful, because for our computations we need to have a support layer for evey path. The first path has as support the Base Boundary, and every other path has its previous path.

Attributes:

Name Type Description
mesh
points
override_vector
Source code in src/compas_slicer/print_organization/curved_print_organization/base_boundary.py
def __init__(
    self, mesh: Mesh, points: list[Point], override_vector: Vector | None = None
) -> None:
    self.mesh = mesh
    self.points = points
    self.override_vector = override_vector
    closest_fks, projected_pts = utils.pull_pts_to_mesh_faces(self.mesh, list(self.points))
    self.normals = [Vector(*self.mesh.face_normal(fkey)) for fkey in closest_fks]

    if self.override_vector:
        self.up_vectors = [self.override_vector for p in self.points]
    else:
        self.up_vectors = self.get_up_vectors()

    self.printpoints = [PrintPoint(pt=pt,  # Create fake print points
                                   layer_height=1.0,
                                   mesh_normal=self.normals[i]) for i, pt in enumerate(self.points)]

    for i, pp in enumerate(self.printpoints):
        pp.up_vector = self.up_vectors[i]
get_up_vectors
get_up_vectors()

Finds the up_vectors of each point of the boundary. A smoothing step is also included.

Source code in src/compas_slicer/print_organization/curved_print_organization/base_boundary.py
def get_up_vectors(self) -> list[Vector]:
    """ Finds the up_vectors of each point of the boundary. A smoothing step is also included. """
    up_vectors = []
    for i, p in enumerate(self.points):
        v1 = Vector.from_start_end(p, self.points[(i + 1) % len(self.points)])
        cross = v1.cross(self.normals[i])
        v = Vector(*normalize_vector(cross))
        if v[2] < 0:
            v.scale(-1)
        up_vectors.append(v)
    up_vectors = utils.smooth_vectors(up_vectors, strength=0.4, iterations=3)
    return up_vectors
to_data
to_data()

Returns a dictionary with the data of the class.

Source code in src/compas_slicer/print_organization/curved_print_organization/base_boundary.py
def to_data(self) -> dict[str, Any]:
    """ Returns a dictionary with the data of the class. """
    return {"points": utils.point_list_to_dict(self.points),
            "up_vectors": utils.point_list_to_dict(self.up_vectors)}

base_boundary

BaseBoundary
BaseBoundary(mesh, points, override_vector=None)

The BaseBoundary is like a fake initial layer that supports the first path of the segment. This is useful, because for our computations we need to have a support layer for evey path. The first path has as support the Base Boundary, and every other path has its previous path.

Attributes:

Name Type Description
mesh
points
override_vector
Source code in src/compas_slicer/print_organization/curved_print_organization/base_boundary.py
def __init__(
    self, mesh: Mesh, points: list[Point], override_vector: Vector | None = None
) -> None:
    self.mesh = mesh
    self.points = points
    self.override_vector = override_vector
    closest_fks, projected_pts = utils.pull_pts_to_mesh_faces(self.mesh, list(self.points))
    self.normals = [Vector(*self.mesh.face_normal(fkey)) for fkey in closest_fks]

    if self.override_vector:
        self.up_vectors = [self.override_vector for p in self.points]
    else:
        self.up_vectors = self.get_up_vectors()

    self.printpoints = [PrintPoint(pt=pt,  # Create fake print points
                                   layer_height=1.0,
                                   mesh_normal=self.normals[i]) for i, pt in enumerate(self.points)]

    for i, pp in enumerate(self.printpoints):
        pp.up_vector = self.up_vectors[i]
get_up_vectors
get_up_vectors()

Finds the up_vectors of each point of the boundary. A smoothing step is also included.

Source code in src/compas_slicer/print_organization/curved_print_organization/base_boundary.py
def get_up_vectors(self) -> list[Vector]:
    """ Finds the up_vectors of each point of the boundary. A smoothing step is also included. """
    up_vectors = []
    for i, p in enumerate(self.points):
        v1 = Vector.from_start_end(p, self.points[(i + 1) % len(self.points)])
        cross = v1.cross(self.normals[i])
        v = Vector(*normalize_vector(cross))
        if v[2] < 0:
            v.scale(-1)
        up_vectors.append(v)
    up_vectors = utils.smooth_vectors(up_vectors, strength=0.4, iterations=3)
    return up_vectors
to_data
to_data()

Returns a dictionary with the data of the class.

Source code in src/compas_slicer/print_organization/curved_print_organization/base_boundary.py
def to_data(self) -> dict[str, Any]:
    """ Returns a dictionary with the data of the class. """
    return {"points": utils.point_list_to_dict(self.points),
            "up_vectors": utils.point_list_to_dict(self.up_vectors)}

interpolation_print_organizer

InterpolationPrintOrganizer

InterpolationPrintOrganizer(slicer, config=None, DATA_PATH='.')

Bases: BasePrintOrganizer

Organize the printing process for non-planar contours.

Attributes:

Name Type Description
slicer InterpolationSlicer

An instance of InterpolationSlicer.

config InterpolationConfig

Interpolation configuration.

DATA_PATH str | Path

Data directory path.

vertical_layers list[VerticalLayer]

Vertical layers from slicer.

horizontal_layers list[Layer]

Horizontal layers from slicer.

base_boundaries list[BaseBoundary]

Base boundaries for each vertical layer.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def __init__(
    self,
    slicer: InterpolationSlicer,
    config: InterpolationConfig | None = None,
    DATA_PATH: str | FilePath = ".",
) -> None:
    from compas_slicer.slicers import InterpolationSlicer

    if not isinstance(slicer, InterpolationSlicer):
        raise TypeError('Please provide an InterpolationSlicer')
    BasePrintOrganizer.__init__(self, slicer)
    self.DATA_PATH = DATA_PATH
    self.OUTPUT_PATH = utils.get_output_directory(DATA_PATH)
    self.config = config if config else InterpolationConfig()

    self.vertical_layers = slicer.vertical_layers
    self.horizontal_layers = slicer.horizontal_layers
    if len(self.vertical_layers) + len(self.horizontal_layers) != len(slicer.layers):
        raise ValueError(
            f"Layer count mismatch: {len(self.vertical_layers)} vertical + "
            f"{len(self.horizontal_layers)} horizontal != {len(slicer.layers)} total"
        )

    if len(self.horizontal_layers) > 0:
        if len(self.horizontal_layers) != 1:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        if not self.horizontal_layers[0].is_brim:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        logger.info('Slicer has one horizontal brim layer.')

    # topological sorting of vertical layers depending on their connectivity
    self.topo_sort_graph: topo_sort.SegmentsDirectedGraph | None = None
    if len(self.vertical_layers) > 1:
        try:
            self.topological_sorting()
        except AssertionError:
            logger.exception("topology sorting failed\n")
            logger.critical("integrity of the output data ")
            # TODO: perhaps its better to be even more explicit and add a
            #  FAILED-timestamp.txt file?
    self.selected_order: list[int] | None = None

    # creation of one base boundary per vertical_layer
    self.base_boundaries: list[BaseBoundary] = self.create_base_boundaries()
topological_sorting
topological_sorting()

Create directed graph of parts with connectivity.

Creates a directed graph where each part's connectivity reflects which other parts it lies on and which other parts lie on it.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def topological_sorting(self) -> None:
    """Create directed graph of parts with connectivity.

    Creates a directed graph where each part's connectivity reflects which
    other parts it lies on and which other parts lie on it.

    """
    avg_layer_height = self.config.avg_layer_height
    self.topo_sort_graph = topo_sort.SegmentsDirectedGraph(self.slicer.mesh, self.vertical_layers,
                                                           4 * avg_layer_height, DATA_PATH=self.DATA_PATH)
create_base_boundaries
create_base_boundaries()

Create one BaseBoundary per vertical_layer.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def create_base_boundaries(self) -> list[BaseBoundary]:
    """Create one BaseBoundary per vertical_layer."""
    bs: list[BaseBoundary] = []
    root_vs = utils.get_mesh_vertex_coords_with_attribute(self.slicer.mesh, 'boundary', 1)
    root_boundary = BaseBoundary(self.slicer.mesh, [Point(*v) for v in root_vs])

    if len(self.vertical_layers) > 1 and self.topo_sort_graph is not None:
        for i, _vertical_layer in enumerate(self.vertical_layers):
            parents_of_current_node = self.topo_sort_graph.get_parents_of_node(i)
            if len(parents_of_current_node) == 0:
                boundary = root_boundary
            else:
                boundary_pts = []
                for parent_index in parents_of_current_node:
                    parent = self.vertical_layers[parent_index]
                    boundary_pts.extend(parent.paths[-1].points)
                boundary = BaseBoundary(self.slicer.mesh, boundary_pts)
            bs.append(boundary)
    else:
        bs.append(root_boundary)

    # save intermediary outputs
    b_data = {i: b.to_data() for i, b in enumerate(bs)}
    utils.save_to_json(b_data, self.OUTPUT_PATH, 'boundaries.json')

    return bs
create_printpoints
create_printpoints()

Create the print points of the fabrication process.

Based on the directed graph, select one topological order. From each path collection in that order, copy PrintPoints in the correct order.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def create_printpoints(self) -> None:
    """Create the print points of the fabrication process.

    Based on the directed graph, select one topological order.
    From each path collection in that order, copy PrintPoints in the correct order.

    """
    current_layer_index = 0

    # (1) --- First add the printpoints of the horizontal brim layer (first layer of print)
    if len(self.horizontal_layers) > 0:  # first add horizontal brim layers
        print_layer = PrintLayer()
        paths = self.horizontal_layers[0].paths
        for _j, path in enumerate(paths):
            print_path = PrintPath(printpoints=[
                PrintPoint(pt=point, layer_height=self.config.avg_layer_height,
                           mesh_normal=utils.get_normal_of_path_on_xy_plane(k, point, path, self.slicer.mesh))
                for k, point in enumerate(path.points)
            ])
            print_layer.paths.append(print_path)
        self.printpoints.layers.append(print_layer)
        current_layer_index += 1
    else:
        # Add empty first layer placeholder if no horizontal layers
        pass

    # (2) --- Select order of vertical layers
    if len(self.vertical_layers) > 1:  # then you need to select one topological order

        if not self.topo_sort_graph:
            logger.error("no topology graph found, cannnot set the order of vertical layers")
            self.selected_order = [0]
        else:
            all_orders = self.topo_sort_graph.get_all_topological_orders()
            self.selected_order = all_orders[0]  # TODO: add more elaborate selection strategy
    else:
        self.selected_order = [0]  # there is only one segment, only this option

    # (3) --- Then create the printpoints of all the vertical layers in the selected order
    if self.selected_order is None:
        raise RuntimeError("selected_order must be set before creating printpoints")
    for _index, i in enumerate(self.selected_order):
        layer = self.vertical_layers[i]
        print_layer = self.get_layer_ppts(layer, self.base_boundaries[i])
        self.printpoints.layers.append(print_layer)
        current_layer_index += 1
get_layer_ppts
get_layer_ppts(layer, base_boundary)

Create the PrintPoints of a single layer.

Source code in src/compas_slicer/print_organization/interpolation_print_organizer.py
def get_layer_ppts(self, layer: VerticalLayer, base_boundary: BaseBoundary) -> PrintLayer:
    """Create the PrintPoints of a single layer."""
    max_layer_height = self.config.max_layer_height
    min_layer_height = self.config.min_layer_height
    avg_layer_height = self.config.avg_layer_height

    all_pts = [pt for path in layer.paths for pt in path.points]
    closest_fks, projected_pts = utils.pull_pts_to_mesh_faces(self.slicer.mesh, all_pts)
    normals = [Vector(*self.slicer.mesh.face_normal(fkey)) for fkey in closest_fks]

    count = 0
    support_polyline_pts = base_boundary.points  # Start with base boundary

    print_layer = PrintLayer()
    for _i, path in enumerate(layer.paths):
        # Batch query: find closest points for all points in this path at once
        closest_pts, distances = _batch_closest_points_on_polyline(
            path.points, support_polyline_pts
        )

        print_path = PrintPath()
        for k, p in enumerate(path.points):
            cp = closest_pts[k]
            d = distances[k]

            normal = normals[count]
            ppt = PrintPoint(pt=p, layer_height=avg_layer_height, mesh_normal=normal)

            ppt.closest_support_pt = Point(cp[0], cp[1], cp[2])
            ppt.distance_to_support = d
            ppt.layer_height = max(min(d, max_layer_height), min_layer_height)
            ppt.up_vector = self.get_printpoint_up_vector(path, k, normal)
            if dot_vectors(subtract_vectors(p, ppt.closest_support_pt), ppt.up_vector) < 0:
                ppt.up_vector = Vector(*scale_vector(ppt.up_vector, -1))
            ppt.frame = ppt.get_frame()

            print_path.printpoints.append(ppt)
            count += 1

        print_layer.paths.append(print_path)
        support_polyline_pts = path.points  # Next path checks against this one

    return print_layer

planar_print_organizer

PlanarPrintOrganizer

PlanarPrintOrganizer(slicer)

Bases: BasePrintOrganizer

Organize the printing process for planar contours.

Attributes:

Name Type Description
slicer PlanarSlicer

An instance of PlanarSlicer.

Source code in src/compas_slicer/print_organization/planar_print_organizer.py
def __init__(self, slicer: PlanarSlicer) -> None:
    from compas_slicer.slicers import PlanarSlicer

    if not isinstance(slicer, PlanarSlicer):
        raise TypeError('Please provide a PlanarSlicer')
    BasePrintOrganizer.__init__(self, slicer)
create_printpoints
create_printpoints(generate_mesh_normals=True)

Create the print points of the fabrication process.

Parameters:

Name Type Description Default
generate_mesh_normals bool

If True, compute mesh normals. If False, use Vector(0, 1, 0).

True
Source code in src/compas_slicer/print_organization/planar_print_organizer.py
def create_printpoints(self, generate_mesh_normals: bool = True) -> None:
    """Create the print points of the fabrication process.

    Parameters
    ----------
    generate_mesh_normals : bool
        If True, compute mesh normals. If False, use Vector(0, 1, 0).

    """

    count = 0
    logger.info('Creating print points ...')
    with progressbar.ProgressBar(max_value=self.slicer.number_of_points) as bar:

        if generate_mesh_normals:
            logger.info('Generating mesh normals ...')
            # fast method for getting the closest mesh normals to all the printpoints
            all_pts = [pt for layer in self.slicer.layers for path in layer.paths for pt in path.points]
            closest_fks, projected_pts = utils.pull_pts_to_mesh_faces(self.slicer.mesh, all_pts)
            normals = [Vector(*self.slicer.mesh.face_normal(fkey)) for fkey in closest_fks]

        for _i, layer in enumerate(self.slicer.layers):
            print_layer = PrintLayer()

            for _j, path in enumerate(layer.paths):
                print_path = PrintPath()

                for k, point in enumerate(path.points):

                    n = normals[count] if generate_mesh_normals else Vector(0, 1, 0)
                    layer_h = self.slicer.layer_height if self.slicer.layer_height else 2.0
                    printpoint = PrintPoint(pt=point, layer_height=layer_h, mesh_normal=n)

                    if layer.is_brim or layer.is_raft:
                        printpoint.up_vector = Vector(0, 0, 1)
                    else:
                        printpoint.up_vector = self.get_printpoint_up_vector(path, k, n)

                    print_path.printpoints.append(printpoint)
                    bar.update(count)
                    count += 1

                print_layer.paths.append(print_path)

            self.printpoints.layers.append(print_layer)

print_organization_utilities

GcodeBuilder

GcodeBuilder()

Builder for constructing G-code output efficiently.

Uses a list internally and joins at the end for better performance than repeated string concatenation.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def __init__(self) -> None:
    self._lines: list[str] = []
comment
comment(text)

Add a comment line.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def comment(self, text: str) -> None:
    """Add a comment line."""
    self._lines.append(f";{text}")
cmd
cmd(gcode, comment='')

Add a G-code command with optional inline comment.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def cmd(self, gcode: str, comment: str = "") -> None:
    """Add a G-code command with optional inline comment."""
    if comment:
        self._lines.append(f"{gcode:<30} ;{comment}")
    else:
        self._lines.append(gcode)
blank
blank()

Add a blank line.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def blank(self) -> None:
    """Add a blank line."""
    self._lines.append("")
build
build()

Return the complete G-code as a string.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def build(self) -> str:
    """Return the complete G-code as a string."""
    return "\n".join(self._lines)

set_blend_radius

set_blend_radius(print_organizer, d_fillet=10.0, buffer=0.3)

Sets the blend radius (filleting) for the robotic motion.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
d_fillet float

Value to attempt to fillet with. Defaults to 10 mm.

10.0
buffer float

Buffer to make sure that the blend radius is never too big. Defaults to 0.3.

0.3
Source code in src/compas_slicer/print_organization/print_organization_utilities/blend_radius.py
def set_blend_radius(
    print_organizer: BasePrintOrganizer, d_fillet: float = 10.0, buffer: float = 0.3
) -> None:
    """Sets the blend radius (filleting) for the robotic motion.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.slicers.BasePrintOrganizer`
    d_fillet: float
        Value to attempt to fillet with. Defaults to 10 mm.
    buffer: float
        Buffer to make sure that the blend radius is never too big.
        Defaults to 0.3.
    """

    logger.info("Setting blend radius")

    extruder_state: bool | None = None

    for printpoint, i, j, k in print_organizer.printpoints_indices_iterator():
        neighboring_items = print_organizer.get_printpoint_neighboring_items(i, j, k)

        if not printpoint.wait_time:

            # if the extruder_toggle changes, it must be a new path and therefore the blend radius should be 0
            if extruder_state != printpoint.extruder_toggle:
                extruder_state = printpoint.extruder_toggle
                radius = 0.0

            else:

                radius = d_fillet
                if neighboring_items[0]:
                    radius = min(radius, norm_vector(Vector.from_start_end(neighboring_items[0].pt, printpoint.pt)) * buffer)

                if neighboring_items[1]:
                    radius = min(radius, norm_vector(Vector.from_start_end(neighboring_items[1].pt, printpoint.pt)) * buffer)

                radius = round(radius, 5)

        else:
            radius = 0.0  # 0.0 blend radius for points where the robot will pause and wait

        printpoint.blend_radius = radius

smooth_printpoint_attribute

smooth_printpoint_attribute(print_organizer, iterations, strength, get_attr_value, set_attr_value)

Iterative smoothing of the printpoints attribute. The attribute is accessed using the function 'get_attr_value(ppt)', and is set using the function 'set_attr_value(ppt, v)'. All attributes are smoothened continuously (i.e. as if their printpoints belong into one long uninterrupted path) For examples of how to use this function look at 'smooth_printpoints_layer_heights' and 'smooth_printpoints_up_vectors' below. The smoothing is happening by taking an average of the previous and next point attributes, and combining them with the current value of the print point; On every iteration: new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
iterations int
required
strength float

the current value with the average of the two neighbors on every interation stop. On each iteration: new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)

required
get_attr_value Callable[[PrintPoint], Any]
required
set_attr_value Callable[[PrintPoint, Any], None]
required
Source code in src/compas_slicer/print_organization/print_organization_utilities/data_smoothing.py
def smooth_printpoint_attribute(
    print_organizer: BasePrintOrganizer,
    iterations: int,
    strength: float,
    get_attr_value: Callable[[PrintPoint], Any],
    set_attr_value: Callable[[PrintPoint, Any], None],
) -> None:
    """
    Iterative smoothing of the printpoints attribute.
    The attribute is accessed using the function 'get_attr_value(ppt)', and is set using the function
    'set_attr_value(ppt, v)'.
    All attributes are smoothened continuously (i.e. as if their printpoints belong into one long uninterrupted path)
    For examples of how to use this function look at 'smooth_printpoints_layer_heights' and
    'smooth_printpoints_up_vectors' below.
    The smoothing is happening by taking an average of the previous and next point attributes, and combining them with
    the current value of the print point; On every iteration:
    new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)

    Parameters
    ----------
    print_organizer: :class: 'compas_slicer.print_organization.BasePrintOrganizer', or other class inheriting from it.
    iterations: int, smoothing iterations
    strength: float. in the range [0.0 - 1.0]. 0.0 corresponds to no smoothing at all, 1.0 corresponds to overwriting
        the current value with the average of the two neighbors on every interation stop. On each iteration:
        new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)
    get_attr_value: function that returns an attribute of a printpoint, get_attr_value(ppt)
    set_attr_value: function that sets an attribute of a printpoint, set_attr_value(ppt, new_value)
    """

    # first smoothen the values
    for ppt in print_organizer.printpoints_iterator():
        if get_attr_value(ppt) is None:
            raise ValueError('The attribute you are trying to smooth has not been assigned a value')

    attrs = np.array([get_attr_value(ppt) for ppt in print_organizer.printpoints_iterator()])

    # Vectorized smoothing: use numpy slicing instead of per-element loop
    for _ in range(iterations):
        # mid = 0.5 * (attrs[i-1] + attrs[i+1]) for interior points
        mid = 0.5 * (attrs[:-2] + attrs[2:])  # shape: (n-2,)
        # new_val = mid * strength + attrs[1:-1] * (1 - strength)
        attrs[1:-1] = mid * strength + attrs[1:-1] * (1 - strength)

    # Assign the smoothened values back to the printpoints
    for i, ppt in enumerate(print_organizer.printpoints_iterator()):
        val = attrs[i]
        # Convert back from numpy type if needed
        set_attr_value(ppt, val.tolist() if hasattr(val, 'tolist') else float(val))

smooth_printpoints_layer_heights

smooth_printpoints_layer_heights(print_organizer, iterations, strength)

This function is an example for how the 'smooth_printpoint_attribute' function can be used.

Source code in src/compas_slicer/print_organization/print_organization_utilities/data_smoothing.py
def smooth_printpoints_layer_heights(
    print_organizer: BasePrintOrganizer, iterations: int, strength: float
) -> None:
    """ This function is an example for how the 'smooth_printpoint_attribute' function can be used. """

    def get_ppt_layer_height(printpoint):
        return printpoint.layer_height  # get value

    def set_ppt_layer_height(printpoint, v):
        printpoint.layer_height = v  # set value

    smooth_printpoint_attribute(print_organizer, iterations, strength, get_ppt_layer_height, set_ppt_layer_height)

smooth_printpoints_up_vectors

smooth_printpoints_up_vectors(print_organizer, iterations, strength)

This function is an example for how the 'smooth_printpoint_attribute' function can be used.

Source code in src/compas_slicer/print_organization/print_organization_utilities/data_smoothing.py
def smooth_printpoints_up_vectors(
    print_organizer: BasePrintOrganizer, iterations: int, strength: float
) -> None:
    """ This function is an example for how the 'smooth_printpoint_attribute' function can be used. """

    def get_ppt_up_vec(printpoint):
        return printpoint.up_vector  # get value

    def set_ppt_up_vec(printpoint, v):
        # Convert list back to Vector for proper serialization
        printpoint.up_vector = Vector(*v) if isinstance(v, list) else v

    smooth_printpoint_attribute(print_organizer, iterations, strength, get_ppt_up_vec, set_ppt_up_vec)
    # finally update any values in the printpoints that are affected by the changed attribute
    for ppt in print_organizer.printpoints_iterator():
        ppt.frame = ppt.get_frame()

set_extruder_toggle

set_extruder_toggle(print_organizer, slicer)

Sets the extruder_toggle value for the printpoints.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
slicer BaseSlicer
required
Source code in src/compas_slicer/print_organization/print_organization_utilities/extruder_toggle.py
def set_extruder_toggle(print_organizer: BasePrintOrganizer, slicer: BaseSlicer) -> None:
    """Sets the extruder_toggle value for the printpoints.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    slicer: :class:`compas.slicers.BaseSlicer`
    """

    logger.info("Setting extruder toggle")

    for i, layer in enumerate(slicer.layers):
        is_vertical_layer = isinstance(layer, compas_slicer.geometry.VerticalLayer)
        is_brim_layer = layer.is_brim

        for j, path in enumerate(layer.paths):
            is_closed_path = path.is_closed

            # --- decide if the path should be interrupted at the end
            interrupt_path = False

            if not is_closed_path:
                interrupt_path = True
                # open paths should always be interrupted

            if not is_vertical_layer and len(layer.paths) > 1:
                interrupt_path = True
                # horizontal layers with multiple paths should be interrupted so that the extruder
                # can travel from one path to the other, exception is added for the brim layers
                if is_brim_layer and (j + 1) % layer.number_of_brim_offsets != 0:
                    interrupt_path = False

            if is_vertical_layer and j == len(layer.paths) - 1:
                interrupt_path = True
                # the last path of a vertical layer should be interrupted

            if i < len(slicer.layers)-1 and not slicer.layers[i+1].paths[0].is_closed:
                interrupt_path = True

            # --- create extruder toggles
            try:
                path_printpoints = print_organizer.printpoints[i][j]
            except (KeyError, IndexError):
                logger.exception(f"no path found for layer {i}")
            else:
                for k, printpoint in enumerate(path_printpoints):

                    if interrupt_path:
                        if k == len(path_printpoints) - 1:
                            printpoint.extruder_toggle = False
                        else:
                            printpoint.extruder_toggle = True
                    else:
                        printpoint.extruder_toggle = True

    # set extruder toggle of last print point to false
    try:
        print_organizer.printpoints[-1][-1][-1].extruder_toggle = False
    except (KeyError, IndexError) as e:
        logger.exception(e)

override_extruder_toggle

override_extruder_toggle(print_organizer, override_value)

Overrides the extruder_toggle value for the printpoints with a user-defined value.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
override_value bool

Value to override the extruder_toggle values with.

required
Source code in src/compas_slicer/print_organization/print_organization_utilities/extruder_toggle.py
def override_extruder_toggle(print_organizer: BasePrintOrganizer, override_value: bool) -> None:
    """Overrides the extruder_toggle value for the printpoints with a user-defined value.

    Parameters
    ----------
    print_organizer: :class:`compas.print_organization.BasePrintOrganizer`
    override_value: bool
        Value to override the extruder_toggle values with.

    """
    if not isinstance(override_value, bool):
        raise TypeError("Override value must be of type bool")
    for printpoint in print_organizer.printpoints_iterator():
        printpoint.extruder_toggle = override_value

check_assigned_extruder_toggle

check_assigned_extruder_toggle(print_organizer)

Checks that all the printpoints have an assigned extruder toggle.

Source code in src/compas_slicer/print_organization/print_organization_utilities/extruder_toggle.py
def check_assigned_extruder_toggle(print_organizer: BasePrintOrganizer) -> bool:
    """ Checks that all the printpoints have an assigned extruder toggle. """
    all_toggles_assigned = True
    for printpoint in print_organizer.printpoints_iterator():
        if printpoint.extruder_toggle is None:
            all_toggles_assigned = False
    return all_toggles_assigned

create_gcode_text

create_gcode_text(print_organizer, config=None)

Create G-code text from organized print points.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer

The print organizer containing printpoints.

required
config GcodeConfig | None

G-code configuration. If None, uses defaults.

None

Returns:

Type Description
str

Complete G-code file content.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def create_gcode_text(
    print_organizer: BasePrintOrganizer, config: GcodeConfig | None = None
) -> str:
    """Create G-code text from organized print points.

    Parameters
    ----------
    print_organizer : BasePrintOrganizer
        The print organizer containing printpoints.
    config : GcodeConfig | None
        G-code configuration. If None, uses defaults.

    Returns
    -------
    str
        Complete G-code file content.

    """
    config = config or GcodeConfig()
    logger.info("Generating G-code")

    gb = GcodeBuilder()
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    _write_header(gb, config, timestamp)
    _write_purge_line(gb, config)
    final_z = _write_toolpath(gb, print_organizer, config)
    _write_footer(gb, config, final_z)

    return gb.build()

set_linear_velocity_constant

set_linear_velocity_constant(print_organizer, v=25.0)

Sets the linear velocity parameter of the printpoints depending on the selected type.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
v float
25.0
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_constant(print_organizer: BasePrintOrganizer, v: float = 25.0) -> None:
    """Sets the linear velocity parameter of the printpoints depending on the selected type.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    v:  float. Velocity value (in mm/s) to set for printpoints. Defaults to 25 mm/s.
    """

    logger.info("Setting constant linear velocity")
    for printpoint in print_organizer.printpoints_iterator():
        printpoint.velocity = v

set_linear_velocity_per_layer

set_linear_velocity_per_layer(print_organizer, per_layer_velocities)

Sets the linear velocity parameter of the printpoints depending on the selected type.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
per_layer_velocities list[float]

A list of velocities (floats) with equal length to the number of layers.

required
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_per_layer(
    print_organizer: BasePrintOrganizer, per_layer_velocities: list[float]
) -> None:
    """Sets the linear velocity parameter of the printpoints depending on the selected type.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    per_layer_velocities: list
        A list of velocities (floats) with equal length to the number of layers.
    """

    logger.info("Setting per-layer linear velocity")
    if len(per_layer_velocities) != print_organizer.number_of_layers:
        raise ValueError(
            f'Wrong number of velocity values: got {len(per_layer_velocities)}, '
            f'need {print_organizer.number_of_layers} (one per layer)'
        )
    for printpoint, i, _j, _k in print_organizer.printpoints_indices_iterator():
        printpoint.velocity = per_layer_velocities[i]

set_linear_velocity_by_range

set_linear_velocity_by_range(print_organizer, param_func, parameter_range, velocity_range, bound_remapping=True)

Sets the linear velocity parameter of the printpoints depending on the selected type.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
param_func Callable[[PrintPoint], float]

and returns the parameter value that will be used for the remapping

required
parameter_range tuple[float, float]

An example of a parameter that can be used is the overhang angle, or the layer height.

required
velocity_range tuple[float, float]

The range of velocities where the parameter will be remapped

required
bound_remapping bool

If True, the remapping is bound in the domain velocity_range, else it is unbound.

True
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_by_range(
    print_organizer: BasePrintOrganizer,
    param_func: Callable[[PrintPoint], float],
    parameter_range: tuple[float, float],
    velocity_range: tuple[float, float],
    bound_remapping: bool = True,
) -> None:
    """Sets the linear velocity parameter of the printpoints depending on the selected type.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    param_func: function that takes as argument a :class: 'compas_slicer.geometry.Printpoint': get_param_func(pp)
        and returns the parameter value that will be used for the remapping
    parameter_range: tuple
        An example of a parameter that can be used is the overhang angle, or the layer height.
    velocity_range: tuple
        The range of velocities where the parameter will be remapped
    bound_remapping: bool
        If True, the remapping is bound in the domain velocity_range, else it is unbound.
    """

    logger.info("Setting linear velocity based on parameter range")
    for printpoint in print_organizer.printpoints_iterator():
        param = param_func(printpoint)
        if param is None:
            raise ValueError('The param_func does not return any value for calculating the velocity range.')
        if bound_remapping:
            v = remap(param, parameter_range[0], parameter_range[1], velocity_range[0], velocity_range[1])
        else:
            v = remap_unbound(param, parameter_range[0], parameter_range[1], velocity_range[0], velocity_range[1])
        printpoint.velocity = v

set_linear_velocity_by_overhang

set_linear_velocity_by_overhang(print_organizer, overhang_range, velocity_range, bound_remapping=True)

Set velocity by overhang by using set_linear_velocity_by_range.

An example function for how to use the 'set_linear_velocity_by_range'. In this case the parameter that controls the velocity is the overhang, measured as a dot product with the horizontal direction.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
overhang_range tuple[float, float]

should be within [0.0, 1.0]. For example a reasonable value would be [0.0, 0.5], that would be remapping overhangs up to 45 degrees

required
velocity_range tuple[float, float]
required
bound_remapping bool
True
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_by_overhang(
    print_organizer: BasePrintOrganizer,
    overhang_range: tuple[float, float],
    velocity_range: tuple[float, float],
    bound_remapping: bool = True,
) -> None:
    """Set velocity by overhang by using set_linear_velocity_by_range.

    An example function for how to use the 'set_linear_velocity_by_range'. In this case the parameter that controls the
    velocity is the overhang, measured as a dot product with the horizontal direction.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    overhang_range: tuple:
        should be within [0.0, 1.0]. For example a reasonable value would be [0.0, 0.5], that would
        be remapping overhangs up to 45 degrees
    velocity_range: tuple
    bound_remapping: bool
    """

    def param_func(ppt): return dot_vectors(ppt.mesh_normal, Vector(0.0, 0.0, 1.0))
    # returns values from 0.0 (no overhang) to 1.0 (horizontal overhang)
    set_linear_velocity_by_range(print_organizer, param_func, overhang_range, velocity_range, bound_remapping)

add_safety_printpoints

add_safety_printpoints(print_organizer, z_hop=10.0)

Generates a safety print point at the interruptions of the print paths.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer

An instance of the BasePrintOrganizer class.

required
z_hop float

Vertical distance (in millimeters) of the safety point above the PrintPoint.

10.0
Source code in src/compas_slicer/print_organization/print_organization_utilities/safety_printpoints.py
def add_safety_printpoints(print_organizer: BasePrintOrganizer, z_hop: float = 10.0) -> None:
    """Generates a safety print point at the interruptions of the print paths.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
        An instance of the BasePrintOrganizer class.
    z_hop: float
        Vertical distance (in millimeters) of the safety point above the PrintPoint.
    """
    if not check_assigned_extruder_toggle(print_organizer):
        raise ValueError('You need to set the extruder toggles first, before you can create safety points')
    logger.info(f"Generating safety print points with height {z_hop} mm")

    from compas_slicer.geometry import PrintPointsCollection

    new_collection = PrintPointsCollection()

    for i, layer in enumerate(print_organizer.printpoints):
        new_layer = PrintLayer()

        for j, path in enumerate(layer):
            new_path = PrintPath()

            for k, printpoint in enumerate(path):
                #  add regular printing points
                new_path.printpoints.append(printpoint)

                # add safety printpoints if there is an interruption
                if printpoint.extruder_toggle is False:

                    # safety ppt after current printpoint
                    new_path.printpoints.append(create_safety_printpoint(printpoint, z_hop, False))

                    #  safety ppt before next printpoint (if there exists one)
                    next_ppt = find_next_printpoint(print_organizer.printpoints, i, j, k)
                    if next_ppt and next_ppt.extruder_toggle is True:  # if it is a printing ppt
                        new_path.printpoints.append(create_safety_printpoint(next_ppt, z_hop, False))

            new_layer.paths.append(new_path)

        new_collection.layers.append(new_layer)

    #  finally, insert a safety print point at the beginning of the entire print
    try:
        safety_printpoint = create_safety_printpoint(new_collection[0][0][0], z_hop, False)
        new_collection[0][0].printpoints.insert(0, safety_printpoint)
    except (KeyError, IndexError) as e:
        logger.exception(e)

    #  the safety printpoint has already been added at the end since the last printpoint extruder_toggle_type is False
    print_organizer.printpoints = new_collection

set_wait_time_on_sharp_corners

set_wait_time_on_sharp_corners(print_organizer, threshold=0.5 * pi, wait_time=0.3)

Sets a wait time at the sharp corners of the path, based on the angle threshold.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
threshold float

angle_threshold

0.5 * pi
wait_time float

Time in seconds to introduce to add as a wait time

0.3
Source code in src/compas_slicer/print_organization/print_organization_utilities/wait_time.py
def set_wait_time_on_sharp_corners(
    print_organizer: BasePrintOrganizer, threshold: float = 0.5 * math.pi, wait_time: float = 0.3
) -> None:
    """
    Sets a wait time at the sharp corners of the path, based on the angle threshold.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    threshold: float
        angle_threshold
    wait_time: float
        Time in seconds to introduce to add as a wait time
    """
    number_of_wait_points = 0
    for printpoint, i, j, k in print_organizer.printpoints_indices_iterator():
        neighbors = print_organizer.get_printpoint_neighboring_items(i, j, k)
        prev_ppt = neighbors[0]
        next_ppt = neighbors[1]

        if prev_ppt and next_ppt:
            v_to_prev = normalize_vector(Vector.from_start_end(printpoint.pt, prev_ppt.pt))
            v_to_next = normalize_vector(Vector.from_start_end(printpoint.pt, next_ppt.pt))
            a = abs(Vector(*v_to_prev).angle(v_to_next))

            if a < threshold:
                printpoint.wait_time = wait_time
                printpoint.blend_radius = 0.0  # 0.0 blend radius for points where the robot will wait
                number_of_wait_points += 1
    logger.info(f'Added wait times for {number_of_wait_points} points')

set_wait_time_based_on_extruder_toggle

set_wait_time_based_on_extruder_toggle(print_organizer, wait_type, wait_time=0.3)

Sets a wait time for the printpoints, either before extrusion starts, after extrusion finishes, or in both cases.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
wait_type WaitType

wait_before_extrusion: sets a wait time before extrusion (extruder_toggle False to True) wait_after_extrusion: sets a wait time after extrusion (extruder_toggle True to False) wait_before_and_after_extrusion: sets a wait time before, and after extrusion wait_at_sharp_corners: sets a wait time at the sharp corners of the path

required
wait_time float

Time in seconds to introduce to add as a wait time

0.3
Source code in src/compas_slicer/print_organization/print_organization_utilities/wait_time.py
def set_wait_time_based_on_extruder_toggle(
    print_organizer: BasePrintOrganizer, wait_type: WaitType, wait_time: float = 0.3
) -> None:
    """
    Sets a wait time for the printpoints, either before extrusion starts,
    after extrusion finishes, or in both cases.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    wait_type: str
        wait_before_extrusion:  sets a wait time before extrusion (extruder_toggle False to True)
        wait_after_extrusion: sets a wait time after extrusion (extruder_toggle True to False)
        wait_before_and_after_extrusion: sets a wait time before, and after extrusion
        wait_at_sharp_corners: sets a wait time at the sharp corners of the path
    wait_time: float
        Time in seconds to introduce to add as a wait time
    """

    for printpoint in print_organizer.printpoints_iterator():
        if printpoint.extruder_toggle is None:
            raise ValueError('You need to set the extruder toggles first, before you can automatically set the wait time')

    logger.info("Setting wait time")

    for printpoint, i, j, k in print_organizer.printpoints_indices_iterator():
        number_of_wait_points = 0
        next_ppt = find_next_printpoint(print_organizer.printpoints, i, j, k)

        # for the brim layer don't add any wait times
        if not print_organizer.slicer.layers[i].is_brim and next_ppt:
            if wait_type == "wait_before_extrusion":
                if printpoint.extruder_toggle is False and next_ppt.extruder_toggle is True:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
            elif wait_type == "wait_after_extrusion":
                if printpoint.extruder_toggle is True and next_ppt.extruder_toggle is False:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
            elif wait_type == "wait_before_and_after_extrusion":
                if printpoint.extruder_toggle is False and next_ppt.extruder_toggle is True:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
                if printpoint.extruder_toggle is True and next_ppt.extruder_toggle is False:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
            else:
                logger.error(f'Unknown wait type: {wait_type}')

        logger.info(f'Added wait times for {number_of_wait_points} points')

override_wait_time

override_wait_time(print_organizer, override_value)

Overrides the wait_time value for the printpoints with a user-defined value.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
override_value float

Value to override the wait_time values with.

required
Source code in src/compas_slicer/print_organization/print_organization_utilities/wait_time.py
def override_wait_time(print_organizer: BasePrintOrganizer, override_value: float) -> None:
    """
    Overrides the wait_time value for the printpoints with a user-defined value.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    override_value: float
        Value to override the wait_time values with.
    """
    for printpoint in print_organizer.printpoints_iterator():
        printpoint.wait_time = override_value

blend_radius

set_blend_radius
set_blend_radius(print_organizer, d_fillet=10.0, buffer=0.3)

Sets the blend radius (filleting) for the robotic motion.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
d_fillet float

Value to attempt to fillet with. Defaults to 10 mm.

10.0
buffer float

Buffer to make sure that the blend radius is never too big. Defaults to 0.3.

0.3
Source code in src/compas_slicer/print_organization/print_organization_utilities/blend_radius.py
def set_blend_radius(
    print_organizer: BasePrintOrganizer, d_fillet: float = 10.0, buffer: float = 0.3
) -> None:
    """Sets the blend radius (filleting) for the robotic motion.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.slicers.BasePrintOrganizer`
    d_fillet: float
        Value to attempt to fillet with. Defaults to 10 mm.
    buffer: float
        Buffer to make sure that the blend radius is never too big.
        Defaults to 0.3.
    """

    logger.info("Setting blend radius")

    extruder_state: bool | None = None

    for printpoint, i, j, k in print_organizer.printpoints_indices_iterator():
        neighboring_items = print_organizer.get_printpoint_neighboring_items(i, j, k)

        if not printpoint.wait_time:

            # if the extruder_toggle changes, it must be a new path and therefore the blend radius should be 0
            if extruder_state != printpoint.extruder_toggle:
                extruder_state = printpoint.extruder_toggle
                radius = 0.0

            else:

                radius = d_fillet
                if neighboring_items[0]:
                    radius = min(radius, norm_vector(Vector.from_start_end(neighboring_items[0].pt, printpoint.pt)) * buffer)

                if neighboring_items[1]:
                    radius = min(radius, norm_vector(Vector.from_start_end(neighboring_items[1].pt, printpoint.pt)) * buffer)

                radius = round(radius, 5)

        else:
            radius = 0.0  # 0.0 blend radius for points where the robot will pause and wait

        printpoint.blend_radius = radius

data_smoothing

smooth_printpoint_attribute
smooth_printpoint_attribute(print_organizer, iterations, strength, get_attr_value, set_attr_value)

Iterative smoothing of the printpoints attribute. The attribute is accessed using the function 'get_attr_value(ppt)', and is set using the function 'set_attr_value(ppt, v)'. All attributes are smoothened continuously (i.e. as if their printpoints belong into one long uninterrupted path) For examples of how to use this function look at 'smooth_printpoints_layer_heights' and 'smooth_printpoints_up_vectors' below. The smoothing is happening by taking an average of the previous and next point attributes, and combining them with the current value of the print point; On every iteration: new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
iterations int
required
strength float

the current value with the average of the two neighbors on every interation stop. On each iteration: new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)

required
get_attr_value Callable[[PrintPoint], Any]
required
set_attr_value Callable[[PrintPoint, Any], None]
required
Source code in src/compas_slicer/print_organization/print_organization_utilities/data_smoothing.py
def smooth_printpoint_attribute(
    print_organizer: BasePrintOrganizer,
    iterations: int,
    strength: float,
    get_attr_value: Callable[[PrintPoint], Any],
    set_attr_value: Callable[[PrintPoint, Any], None],
) -> None:
    """
    Iterative smoothing of the printpoints attribute.
    The attribute is accessed using the function 'get_attr_value(ppt)', and is set using the function
    'set_attr_value(ppt, v)'.
    All attributes are smoothened continuously (i.e. as if their printpoints belong into one long uninterrupted path)
    For examples of how to use this function look at 'smooth_printpoints_layer_heights' and
    'smooth_printpoints_up_vectors' below.
    The smoothing is happening by taking an average of the previous and next point attributes, and combining them with
    the current value of the print point; On every iteration:
    new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)

    Parameters
    ----------
    print_organizer: :class: 'compas_slicer.print_organization.BasePrintOrganizer', or other class inheriting from it.
    iterations: int, smoothing iterations
    strength: float. in the range [0.0 - 1.0]. 0.0 corresponds to no smoothing at all, 1.0 corresponds to overwriting
        the current value with the average of the two neighbors on every interation stop. On each iteration:
        new_val = (0.5*(neighbor_left_val + neighbor_right_attr)) * strength - current_val * (1-strength)
    get_attr_value: function that returns an attribute of a printpoint, get_attr_value(ppt)
    set_attr_value: function that sets an attribute of a printpoint, set_attr_value(ppt, new_value)
    """

    # first smoothen the values
    for ppt in print_organizer.printpoints_iterator():
        if get_attr_value(ppt) is None:
            raise ValueError('The attribute you are trying to smooth has not been assigned a value')

    attrs = np.array([get_attr_value(ppt) for ppt in print_organizer.printpoints_iterator()])

    # Vectorized smoothing: use numpy slicing instead of per-element loop
    for _ in range(iterations):
        # mid = 0.5 * (attrs[i-1] + attrs[i+1]) for interior points
        mid = 0.5 * (attrs[:-2] + attrs[2:])  # shape: (n-2,)
        # new_val = mid * strength + attrs[1:-1] * (1 - strength)
        attrs[1:-1] = mid * strength + attrs[1:-1] * (1 - strength)

    # Assign the smoothened values back to the printpoints
    for i, ppt in enumerate(print_organizer.printpoints_iterator()):
        val = attrs[i]
        # Convert back from numpy type if needed
        set_attr_value(ppt, val.tolist() if hasattr(val, 'tolist') else float(val))
smooth_printpoints_layer_heights
smooth_printpoints_layer_heights(print_organizer, iterations, strength)

This function is an example for how the 'smooth_printpoint_attribute' function can be used.

Source code in src/compas_slicer/print_organization/print_organization_utilities/data_smoothing.py
def smooth_printpoints_layer_heights(
    print_organizer: BasePrintOrganizer, iterations: int, strength: float
) -> None:
    """ This function is an example for how the 'smooth_printpoint_attribute' function can be used. """

    def get_ppt_layer_height(printpoint):
        return printpoint.layer_height  # get value

    def set_ppt_layer_height(printpoint, v):
        printpoint.layer_height = v  # set value

    smooth_printpoint_attribute(print_organizer, iterations, strength, get_ppt_layer_height, set_ppt_layer_height)
smooth_printpoints_up_vectors
smooth_printpoints_up_vectors(print_organizer, iterations, strength)

This function is an example for how the 'smooth_printpoint_attribute' function can be used.

Source code in src/compas_slicer/print_organization/print_organization_utilities/data_smoothing.py
def smooth_printpoints_up_vectors(
    print_organizer: BasePrintOrganizer, iterations: int, strength: float
) -> None:
    """ This function is an example for how the 'smooth_printpoint_attribute' function can be used. """

    def get_ppt_up_vec(printpoint):
        return printpoint.up_vector  # get value

    def set_ppt_up_vec(printpoint, v):
        # Convert list back to Vector for proper serialization
        printpoint.up_vector = Vector(*v) if isinstance(v, list) else v

    smooth_printpoint_attribute(print_organizer, iterations, strength, get_ppt_up_vec, set_ppt_up_vec)
    # finally update any values in the printpoints that are affected by the changed attribute
    for ppt in print_organizer.printpoints_iterator():
        ppt.frame = ppt.get_frame()

extruder_toggle

set_extruder_toggle
set_extruder_toggle(print_organizer, slicer)

Sets the extruder_toggle value for the printpoints.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
slicer BaseSlicer
required
Source code in src/compas_slicer/print_organization/print_organization_utilities/extruder_toggle.py
def set_extruder_toggle(print_organizer: BasePrintOrganizer, slicer: BaseSlicer) -> None:
    """Sets the extruder_toggle value for the printpoints.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    slicer: :class:`compas.slicers.BaseSlicer`
    """

    logger.info("Setting extruder toggle")

    for i, layer in enumerate(slicer.layers):
        is_vertical_layer = isinstance(layer, compas_slicer.geometry.VerticalLayer)
        is_brim_layer = layer.is_brim

        for j, path in enumerate(layer.paths):
            is_closed_path = path.is_closed

            # --- decide if the path should be interrupted at the end
            interrupt_path = False

            if not is_closed_path:
                interrupt_path = True
                # open paths should always be interrupted

            if not is_vertical_layer and len(layer.paths) > 1:
                interrupt_path = True
                # horizontal layers with multiple paths should be interrupted so that the extruder
                # can travel from one path to the other, exception is added for the brim layers
                if is_brim_layer and (j + 1) % layer.number_of_brim_offsets != 0:
                    interrupt_path = False

            if is_vertical_layer and j == len(layer.paths) - 1:
                interrupt_path = True
                # the last path of a vertical layer should be interrupted

            if i < len(slicer.layers)-1 and not slicer.layers[i+1].paths[0].is_closed:
                interrupt_path = True

            # --- create extruder toggles
            try:
                path_printpoints = print_organizer.printpoints[i][j]
            except (KeyError, IndexError):
                logger.exception(f"no path found for layer {i}")
            else:
                for k, printpoint in enumerate(path_printpoints):

                    if interrupt_path:
                        if k == len(path_printpoints) - 1:
                            printpoint.extruder_toggle = False
                        else:
                            printpoint.extruder_toggle = True
                    else:
                        printpoint.extruder_toggle = True

    # set extruder toggle of last print point to false
    try:
        print_organizer.printpoints[-1][-1][-1].extruder_toggle = False
    except (KeyError, IndexError) as e:
        logger.exception(e)
override_extruder_toggle
override_extruder_toggle(print_organizer, override_value)

Overrides the extruder_toggle value for the printpoints with a user-defined value.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
override_value bool

Value to override the extruder_toggle values with.

required
Source code in src/compas_slicer/print_organization/print_organization_utilities/extruder_toggle.py
def override_extruder_toggle(print_organizer: BasePrintOrganizer, override_value: bool) -> None:
    """Overrides the extruder_toggle value for the printpoints with a user-defined value.

    Parameters
    ----------
    print_organizer: :class:`compas.print_organization.BasePrintOrganizer`
    override_value: bool
        Value to override the extruder_toggle values with.

    """
    if not isinstance(override_value, bool):
        raise TypeError("Override value must be of type bool")
    for printpoint in print_organizer.printpoints_iterator():
        printpoint.extruder_toggle = override_value
check_assigned_extruder_toggle
check_assigned_extruder_toggle(print_organizer)

Checks that all the printpoints have an assigned extruder toggle.

Source code in src/compas_slicer/print_organization/print_organization_utilities/extruder_toggle.py
def check_assigned_extruder_toggle(print_organizer: BasePrintOrganizer) -> bool:
    """ Checks that all the printpoints have an assigned extruder toggle. """
    all_toggles_assigned = True
    for printpoint in print_organizer.printpoints_iterator():
        if printpoint.extruder_toggle is None:
            all_toggles_assigned = False
    return all_toggles_assigned

gcode

G-code generation for compas_slicer.

This module generates G-code for FDM 3D printing from organized print points.

GcodeBuilder
GcodeBuilder()

Builder for constructing G-code output efficiently.

Uses a list internally and joins at the end for better performance than repeated string concatenation.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def __init__(self) -> None:
    self._lines: list[str] = []
comment
comment(text)

Add a comment line.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def comment(self, text: str) -> None:
    """Add a comment line."""
    self._lines.append(f";{text}")
cmd
cmd(gcode, comment='')

Add a G-code command with optional inline comment.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def cmd(self, gcode: str, comment: str = "") -> None:
    """Add a G-code command with optional inline comment."""
    if comment:
        self._lines.append(f"{gcode:<30} ;{comment}")
    else:
        self._lines.append(gcode)
blank
blank()

Add a blank line.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def blank(self) -> None:
    """Add a blank line."""
    self._lines.append("")
build
build()

Return the complete G-code as a string.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def build(self) -> str:
    """Return the complete G-code as a string."""
    return "\n".join(self._lines)
create_gcode_text
create_gcode_text(print_organizer, config=None)

Create G-code text from organized print points.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer

The print organizer containing printpoints.

required
config GcodeConfig | None

G-code configuration. If None, uses defaults.

None

Returns:

Type Description
str

Complete G-code file content.

Source code in src/compas_slicer/print_organization/print_organization_utilities/gcode.py
def create_gcode_text(
    print_organizer: BasePrintOrganizer, config: GcodeConfig | None = None
) -> str:
    """Create G-code text from organized print points.

    Parameters
    ----------
    print_organizer : BasePrintOrganizer
        The print organizer containing printpoints.
    config : GcodeConfig | None
        G-code configuration. If None, uses defaults.

    Returns
    -------
    str
        Complete G-code file content.

    """
    config = config or GcodeConfig()
    logger.info("Generating G-code")

    gb = GcodeBuilder()
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    _write_header(gb, config, timestamp)
    _write_purge_line(gb, config)
    final_z = _write_toolpath(gb, print_organizer, config)
    _write_footer(gb, config, final_z)

    return gb.build()

linear_velocity

set_linear_velocity_constant
set_linear_velocity_constant(print_organizer, v=25.0)

Sets the linear velocity parameter of the printpoints depending on the selected type.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
v float
25.0
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_constant(print_organizer: BasePrintOrganizer, v: float = 25.0) -> None:
    """Sets the linear velocity parameter of the printpoints depending on the selected type.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    v:  float. Velocity value (in mm/s) to set for printpoints. Defaults to 25 mm/s.
    """

    logger.info("Setting constant linear velocity")
    for printpoint in print_organizer.printpoints_iterator():
        printpoint.velocity = v
set_linear_velocity_per_layer
set_linear_velocity_per_layer(print_organizer, per_layer_velocities)

Sets the linear velocity parameter of the printpoints depending on the selected type.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
per_layer_velocities list[float]

A list of velocities (floats) with equal length to the number of layers.

required
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_per_layer(
    print_organizer: BasePrintOrganizer, per_layer_velocities: list[float]
) -> None:
    """Sets the linear velocity parameter of the printpoints depending on the selected type.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    per_layer_velocities: list
        A list of velocities (floats) with equal length to the number of layers.
    """

    logger.info("Setting per-layer linear velocity")
    if len(per_layer_velocities) != print_organizer.number_of_layers:
        raise ValueError(
            f'Wrong number of velocity values: got {len(per_layer_velocities)}, '
            f'need {print_organizer.number_of_layers} (one per layer)'
        )
    for printpoint, i, _j, _k in print_organizer.printpoints_indices_iterator():
        printpoint.velocity = per_layer_velocities[i]
set_linear_velocity_by_range
set_linear_velocity_by_range(print_organizer, param_func, parameter_range, velocity_range, bound_remapping=True)

Sets the linear velocity parameter of the printpoints depending on the selected type.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
param_func Callable[[PrintPoint], float]

and returns the parameter value that will be used for the remapping

required
parameter_range tuple[float, float]

An example of a parameter that can be used is the overhang angle, or the layer height.

required
velocity_range tuple[float, float]

The range of velocities where the parameter will be remapped

required
bound_remapping bool

If True, the remapping is bound in the domain velocity_range, else it is unbound.

True
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_by_range(
    print_organizer: BasePrintOrganizer,
    param_func: Callable[[PrintPoint], float],
    parameter_range: tuple[float, float],
    velocity_range: tuple[float, float],
    bound_remapping: bool = True,
) -> None:
    """Sets the linear velocity parameter of the printpoints depending on the selected type.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    param_func: function that takes as argument a :class: 'compas_slicer.geometry.Printpoint': get_param_func(pp)
        and returns the parameter value that will be used for the remapping
    parameter_range: tuple
        An example of a parameter that can be used is the overhang angle, or the layer height.
    velocity_range: tuple
        The range of velocities where the parameter will be remapped
    bound_remapping: bool
        If True, the remapping is bound in the domain velocity_range, else it is unbound.
    """

    logger.info("Setting linear velocity based on parameter range")
    for printpoint in print_organizer.printpoints_iterator():
        param = param_func(printpoint)
        if param is None:
            raise ValueError('The param_func does not return any value for calculating the velocity range.')
        if bound_remapping:
            v = remap(param, parameter_range[0], parameter_range[1], velocity_range[0], velocity_range[1])
        else:
            v = remap_unbound(param, parameter_range[0], parameter_range[1], velocity_range[0], velocity_range[1])
        printpoint.velocity = v
set_linear_velocity_by_overhang
set_linear_velocity_by_overhang(print_organizer, overhang_range, velocity_range, bound_remapping=True)

Set velocity by overhang by using set_linear_velocity_by_range.

An example function for how to use the 'set_linear_velocity_by_range'. In this case the parameter that controls the velocity is the overhang, measured as a dot product with the horizontal direction.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
overhang_range tuple[float, float]

should be within [0.0, 1.0]. For example a reasonable value would be [0.0, 0.5], that would be remapping overhangs up to 45 degrees

required
velocity_range tuple[float, float]
required
bound_remapping bool
True
Source code in src/compas_slicer/print_organization/print_organization_utilities/linear_velocity.py
def set_linear_velocity_by_overhang(
    print_organizer: BasePrintOrganizer,
    overhang_range: tuple[float, float],
    velocity_range: tuple[float, float],
    bound_remapping: bool = True,
) -> None:
    """Set velocity by overhang by using set_linear_velocity_by_range.

    An example function for how to use the 'set_linear_velocity_by_range'. In this case the parameter that controls the
    velocity is the overhang, measured as a dot product with the horizontal direction.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    overhang_range: tuple:
        should be within [0.0, 1.0]. For example a reasonable value would be [0.0, 0.5], that would
        be remapping overhangs up to 45 degrees
    velocity_range: tuple
    bound_remapping: bool
    """

    def param_func(ppt): return dot_vectors(ppt.mesh_normal, Vector(0.0, 0.0, 1.0))
    # returns values from 0.0 (no overhang) to 1.0 (horizontal overhang)
    set_linear_velocity_by_range(print_organizer, param_func, overhang_range, velocity_range, bound_remapping)

safety_printpoints

add_safety_printpoints
add_safety_printpoints(print_organizer, z_hop=10.0)

Generates a safety print point at the interruptions of the print paths.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer

An instance of the BasePrintOrganizer class.

required
z_hop float

Vertical distance (in millimeters) of the safety point above the PrintPoint.

10.0
Source code in src/compas_slicer/print_organization/print_organization_utilities/safety_printpoints.py
def add_safety_printpoints(print_organizer: BasePrintOrganizer, z_hop: float = 10.0) -> None:
    """Generates a safety print point at the interruptions of the print paths.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
        An instance of the BasePrintOrganizer class.
    z_hop: float
        Vertical distance (in millimeters) of the safety point above the PrintPoint.
    """
    if not check_assigned_extruder_toggle(print_organizer):
        raise ValueError('You need to set the extruder toggles first, before you can create safety points')
    logger.info(f"Generating safety print points with height {z_hop} mm")

    from compas_slicer.geometry import PrintPointsCollection

    new_collection = PrintPointsCollection()

    for i, layer in enumerate(print_organizer.printpoints):
        new_layer = PrintLayer()

        for j, path in enumerate(layer):
            new_path = PrintPath()

            for k, printpoint in enumerate(path):
                #  add regular printing points
                new_path.printpoints.append(printpoint)

                # add safety printpoints if there is an interruption
                if printpoint.extruder_toggle is False:

                    # safety ppt after current printpoint
                    new_path.printpoints.append(create_safety_printpoint(printpoint, z_hop, False))

                    #  safety ppt before next printpoint (if there exists one)
                    next_ppt = find_next_printpoint(print_organizer.printpoints, i, j, k)
                    if next_ppt and next_ppt.extruder_toggle is True:  # if it is a printing ppt
                        new_path.printpoints.append(create_safety_printpoint(next_ppt, z_hop, False))

            new_layer.paths.append(new_path)

        new_collection.layers.append(new_layer)

    #  finally, insert a safety print point at the beginning of the entire print
    try:
        safety_printpoint = create_safety_printpoint(new_collection[0][0][0], z_hop, False)
        new_collection[0][0].printpoints.insert(0, safety_printpoint)
    except (KeyError, IndexError) as e:
        logger.exception(e)

    #  the safety printpoint has already been added at the end since the last printpoint extruder_toggle_type is False
    print_organizer.printpoints = new_collection
create_safety_printpoint
create_safety_printpoint(printpoint, z_hop, extruder_toggle)

Parameters:

Name Type Description Default
printpoint PrintPoint
required
z_hop float
required
extruder_toggle bool
required

Returns:

Type Description
class: 'compas_slicer.geometry.PrintPoint'
Source code in src/compas_slicer/print_organization/print_organization_utilities/safety_printpoints.py
def create_safety_printpoint(printpoint: PrintPoint, z_hop: float, extruder_toggle: bool) -> PrintPoint:
    """

    Parameters
    ----------
    printpoint: :class: 'compas_slicer.geometry.PrintPoint'
    z_hop: float
    extruder_toggle: bool

    Returns
    ----------
    :class: 'compas_slicer.geometry.PrintPoint'
    """

    pt0 = printpoint.pt
    safety_printpoint = copy.deepcopy(printpoint)
    safety_printpoint.pt = pt0 + Vector(0, 0, z_hop)
    if safety_printpoint.frame is not None:
        safety_printpoint.frame.point = safety_printpoint.pt
    safety_printpoint.extruder_toggle = extruder_toggle
    return safety_printpoint

wait_time

set_wait_time_on_sharp_corners
set_wait_time_on_sharp_corners(print_organizer, threshold=0.5 * pi, wait_time=0.3)

Sets a wait time at the sharp corners of the path, based on the angle threshold.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
threshold float

angle_threshold

0.5 * pi
wait_time float

Time in seconds to introduce to add as a wait time

0.3
Source code in src/compas_slicer/print_organization/print_organization_utilities/wait_time.py
def set_wait_time_on_sharp_corners(
    print_organizer: BasePrintOrganizer, threshold: float = 0.5 * math.pi, wait_time: float = 0.3
) -> None:
    """
    Sets a wait time at the sharp corners of the path, based on the angle threshold.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    threshold: float
        angle_threshold
    wait_time: float
        Time in seconds to introduce to add as a wait time
    """
    number_of_wait_points = 0
    for printpoint, i, j, k in print_organizer.printpoints_indices_iterator():
        neighbors = print_organizer.get_printpoint_neighboring_items(i, j, k)
        prev_ppt = neighbors[0]
        next_ppt = neighbors[1]

        if prev_ppt and next_ppt:
            v_to_prev = normalize_vector(Vector.from_start_end(printpoint.pt, prev_ppt.pt))
            v_to_next = normalize_vector(Vector.from_start_end(printpoint.pt, next_ppt.pt))
            a = abs(Vector(*v_to_prev).angle(v_to_next))

            if a < threshold:
                printpoint.wait_time = wait_time
                printpoint.blend_radius = 0.0  # 0.0 blend radius for points where the robot will wait
                number_of_wait_points += 1
    logger.info(f'Added wait times for {number_of_wait_points} points')
set_wait_time_based_on_extruder_toggle
set_wait_time_based_on_extruder_toggle(print_organizer, wait_type, wait_time=0.3)

Sets a wait time for the printpoints, either before extrusion starts, after extrusion finishes, or in both cases.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
wait_type WaitType

wait_before_extrusion: sets a wait time before extrusion (extruder_toggle False to True) wait_after_extrusion: sets a wait time after extrusion (extruder_toggle True to False) wait_before_and_after_extrusion: sets a wait time before, and after extrusion wait_at_sharp_corners: sets a wait time at the sharp corners of the path

required
wait_time float

Time in seconds to introduce to add as a wait time

0.3
Source code in src/compas_slicer/print_organization/print_organization_utilities/wait_time.py
def set_wait_time_based_on_extruder_toggle(
    print_organizer: BasePrintOrganizer, wait_type: WaitType, wait_time: float = 0.3
) -> None:
    """
    Sets a wait time for the printpoints, either before extrusion starts,
    after extrusion finishes, or in both cases.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    wait_type: str
        wait_before_extrusion:  sets a wait time before extrusion (extruder_toggle False to True)
        wait_after_extrusion: sets a wait time after extrusion (extruder_toggle True to False)
        wait_before_and_after_extrusion: sets a wait time before, and after extrusion
        wait_at_sharp_corners: sets a wait time at the sharp corners of the path
    wait_time: float
        Time in seconds to introduce to add as a wait time
    """

    for printpoint in print_organizer.printpoints_iterator():
        if printpoint.extruder_toggle is None:
            raise ValueError('You need to set the extruder toggles first, before you can automatically set the wait time')

    logger.info("Setting wait time")

    for printpoint, i, j, k in print_organizer.printpoints_indices_iterator():
        number_of_wait_points = 0
        next_ppt = find_next_printpoint(print_organizer.printpoints, i, j, k)

        # for the brim layer don't add any wait times
        if not print_organizer.slicer.layers[i].is_brim and next_ppt:
            if wait_type == "wait_before_extrusion":
                if printpoint.extruder_toggle is False and next_ppt.extruder_toggle is True:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
            elif wait_type == "wait_after_extrusion":
                if printpoint.extruder_toggle is True and next_ppt.extruder_toggle is False:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
            elif wait_type == "wait_before_and_after_extrusion":
                if printpoint.extruder_toggle is False and next_ppt.extruder_toggle is True:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
                if printpoint.extruder_toggle is True and next_ppt.extruder_toggle is False:
                    next_ppt.wait_time = wait_time
                    next_ppt.blend_radius = 0.0
                    number_of_wait_points += 1
            else:
                logger.error(f'Unknown wait type: {wait_type}')

        logger.info(f'Added wait times for {number_of_wait_points} points')
override_wait_time
override_wait_time(print_organizer, override_value)

Overrides the wait_time value for the printpoints with a user-defined value.

Parameters:

Name Type Description Default
print_organizer BasePrintOrganizer
required
override_value float

Value to override the wait_time values with.

required
Source code in src/compas_slicer/print_organization/print_organization_utilities/wait_time.py
def override_wait_time(print_organizer: BasePrintOrganizer, override_value: float) -> None:
    """
    Overrides the wait_time value for the printpoints with a user-defined value.

    Parameters
    ----------
    print_organizer: :class:`compas_slicer.print_organization.BasePrintOrganizer`
    override_value: float
        Value to override the wait_time values with.
    """
    for printpoint in print_organizer.printpoints_iterator():
        printpoint.wait_time = override_value

scalar_field_print_organizer

ScalarFieldPrintOrganizer

ScalarFieldPrintOrganizer(slicer, config=None, DATA_PATH='.')

Bases: BasePrintOrganizer

Organize the printing process for scalar field contours.

Attributes:

Name Type Description
slicer ScalarFieldSlicer

An instance of ScalarFieldSlicer.

config InterpolationConfig

Configuration parameters.

DATA_PATH str | Path

Data directory path.

vertical_layers list[VerticalLayer]

Vertical layers from slicer.

horizontal_layers list[Layer]

Horizontal layers from slicer.

g_evaluation GradientEvaluation

Gradient evaluation object.

Source code in src/compas_slicer/print_organization/scalar_field_print_organizer.py
def __init__(
    self,
    slicer: ScalarFieldSlicer,
    config: InterpolationConfig | None = None,
    DATA_PATH: str | FilePath = ".",
) -> None:
    from compas_slicer.slicers import ScalarFieldSlicer

    if not isinstance(slicer, ScalarFieldSlicer):
        raise TypeError('Please provide a ScalarFieldSlicer')
    BasePrintOrganizer.__init__(self, slicer)
    self.DATA_PATH = DATA_PATH
    self.OUTPUT_PATH = utils.get_output_directory(DATA_PATH)
    self.config = config if config else InterpolationConfig()

    self.vertical_layers = slicer.vertical_layers
    self.horizontal_layers = slicer.horizontal_layers
    if len(self.vertical_layers) + len(self.horizontal_layers) != len(slicer.layers):
        raise ValueError(
            f"Layer count mismatch: {len(self.vertical_layers)} vertical + "
            f"{len(self.horizontal_layers)} horizontal != {len(slicer.layers)} total"
        )

    if len(self.horizontal_layers) > 0:
        if len(self.horizontal_layers) != 1:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        if not self.horizontal_layers[0].is_brim:
            raise ValueError("Only one brim horizontal layer is currently supported.")
        logger.info('Slicer has one horizontal brim layer.')

    self.g_evaluation: GradientEvaluation = self.add_gradient_to_vertices()
create_printpoints
create_printpoints()

Create the print points of the fabrication process.

Source code in src/compas_slicer/print_organization/scalar_field_print_organizer.py
def create_printpoints(self) -> None:
    """Create the print points of the fabrication process."""
    count = 0
    logger.info('Creating print points ...')
    with progressbar.ProgressBar(max_value=self.slicer.number_of_points) as bar:

        for _i, layer in enumerate(self.slicer.layers):
            print_layer = PrintLayer()

            for _j, path in enumerate(layer.paths):
                print_path = PrintPath()

                for k, point in enumerate(path.points):
                    normal = utils.get_normal_of_path_on_xy_plane(k, point, path, self.slicer.mesh)

                    h = self.config.avg_layer_height
                    printpoint = PrintPoint(pt=point, layer_height=h, mesh_normal=normal)

                    print_path.printpoints.append(printpoint)
                    bar.update(count)
                    count += 1

                print_layer.paths.append(print_path)

            self.printpoints.layers.append(print_layer)

    # transfer gradient information to printpoints
    transfer_mesh_attributes_to_printpoints(self.slicer.mesh, self.printpoints)

    # add non-planar print data to printpoints
    for layer in self.printpoints:
        for path in layer:
            for pp in path:
                grad_norm = pp.attributes['gradient_norm']
                grad = pp.attributes['gradient']
                pp.distance_to_support = grad_norm
                pp.layer_height = grad_norm
                pp.up_vector = Vector(*normalize_vector(grad))
                pp.frame = pp.get_frame()