Skip to content

Slicers

Mesh slicing algorithms.

slicers

Mesh slicing algorithms.

BaseSlicer

BaseSlicer(mesh)

Base class for slicers that holds all information for the slice process.

Do not use this class directly. Instead use PlanarSlicer or InterpolationSlicer. This class is meant to be extended for implementing various slicers.

Attributes:

Name Type Description
mesh Mesh

Input mesh, must be triangular (no quads or n-gons allowed).

layer_height float | None

Height between layers.

layers list[Layer]

List of layers generated by slicing.

Source code in src/compas_slicer/slicers/base_slicer.py
def __init__(self, mesh: Mesh) -> None:
    if not isinstance(mesh, Mesh):
        raise TypeError(f"Input mesh must be Mesh, not {type(mesh)}")
    utils.check_triangular_mesh(mesh)

    self.mesh = mesh
    logger.info(f"Input Mesh with: {len(list(self.mesh.vertices()))} vertices, {len(list(self.mesh.faces()))} faces")

    self.layer_height: float | None = None
    self.layers: list[Layer] = []

number_of_points property

number_of_points

Total number of points in the slicer.

number_of_layers property

number_of_layers

Total number of layers.

number_of_paths property

number_of_paths

Total paths, open paths, closed paths.

vertical_layers property

vertical_layers

List of all vertical layers in the slicer.

horizontal_layers property

horizontal_layers

List of all non-vertical layers in the slicer.

slice_model

slice_model(*args, **kwargs)

Slices the model and applies standard post-processing.

Source code in src/compas_slicer/slicers/base_slicer.py
def slice_model(self, *args: Any, **kwargs: Any) -> None:
    """Slices the model and applies standard post-processing."""
    self.generate_paths()
    self.remove_invalid_paths_and_layers()
    self.post_processing()

generate_paths abstractmethod

generate_paths()

Generate paths. To be implemented by inheriting classes.

Source code in src/compas_slicer/slicers/base_slicer.py
@abstractmethod
def generate_paths(self) -> None:
    """Generate paths. To be implemented by inheriting classes."""
    pass

post_processing

post_processing()

Applies standard post-processing: seams_align and unify_paths.

Source code in src/compas_slicer/slicers/base_slicer.py
def post_processing(self) -> None:
    """Applies standard post-processing: seams_align and unify_paths."""
    self.close_paths()
    seams_align(self, align_with="next_path")
    unify_paths_orientation(self)
    self.close_paths()
    logger.info(f"Created {len(self.layers)} Layers with {self.number_of_points} total points")

close_paths

close_paths()

For closed paths, ensures first and last point are identical.

Source code in src/compas_slicer/slicers/base_slicer.py
def close_paths(self) -> None:
    """For closed paths, ensures first and last point are identical."""
    for layer in self.layers:
        for path in layer.paths:
            if path.is_closed and distance_point_point_sqrd(path.points[0], path.points[-1]) > 0.00001:
                path.points.append(path.points[0])

remove_invalid_paths_and_layers

remove_invalid_paths_and_layers()

Removes invalid layers and paths from the slicer.

Source code in src/compas_slicer/slicers/base_slicer.py
def remove_invalid_paths_and_layers(self) -> None:
    """Removes invalid layers and paths from the slicer."""
    paths_to_remove = []
    layers_to_remove = []

    for i, layer in enumerate(self.layers):
        for j, path in enumerate(layer.paths):
            if len(path.points) < 2:
                paths_to_remove.append(path)
                logger.warning(f"Invalid Path: Layer {i}, Path {j}, {path}")
                if len(layer.paths) == 1:
                    layers_to_remove.append(layer)
                    logger.warning(f"Invalid Layer: Layer {i}, {layer}")
        if len(layer.paths) < 1:
            layers_to_remove.append(layer)
            logger.warning(f"Invalid Layer: Layer {i}, {layer}")

    for layer in self.layers:
        for path in list(layer.paths):
            if path in paths_to_remove:
                layer.paths.remove(path)
        if layer in layers_to_remove:
            self.layers.remove(layer)

find_vertical_layers_with_first_path_on_base

find_vertical_layers_with_first_path_on_base()

Find vertical layers whose first path is on the base.

Returns:

Type Description
tuple[list[Path], list[int]]

Paths on base and their vertical layer indices.

