pyicesl
specification
IceSL python module Here is the official documentation of the python bindings of IceSL, named pyicesl
. We document the different modules, classes and functions to use IceSL via the python language. pyicesl
currently includes a subset of the modeling and slicing capabilities of IceSL. It does NOT include a graphical interface, thus not featuring model viewing neither paths pre-visualization nor a printing settings panel.
Some familiarity with the use of IceSL, its pipeline, the printing settings and how geometry is specified, is recommended before using this module. Depending on the community engagement with pyicesl
, less popular features of IceSL (e.g., DLP slicer
) might be included in the future.
pyicesl
Module
To use pyicesl
, download the python module of IceSL corresponding to your platform. This should be a .whl
file.
Create an environment to install pyicesl
as follows:
In Windows:
python -m venv pyicesl_env
cd pyicesl_env
Scripts\Activate
pip install numpy
python -m pip install --no-index --find-links=\path\to\folder\of\whl\file pyicesl
python
In Linux:
python3 -m venv pyicesl_env
cd pyicesl_env
source bin/activate
pip install numpy
python3 -m pip install --no-index --find-links=/path/to/folder/of/whl/file pyicesl
python3
This environment only has to be created once and can be reused later. pyicesl
is divided in two main modules modeler
and slicer
.
modeler
Module
The modeler
module includes the functions to create mesh primitives, implicits and perform CSG operations with them.
import pyicesl.modeler
Mesh Primitives
pyicesl.modeler.cube ( size = 1, centered = False )
pyicesl.modeler.cube ( size_x, size_y, size_z, centered = False )
pyicesl.modeler.sphere ( radius )
pyicesl.modeler.cylinder ( radius = 1, height = 1, centered = False )
pyicesl.modeler.cone ( radius_bottom = 1, radius_top = 1, height = 1, centered = False )
pyicesl.modeler.polyhedron ( points, triangles )
pyicesl.modeler.load ( mesh_file )
The cube
, sphere
, cylinder
and cone
functions create their corresponding namesakes with their respective parameters. centered
centers the primitive along its height (i.e., z axis).
The polyhedron
function creates a mesh with its lists of vertices and triangles specified in points
and triangles
respectively.
The load
function loads the mesh (i.e., .stl
or .obj
file) specified in mesh_file
.
All these functions return an object of class pyicesl.slicer.Shape
.
Examples:
import pyicesl.modeler as modeler
myModel = modeler.load("/path/to/model.stl")
myPoly = model.polyhedron(
points = [ [0,0,0], [1,0,0], [0,1,0], [1,1,0], [0.5,0.5,1] ],
triangles = [ [0,3,1], [0,2,3], [0,1,4], [1,3,4], [3,2,4], [2,0,4] ]
)
Implicits
pyicesl.modeler.implicit_distance_field ( glsl_runtime, implicit_bbox )
pyicesl.modeler.implicit_solid ( glsl_runtime, implicit_bbox, voxel_size )
The functions implicit_distance_field
and implicit_solid
create an implicit shape using the function distance
/solid
respectively specified in the GLSL code glsl_runtime
. Implicit evaluation boundaries are given in the parameters implicit_bbox
(a tuple of two 3-element lists). In the case of implicit_solid
, its evaluation resolution can be specified in voxel_size
. See the documentation for more details on how IceSL creates and evaluates implicits.
Both functions return an object of class pyicesl.modeler.Implicit
.
Example
import pyicesl.modeler as modeler
imp = modeler.implicit_distance_field(
glsl_runtime = """
float distance(vec3 p) {
return -1;
}
""",
implicit_bbox = ([-10,-10,-10], [10,10,10])
)
import pyicesl.modeler as modeler
with open("path/to/implicit.glsl") as imp_file:
imp = modeler.implicit_solid(
glsl_runtime = imp_file.read(),
implicit_box = ([0,0,0], [5,5,5]),
voxel_size = 0.5
)
Transformations
pyicesl
does not offer precalculated geometrical transformations. Instead, use the numpy
and quaternion
modules to create these. numpy
's matrix class can be used to apply (i.e., multiply) transformations with shapes. Install the quaternion
module with pip install numpy-quaternion
.
import numpy
import quaternion
def translate( tx, ty, tz ):
return numpy.array([[1.0, 0.0, 0.0, tx ],
[0.0, 1.0, 0.0, ty ],
[0.0, 0.0, 1.0, tz ],
[0.0, 0.0, 0.0, 1.0]])
def scale( sx, sy, sz ):
return numpy.array([[sx , 0.0, 0.0, 0.0],
[0.0, sy , 0.0, 0.0],
[0.0, 0.0, sz , 0.0],
[0.0, 0.0, 0.0, 1.0]])
def rotate( rx, ry, rz ):
matrix = numpy.identity(4)
matrix[:3,:3] = quaternion.as_rotation_matrix(
quaternion.from_rotation_vector(
numpy.array( [ rx, ry, rz ] ) ) )
return matrix
Example (following the snippet above):
import pyicesl.modeler as modeler
myCylinder = modeler.cylinder()
myCylinder = scale(5,5,20) * myCylinder
myCylinder = rotate(numpy.pi / 2.0, 0, 0) * myCylinder
CSG Operations
CSG operations over shapes are possible with pyicesl
.
pyicesl.modeler.union ( shape1, shape2, ... )
pyicesl.modeler.intersection ( shape1, shape2, ... )
pyicesl.modeler.difference ( shape1, shape2, ... )
The union
, intersection
and difference
create a new object of class pyicesl.slicer.Shape
as the result of their respective operation. We remind the reader that difference
is the subtraction from shape1
of the union of the other shape arguments.
Example:
import pyicesl.modeler as modeler
myCube = modeler.cube(size = 40, centered = True)
mySphere = modeler.sphere(radius = 25)
myEnsemble = modeler.difference(myCube, mySphere)
slicer
Module
The slicer
module contains all the classes necessary to slice geometry as well as modify printing parameters. To allow for a flexible pipeline, this module also features functions and classes to manipulate brushes, contours, slicing plans, and deposition paths.
Brushes and Shapes
When creating a primitive or an implicit as well as doing CSG operations upon these, the result is an object of class Shape
:
class pyicesl.slicer.Shape()
pyicesl.slicer.Shape.bbox
Property bbox
is the bounding box of the shape encoded as a two-element tuple that represent the minimal and maximal corners respectively. Each corner is in turn encoded as a numpy.ndarray
.
Shapes can be transformed via left-hand multiplication with a transformation matrix represented as a numpy.ndarray
.
Example:
import numpy
def translate(tx, ty, tz):
return numpy.array([[1.0, 0.0, 0.0, tx ],
[0.0, 1.0, 0.0, ty ],
[0.0, 0.0, 1.0, tz ],
[0.0, 0.0, 0.0, 1.0]])
myTransformedCube = translate(10,20,0) * modeler.cube(10) # 1cm cube moved 1cm to the right and 2cm to the back
Shapes can be in turn emitted to an object Brush
.
class pyicesl.slicer.Brush( shape = None, id_brush = 0 )
pyicesl.slicer.Brush.emit( shape )
pyicesl.slicer.Brush.clear()
A brush as an ID associated with it, see the documentation to understand the concept of brush. Method emit
takes an object of class Shape
and unions it with previously emitted shapes in the brush itself. Method clear
erases any shapes emitted in the brush.
Example:
import pyicesl.modeler as modeler
import pyicesl.slicer as slicer
myBrush = slicer.Brush(shape = modeler.sphere(25)) # brush with an emitted cube
myBrush.emit(shape = modeler.cube(40)) # union of sphere with cube inside brush
myBrush.clear() # empty brush
Slicing Plans
Slicing plans are objects that define where the geometry is sliced across its height.
class pyicesl.slicer.SlicingPlan()
pyicesl.slicer.SlicingPlan.slices
pyicesl.slicer.Slicing.get_slice_range( from_mm, to_mm )
Class SlicingPlan
is considered a base class and virtual. Property slices
is a numpy.array
where each entry is a two-element numpy.array
indicating the bottom and top heights respectively of each slice that comprise the complete slicing plan. Method get_slice_range
returns a two-element tuple of the first and last index in the array slices
that make up the slices from from_mm
to to_mm
.
class pyicesl.slicer.UniformSlicingPlan( from_mm, to_mm, thickness )
class pyicesl.slicer.AdaptiveSlicingPlan( slices )
Class UniformSlicingPlan
divides the height evenly between from_mm
and to_mm
in steps of size thickeness
. Lastly, class AdaptiveSlicingPlan
provides flexibility to locate the slices at whatever height is specified in the parameter slices
. It is important to make sure that each top of a slice correspond to the bottom of the next slice when specifying the slices manually.
Example:
import pyicesl.modeler as modeler
import pyicesl.slicer as slicer
myShape = modeler.cube(size = 10, centered = True)
myPlan = slicer.UniformSlicingPlan(myShape.bbox[0][2], myShape.bbox[1][2], 0.2) # from the bottom to the top of the cube with thickness 0.2mm
myPlan.slices[myPlan.get_slice_range(-2, 2)[0] : myPlan.get_slice_range(-2, 2)[1]] # get slice range from -2.0mm to 2.0mm.
myAdaptivePlan = slicer.AdaptiveSlicingPlan([[-1.0, -0.5], [-0.5, -0.25], [-0.25, 0], [0, 0.25], [0.25, 0.5], [0.5, 1.0]]) # slicing plan with differen layer heights
Contours and Outlines
Contours are 2D polygons that are usually the result of slicing a 3D geometry. In this sense, they represent the discretization of 3D on 2D.
class pyicesl.slicer.Contour( vertices, brush_id, fp_per_int )
pyicesl.slicer.Contour.get_points()
pyicesl_slicer.Contour.set_points( points )
A contour has a list of 2D points (whose coordinates are measures in mm) that represent the contour itself (i.e., parameter vertices
). When creating a contour, a brush ID (i.e., brush_id
) has to be assigned to it. Parameter fp_per_int
sets the resolution of the slicing and cannot be more than 1.0
. A good default value is 0.01
.
Method get_points
returns the 2D points that make up the contour. set_points
sets the new contour points through parameter points
.
Because contours are polygons, they have to be closed and specified in a clockwise manner. A contour is considered closed if its last point is equal to its first.
Outlines are contours without a brush ID assigned. They are used as transient data objects and thus cannot be created, only received as output of a pipeline operation and modified.
class python.slicer.Outline
pyicesl.slicer.Outline.get_points()
pyicesl.slicer.Outline.set_points( points, is_closed )
Example:
import pyicesl.slicer as slicer
myContourBrush0 = slicer.Contour([[0,0], [0,10], [10, 10], [10, 0], [0,0]], 0, 0.01) # 1cm square brush 0
myContourBrush1 = slicer.Contour([[0,0], [0,0]], 1, 0.01) # Dummy contour on brush 1
myContourBrush1.set_points( points = myContourBrush0.get_points() ) # 1mm open square
Deposition Paths
A deposition path is a list of points (whose coordinates are integers) where material will be deposited along. They are the basic building block of a processed 3D printing layer. For this reason, they are inherently 2D, though they can be "lifted".
class pyicesl.slicer.Path( points, is_closed, fp_per_int, tool = 0, path_type = 0 )
pyicesl.slicer.Path.closed # read-only
pyicesl.slicer.Path.tool
pyicesl.slicer.Path.path_type
pyicesl.slicer.Path.get_points()
pyicesl.slicer.Path.set_points( points, is_closed )
pyicesl.slicer.Path.path_attributes # read-only
pycessl.slicer.Path.add_path_attribute( name )
pyicesl.slicer.Path.get_path_attribute_value( name )
pyicesl.slicer.Path.set_path_attribute_value( name, value )
pyicesl.slicer.Path.per_vertex_attributes # read-only
pyicesl.slicer.Path.add_per_vertex_attribute( name )
pyicesl.slicer.Path.get_per_vertex_attribute_values( name )
pyicesl.slicer.Path.get_per_vertex_attribute_value( name, vertex_id )
pyicesl.slicer.Path.set_per_vertex_attribute_values( name, value )
pyicesl.slicer.Path.set_per_vertex_attribute_value( name, vertex_id, value )
A path
is crated with a set of points points
, a scale factor fp_per_int
and assigned an extruder ID with tool
and a deposition path type with path_type
. When parameter fp_per_int
is set to 0.001
, a unit in points
correspond to 1mm.
A complete list of the path types is as follows:
pyicesl.slicer.Path.Undefined
pyicesl.slicer.Path.Travel
pyicesl.slicer.Path.Perimeter
pyicesl.slicer.Path.Infill
pyicesl.slicer.Path.GapFill
pyicesl.slicer.Path.Shell
pyicesl.slicer.Path.Support
pyicesl.slicer.Path.Bridge
pyicesl.slicer.Path.Raft
pyicesl.slicer.Path.Brim
pyicesl.slicer.Path.Shield
pyicesl.slicer.Path.Tower
pyicesl.slicer.Path.Visible
pyicesl.slicer.Path.FreeZipper
pyicesl.slicer.Path.NoRetract
pyicesl.slicer.Path.TravelAbove
pyicesl.slicer.Path.Cavity
pyicesl.slicer.Path.Cover
pyicesl.slicer.Path.PrimeAtStart
pyicesl.slicer.Path.Ironing
get_points
and set_points
work similarly as in class pyicesl.slicer.Contour
. IceSL deposition paths have attributes concerning the complete path (i.e., pyicesl.slicer.Path.path_attributes
) and attributes that apply to specific points in the path (i.e., pyicesl.slicer.Path.per_vertex_attributes
). Thus, add_path/per_vertex_attribute
add name
as a path or per-vertex attribute respectively to the path.
get/set_path_attribute_value
and get/set_per_vertex_attribute_values
take a path or vertex attribute name respectively and either query its value or assign it (i.e., parameter value
) as an individual value (path attribute) or a list of values (per-vertex value). get/set_per_vertex_attribute_value
follow a similar behavior, however in this case the parameter vertex_id
indicates the index in the list of points of the path (i.e., pyicesl.slicer.Path.get_points()
).
Example:
import pyicesl.slicer as slicer
myPath = slicer.Path([[0,0], [0,10], [10, 10], [10, 0], [0,0]], True, 0.001, 0, slicer.Path.Type.Brim) # 1cm brim square in extruder 0
if not "flow_multiplier" in myPath.path_attributes:
myPath.add_path_attribute("flow_multiplier") # add flow-multiplier to path
myPath.set_path_attribute_value("flow_multiplier", 0.5) # 50% of flow
if not "zoffset" in myPath.per_vertex_attributes:
myPath.add_per_vertex_attribute("zoffset") # add z-offset to vertices
zoffset = 0
for p in range(len(myPath.get_points())): # raise the path in a spiral fashion
myPath.set_per_vertex_attribute_value("zoffset", p, zoffset)
zoffset += 0.1 # raise 0.1mm
Application
Class
The Application
class is used to create services (e.g., Filament Slicer) and it internally manages resources and assets.
class pyicesl.slicer.Application()
pyicesl.slicer.Application.get_assets_dir()
pyicesl.slicer.Application.dispose()
Method get_assets_dir
indicates where the IceSL assets are located (e.g., printer profiles, infillers, etc). Method dispose
frees general and gpu resources.
FilamentSlicer
class
The FilamentSlicer
class is the service responsible for slicing geometry, processing contours, creating deposition paths and generating GCode instructions.
class pyicesl.slicer.FilamentSlicer( appplication )
pyicesl.slicer.FilamentSlicer.slice( brushes, slicing_plan )
pyicesl.slicer.FilamentSlicer.process( contours_per_slice, slicing_plan )
pyicesl.slicer.FilamentSlicer.save( input, slicing_plan, saveFilename )
pyicesl.slicer.FilamentSlicer.run( brushes, saveFilename)
Method slice
takes a list of pyicesl.slicer.Brush
objects (i.e., brushes
), a pyicesl.slicer.SlicingPlan
(i.e., slicing_plan
) and slices the geometry emitted in the brushes according to the slicing plan specified. Its return value is a list wherein each element represents a sliced layer ordered by height. Each representation of a layer is in itself a list of pyicesl.slicer.Contour
objects (i.e., contours) resulting from the slicing process.
Method process
takes a list of lists of pyicesl.slicer.Contour
objects (i.e., contours_per_slice
; the output of slice
), an object pyicesl.slicer.SlicingPlan
(i.e., slicing_plan
) and creates deposition paths corresponding to each contour in each layer. Its output is a list of tuples wherein each element represents a processed layer ordered by height. Each representation of a layer is in itself a two-element tuple where the first element is a list of pyicesl.slicer.Path
objects (i.e., the deposition paths) resulting from the processing method. The second tuple element is a Boolean indicating whether the layer is a result of a spiralization calculation.
In method save
, parameter input
is a list of two-element tuples (i.e., the output of processing
; the deposition paths) and parameter saveFilename
is a filename destination. The method translates input
into 3D printing instructions (i.e., GCode
).
Finally, method run
executes the full pipeline (i.e., slice
, process
and save
methods) with a uniform slicing pipeline on a list of pyicesl.slicer.Brush
objects (i.e., brushes
) with output on file saveFilename
.
pyicesl.slicer.FilamentSlicer.get_processing_and_tight_boxes( brushes )
pyicesl.slicer.FilamentSlicer.getSlicingPlan( tight_box )
get_processing_and_tight_boxes
calculates the processing and tight boxes of the geometry emitted in brushes
(i.e., a list of pyicesl.slicer.Brush
objects). getSlicingPlan
calculates the slicing plan with the currently specified printing parameters w.r.t. tight_box
(i.e., the output of get_processing_and_tight_boxes
).
pyicesl.slicer.FilamentSlicer.set_setting_value( setting, value )
pyicesl.slicer.FilamentSlicer.set_setting_value( setting, field, field_bbox )
Last but not least important, an object FilamentSlicer
is able to change printing parameters via the method set_setting_value
. Argumentsetting
is the string name of the parameter to be changed (see the list of parameters) and argument value
is the parameter's value; can be a Boolean, a Number, a String and a numpy array (in the case of a ratio).
To set a per-layer parameter, the argument value
can be a dictionary where the key is the height and the value is the parameter's value at this height.
Example:
import pyicesl.slicer as slicer
fdmslcr = slicer.FilamentSlicer(slicer.Application())
fdmslcr.set_setting_value('infill_type_0', 'Cubic') # infill type `Cubic` on brush 0
fdmslcr.set_setting_value('infill_percentage_0', 50) # infill percentage 50% on brush 0
fdmslcr.set_setting_value('infill_percentage_0', {0 : 5, 10.0 : 50.0}) # infill percentage per-layer (5% at 0mm height and 50% at 10mm height. Interpolation between the value is expected
To set a parameter's value as a field, the same method with a different signature is used. Its arguments are; setting
(the parameter's name), field
(a numpy array of 64x64x64 dimension) and field_bbox
(the field's action bounding box as explained in the Shape's section).
Example:
import numpy
import pyicesl.modeler as modeler
import pyicesl.slicer as slicer
mySphere = modeler.Sphere(10)
fdmslcr = slicer.FilamentSlicer(slicer.Application())
field_size = 64
field = numpy.fromfunction(
lambda k, j, i: (i+j+k) / (3.0 * (field_size - 1.0),
(field_size, field_size, field_size)
)
fdmslcr.set_setting_value('infill_percentage_0', field, mySphere.bbox)
pyicesl
Examples
Automated Pipeline
import os
import pyicesl.slicer as slicer
import pyicesl.modeler as modeler
brush = slicer.Brush(modeler.sphere(25)) # sphere in brush 0
fdmslcr = slicer.FilamentSlicer(slicer.Application())
# slice with default parameters ('gcodes' folder must exist)
fdmslcr.run([brush], os.path.join(os.getcwd(), 'gcodes', 'slicing_output.gcode'))
slicer.Application.dispose()
Configurable Pipeline
import os
import pyicesl.slicer as slicer
import pyicesl.modeler as modeler
app = slicer.Application()
fdmslcr = slicer.FilamentSlicer(app)
fdmslcr.set_setting_value('printer', 'CR10S_Pro') # select printer
brush = slicer.Brush()
model = modeler.difference(
modeler.cube(size = 40, centered = True),
modeler.sphere(radius = 25))
brush.emit(model)
proc_box, tight_box = fdmslcr.get_processing_and_tight_boxes([brush])
plan = fdmslcr.get_slicing_plan(tight_box)
contours = fdmslcr.slice([brush], plan)
layers = fdmslcr.process(contours, plan)
fdmslcr.save(layers, plan, os.path.join(os.getcwd(), 'gcodes', 'slicing_output.gcode')) # 'gcodes' folder must exist
app.dispose()
Partial Pipeline
import os
import pyicesl.slicer as slicer
fdmslcr = slicer.FilamentSlicer(slicer.Application())
contour = slicer.Contour([[0,0], [0,10], [10, 10], [10, 0], [0,0]], 0, 0.01)
plan = slicer.AdaptiveSlicingPlan([[0,0.2]])
layers = fdmslcr.process([[contour]], plan)
fdmslcr.save(layers, plan, os.path.join(os.getcwd(), 'gcodes', 'slicing_output.gcode'))
slicer.Application().dispose()
import os
import pyicesl.slicer as slicer
fdmslcr = slicer.FilamentSlicer(slicer.Application())
path = slicer.Path([[0,0], [0,10], [10, 10], [10, 0], [0,0]], True, 0.001, 0, slicer.Path.Type.Brim)
plan = slicer.AdaptiveSlicingPlan([[0,0.2]])
layer = ([path], False)
fdmslcr.save([layer], plan, os.path.join(os.getcwd(), 'gcodes', 'slicing_output.gcode'))
slicer.Application().dispose()