Source code in src/compas_slicer/slicers/base_slicer.py
def find_vertical_layers_with_first_path_on_base(self) -> tuple[list[Path], list[int]]:
    """Find vertical layers whose first path is on the base.

    Returns
    -------
    tuple[list[Path], list[int]]
        Paths on base and their vertical layer indices.

    """
    vertices = list(self.mesh.vertices_attributes('xyz'))
    bbox = bounding_box(vertices)
    z_min = min(p[2] for p in bbox)
    paths_on_base = []
    vertical_layer_indices = []
    d_threshold = 30

    for i, vertical_layer in enumerate(self.vertical_layers):
        first_path = vertical_layer.paths[0]
        avg_z_dist = np.average(np.array([abs(pt[2] - z_min) for pt in first_path.points]))
        if avg_z_dist < d_threshold:
            paths_on_base.append(first_path)
            vertical_layer_indices.append(i)

    return paths_on_base, vertical_layer_indices

printout_info

printout_info()

Prints out slicing information.

Source code in src/compas_slicer/slicers/base_slicer.py
def printout_info(self) -> None:
    """Prints out slicing information."""
    no_of_paths, closed_paths, open_paths = self.number_of_paths
    logger.info("---- Slicer Info ----")
    logger.info(f"Number of layers: {self.number_of_layers}")
    logger.info(f"Number of paths: {no_of_paths}, open: {open_paths}, closed: {closed_paths}")
    logger.info(f"Number of sampling printpoints: {self.number_of_points}")

from_data classmethod

from_data(data)

Construct a slicer from its data representation.

Parameters:

Name Type Description Default
data dict

The data dictionary.

required

Returns:

Type Description
BaseSlicer

The constructed slicer.

Source code in src/compas_slicer/slicers/base_slicer.py
@classmethod
def from_data(cls, data: dict[str, Any]) -> BaseSlicer:
    """Construct a slicer from its data representation.

    Parameters
    ----------
    data : dict
        The data dictionary.

    Returns
    -------
    BaseSlicer
        The constructed slicer.

    """
    mesh = Mesh.__from_data__(data["mesh"])
    slicer = cls(mesh)
    layers_data = data["layers"]
    for layer_key in layers_data:
        if layers_data[layer_key]["layer_type"] == "horizontal_layer":
            slicer.layers.append(Layer.from_data(layers_data[layer_key]))
        else:
            slicer.layers.append(VerticalLayer.from_data(layers_data[layer_key]))
    slicer.layer_height = data["layer_height"]
    return slicer

to_json

to_json(filepath, name)

Writes the slicer to a JSON file.

Source code in src/compas_slicer/slicers/base_slicer.py
def to_json(self, filepath: str | FilePath, name: str) -> None:
    """Writes the slicer to a JSON file."""
    utils.save_to_json(self.to_data(), filepath, name)

to_data

to_data()

Returns a dictionary of structured data representing the slicer.

Returns:

Type Description
dict

The slicer's data.

Source code in src/compas_slicer/slicers/base_slicer.py
def to_data(self) -> dict[str, Any]:
    """Returns a dictionary of structured data representing the slicer.

    Returns
    -------
    dict
        The slicer's data.

    """
    mesh = self.mesh.copy()
    v_key = next(iter(mesh.vertices()))
    v_attrs = mesh.vertex_attributes(v_key)
    for attr_key in v_attrs:
        if not utils.is_jsonable(v_attrs[attr_key]):
            logger.error(f"vertex: {attr_key} {v_attrs[attr_key]}")
            for v in mesh.vertices():
                mesh.unset_vertex_attribute(v, attr_key)

    f_key = next(iter(mesh.faces()))
    f_attrs = mesh.face_attributes(f_key)
    for attr_key in f_attrs:
        if not utils.is_jsonable(f_attrs[attr_key]):
            logger.error(f"face: {attr_key} {f_attrs[attr_key]}")
            mesh.update_default_face_attributes({attr_key: 0.0})

    return {
        "layers": self.get_layers_dict(),
        "mesh": mesh.__data__,
        "layer_height": self.layer_height,
    }

get_layers_dict

get_layers_dict()

Returns a dictionary of layers.

Source code in src/compas_slicer/slicers/base_slicer.py
def get_layers_dict(self) -> dict[int, dict[str, Any]]:
    """Returns a dictionary of layers."""
    return {i: layer.to_data() for i, layer in enumerate(self.layers)}

PlanarSlicer

PlanarSlicer(mesh, layer_height=2.0, slice_height_range=None)

Bases: BaseSlicer

Generates planar contours on a mesh that are parallel to the xy plane.

Attributes:

Name Type Description
mesh Mesh

Input mesh, must be triangular (no quads or n-gons allowed).

layer_height float

Distance between layers (slices) in mm.

slice_height_range tuple[float, float] | None

Optional tuple (z_start, z_end) to slice only part of the model. Values are relative to mesh minimum height.

Source code in src/compas_slicer/slicers/planar_slicer.py
def __init__(
    self,
    mesh: Mesh,
    layer_height: float = 2.0,
    slice_height_range: tuple[float, float] | None = None,
) -> None:
    logger.info('PlanarSlicer')
    BaseSlicer.__init__(self, mesh)

    self.layer_height = layer_height
    self.slice_height_range = slice_height_range

generate_paths

generate_paths()

Generate the planar slicing paths.

Source code in src/compas_slicer/slicers/planar_slicer.py
def generate_paths(self) -> None:
    """Generate the planar slicing paths."""
    z = [self.mesh.vertex_attribute(key, 'z') for key in self.mesh.vertices()]
    min_z, max_z = min(z), max(z)

    if self.slice_height_range:
        if min_z <= self.slice_height_range[0] <= max_z and min_z <= self.slice_height_range[1] <= max_z:
            logger.info(f"Slicing mesh in range from Z = {self.slice_height_range[0]} to Z = {self.slice_height_range[1]}.")
            max_z = min_z + self.slice_height_range[1]
            min_z = min_z + self.slice_height_range[0]
        else:
            logger.warning("Slice height range out of bounds of geometry, slice height range not used.")

    d = abs(min_z - max_z)
    no_of_layers = int(d / self.layer_height) + 1
    normal = Vector(0, 0, 1)
    planes = [Plane(Point(0, 0, min_z + i * self.layer_height), normal) for i in range(no_of_layers)]

    logger.info("Planar slicing using CGAL ...")
    self.layers = create_planar_paths(self.mesh, planes)

InterpolationSlicer

InterpolationSlicer(mesh, preprocessor=None, config=None)

Bases: BaseSlicer

Generates non-planar contours that interpolate user-defined boundaries.

Attributes:

Name Type Description
mesh Mesh

Input mesh, must be triangular (no quads or n-gons allowed). Topology matters; irregular tessellation can lead to undesired results. Recommend: re-topologize, triangulate, and weld mesh in advance.

preprocessor InterpolationSlicingPreprocessor | None

Preprocessor containing compound targets.

config InterpolationConfig

Interpolation configuration.

n_multiplier float

Multiplier for number of isocurves.

Source code in src/compas_slicer/slicers/interpolation_slicer.py
def __init__(
    self,
    mesh: Mesh,
    preprocessor: InterpolationSlicingPreprocessor | None = None,
    config: InterpolationConfig | None = None,
) -> None:
    logger.info('InterpolationSlicer')
    BaseSlicer.__init__(self, mesh)

    # make sure the mesh of the preprocessor and the mesh of the slicer match
    if preprocessor and len(list(mesh.vertices())) != len(list(preprocessor.mesh.vertices())):
        raise ValueError(
            f"Mesh vertex count mismatch: slicer mesh has {len(list(mesh.vertices()))} vertices, "
            f"preprocessor mesh has {len(list(preprocessor.mesh.vertices()))} vertices"
        )

    self.config = config if config else InterpolationConfig()
    self.preprocessor = preprocessor
    self.n_multiplier: float = 1.0

generate_paths

generate_paths()

Generate curved paths.

Source code in src/compas_slicer/slicers/interpolation_slicer.py
def generate_paths(self) -> None:
    """Generate curved paths."""
    if not self.preprocessor:
        raise ValueError('You need to provide a pre-processor in order to generate paths.')

    avg_layer_height = self.config.avg_layer_height
    n = find_no_of_isocurves(self.preprocessor.target_LOW, self.preprocessor.target_HIGH, avg_layer_height)
    params_list = get_interpolation_parameters_list(n)
    logger.info(f'{n} paths will be generated')

    vertical_layers_manager = VerticalLayersManager(avg_layer_height)

    # create paths + layers
    with progressbar.ProgressBar(max_value=len(params_list)) as bar:
        for i, param in enumerate(params_list):
            assign_interpolation_distance_to_mesh_vertices(self.mesh, param, self.preprocessor.target_LOW,
                                                           self.preprocessor.target_HIGH)
            contours = ScalarFieldContours(self.mesh)
            contours.compute()
            contours.add_to_vertical_layers_manager(vertical_layers_manager)

            bar.update(i)  # advance progress bar

    self.layers = vertical_layers_manager.layers

ScalarFieldSlicer

ScalarFieldSlicer(mesh, scalar_field, no_of_isocurves, config=None)

Bases: BaseSlicer

Generates the isocontours of a scalar field defined on mesh vertices.

Attributes:

Name Type Description
mesh Mesh

Input mesh, must be triangular (no quads or n-gons allowed). Topology matters; irregular tessellation can lead to undesired results. Recommend: re-topologize, triangulate, and weld mesh in advance.

scalar_field list[float]

One float per vertex representing the scalar field.

no_of_isocurves int

Number of isocontours to generate.

config InterpolationConfig

Configuration parameters.

Source code in src/compas_slicer/slicers/scalar_field_slicer.py
def __init__(
    self,
    mesh: Mesh,
    scalar_field: Sequence[float],
    no_of_isocurves: int,
    config: InterpolationConfig | None = None,
) -> None:
    logger.info('ScalarFieldSlicer')
    BaseSlicer.__init__(self, mesh)

    self.no_of_isocurves = no_of_isocurves
    self.scalar_field: list[float] = list(np.array(scalar_field) - np.min(np.array(scalar_field)))
    self.config = config if config else InterpolationConfig()

    mesh.update_default_vertex_attributes({'scalar_field': 0})

generate_paths

generate_paths()

Generate isocontours.

Source code in src/compas_slicer/slicers/scalar_field_slicer.py
def generate_paths(self) -> None:
    """Generate isocontours."""
    start_domain, end_domain = min(self.scalar_field), max(self.scalar_field)
    step = (end_domain - start_domain) / (self.no_of_isocurves + 1)

    max_dist = self.config.vertical_layers_max_centroid_dist
    vertical_layers_manager = VerticalLayersManager(max_dist)

    # create paths + layers
    with progressbar.ProgressBar(max_value=self.no_of_isocurves) as bar:
        for i in range(0, self.no_of_isocurves + 1):
            for vkey, data in self.mesh.vertices(data=True):
                if i == 0:
                    data['scalar_field'] = self.scalar_field[vkey] - 0.05 * step  # things can be tricky in the edge
                else:
                    data['scalar_field'] = self.scalar_field[vkey] - i * step

            contours = ScalarFieldContours(self.mesh)
            contours.compute()
            contours.add_to_vertical_layers_manager(vertical_layers_manager)

            bar.update(i)  # advance progress bar

    self.layers = vertical_layers_manager.layers

UVSlicer

UVSlicer(mesh, vkey_to_uv, no_of_isocurves, config=None)

Bases: BaseSlicer

Generates contours on the mesh corresponding to straight lines on the UV plane.

Uses a UV map (from 3D space to plane) defined on mesh vertices.

Attributes:

Name Type Description
mesh Mesh

Input mesh, must be triangular (no quads or n-gons allowed). Topology matters; irregular tessellation can lead to undesired results. Recommend: re-topologize, triangulate, and weld mesh in advance.

vkey_to_uv dict[int, tuple[float, float]]

Mapping from vertex key to UV coordinates. UV should be in [0,1].

no_of_isocurves int

Number of levels to generate.

config InterpolationConfig

Configuration parameters.

Source code in src/compas_slicer/slicers/uv_slicer.py
def __init__(
    self,
    mesh: Mesh,
    vkey_to_uv: dict[int, tuple[float, float]],
    no_of_isocurves: int,
    config: InterpolationConfig | None = None,
) -> None:
    logger.info('UVSlicer')
    BaseSlicer.__init__(self, mesh)

    self.vkey_to_uv = vkey_to_uv
    self.no_of_isocurves = no_of_isocurves
    self.config = config if config else InterpolationConfig()

    u = [self.vkey_to_uv[vkey][0] for vkey in mesh.vertices()]
    v = [self.vkey_to_uv[vkey][1] for vkey in mesh.vertices()]
    u_arr = np.array(u) * float(no_of_isocurves + 1)
    vkey_to_i = self.mesh.key_index()

    mesh.update_default_vertex_attributes({'uv': 0})
    for vkey in mesh.vertices():
        mesh.vertex_attribute(vkey, 'uv', (u_arr[vkey_to_i[vkey]], v[vkey_to_i[vkey]]))

generate_paths

generate_paths()

Generate isocontours.

Source code in src/compas_slicer/slicers/uv_slicer.py
def generate_paths(self) -> None:
    """Generate isocontours."""
    paths_type = 'flat'  # 'spiral' # 'zigzag'
    v_left, v_right = 0.0, 1.0 - 1e-5

    max_dist = self.config.vertical_layers_max_centroid_dist
    vertical_layers_manager = VerticalLayersManager(max_dist)

    # create paths + layers
    with progressbar.ProgressBar(max_value=self.no_of_isocurves) as bar:
        for i in range(0, self.no_of_isocurves + 1):
            u_val = float(i)
            if i == 0:
                u_val += 0.05  # contours are a bit tricky in the edges
            if paths_type == 'spiral':
                u1, u2 = u_val, u_val + 1.0
            else:  # 'flat'
                u1 = u2 = u_val

            p1 = (u1, v_left)
            p2 = (u2, v_right)

            contours = UVContours(self.mesh, p1, p2)
            contours.compute()
            contours.add_to_vertical_layers_manager(vertical_layers_manager)

            bar.update(i)  # advance progress bar

    self.layers = vertical_layers_manager.layers