Multi-Material and Multi-Part Support in AnchorSCAD
April 20, 2025 ยท View on GitHub
Introduction
The current development version of OpenSCAD has minimal support for generating multiple models
from a single .scad file. This is called "Lazy Union". The implicit union for all top level shapes
are rendered as separate models into 3mf files. AnchorSCAD now uses this experimental openscad
"lazy union" feature together with new Part, Material, and MaterialMap attributes to create Lazy Union enabled files.
This allows a single AnchorSCAD model to define multiple distinct objects, potentially with different materials, which can be useful for multi-material printing, defining support structures, or creating complex assemblies.
(Note that the lazy-union feature needs to be enabled in the Preferences->Features menu or the '--enable lazy-union' CLI option.)
Part, Material, and MaterialMap Classes
AnchorSCAD uses three core classes to manage multi-part and multi-material rendering:
anchorscad.Part: Defines a distinct part of the model. It has anameand apriority. Parts allow grouping related shapes. By default, shapes belong to theDEFAULT_PART.anchorscad.Material: Defines a material assigned to a shape. It includes aname,priority, and akind(typicallyPHYSICAL_MATERIAL_KINDorNON_PHYSICAL_MATERIAL_KIND). By default, shapes useDEFAULT_EXAMPLE_MATERIAL.anchorscad.MaterialMap: Allows remapping materials for shapes within a specific context, useful for reusing designs with different material assignments.
Internally, the renderer uses a PartMaterial combination to track each geometric entity.
Priority and Volume Conflict Resolution ("Curing")
When different parts or materials overlap in space, AnchorSCAD resolves conflicts using priorities. The resolution happens based on the combined PartMaterial priority:
- Part Priority First: Parts with higher priority take precedence over parts with lower priority.
- Material Priority Second: Within the same Part priority, materials with higher priority take precedence over materials with lower priority.
During rendering, if a PartMaterial combination (A) has a higher priority than another PartMaterial combination (B), and B's material kind is PHYSICAL_MATERIAL_KIND, the volume of A will be subtracted from the volume of B using the OpenSCAD difference() operation. This prevents overlapping volumes for distinct physical parts/materials.
Physical vs. Non-Physical Materials
Materials with kind=NON_PHYSICAL_MATERIAL_KIND (like the default COORDINATES_MATERIAL used for anchors) are excluded from the volume subtraction process. They can overlap with physical parts and are typically used for non-printable elements like:
- Visual aids (e.g., example anchors)
- Slicer modifiers (e.g., support blockers/enforcers, variable infill regions)
These non-physical parts are often rendered as separate objects in the final output file (e.g., 3MF).
Default Parts and Materials
All example shapes are generated with the part named "default" (priority 5.0) and the material named "default" (priority 5.0). Example anchors are rendered with the part "default" and the material named "anchor" (which is non-physical).
Rendering Multi-Part/Multi-Material Models
Parts and Materials can be assigned to a model using the .part() and .material() attribute functions, respectively. The model below is a simple "sphere on box". The box and sphere are assigned different materials, and both belong to the default part.
Notably, the box material is given a higher priority (10) than the sphere material (9). Since both belong to the same default part (priority 5), the material priority dictates the conflict resolution. The box volume will be subtracted from the sphere volume where they overlap.
The EXAMPLE_ANCHOR is rendered using the "anchor" material and hence will have both the box and sphere removed.
@ad.shape
@ad.datatree
class MultiMaterialTest(ad.CompositeShape):
'''
A basic test of multi-material support. Basically a box with a sphere on top.
The box and sphere are different materials.
'''
xy: float=30
z: float=10
size: tuple=ad.dtfield(
doc='The (x,y,z) size of ShapeName',
self_default=lambda s: (s.xy, s.xy, s.z))
# A node that builds a box from the size tuple. See:
# https://github.com/owebeeone/anchorscad/blob/master/docs/datatrees_docs.md
box_node: ad.Node=ad.dtfield(ad.ShapeNode(ad.Box, 'size'))
sphere_r: float=ad.dtfield(self_default=lambda s: s.xy/2)
shpere_node: ad.Node=ad.dtfield(ad.ShapeNode(ad.Sphere, prefix='sphere_'))
EXAMPLE_SHAPE_ARGS=ad.args(xy=20, z=10)
EXAMPLE_ANCHORS=(
ad.surface_args('sphere', 'top'),)
def build(self) -> ad.Maker:
box_shape = self.box_node()
maker = box_shape.solid('box') \
.material(ad.Material('box', priority=10)) \
.at('face_centre', 'base', post=ad.ROTX_180)
maker.add_at(
self.shpere_node()
.solid('sphere')
.material(ad.Material('sphere', priority=9))
.at('top', rh=1.4),
'face_centre', 'top')
return maker
The following OpenSCAD code is generated from the MultiMaterialTest example. AnchorSCAD's PartMarterialResolver processes the shapes based on their assigned parts and materials, applying priority rules.
Key points about the generated code:
- Modular Structure: The code defines separate
moduleblocks for each unique combination ofPartMaterialencountered. The module names follow the pattern<part>_<part_priority>_<material>_<material_priority>(with potentially_curedor_non_physicalsuffixes), making it easier to identify the origin of each geometric piece. - Curing: The
default_5_sphere_9_curedmodule demonstrates the conflict resolution. Because theboxmaterial (priority 10) is higher than thespherematerial (priority 9) within the samedefaultpart (priority 5), thedefault_5_box_10module's geometry is subtracted from thedefault_5_sphere_9module's geometry usingdifference(). - Non-Physical Parts: The
default_5_non_physical_20_non_physical_non_physicalmodule corresponds to the defaultEXAMPLE_ANCHORspecified in the Python code, which uses the non-physicalCOORDINATES_MATERIAL(anchor, priority 20). It appears as a separate entity. - Lazy Union: The top-level
lazy_unionsection simply calls the relevant modules. This structure, combined with OpenSCAD's experimental feature, allows generating a 3MF file containing multiple, individually addressable models (the box, the cured sphere, and the non-physical anchor).
// Start: lazy_union
default_5_box_10();
default_5_sphere_9_cured();
default_5_non_physical_20_non_physical_non_physical();
// End: lazy_union
// Modules.
// 'PartMaterial undef-default - box 10'
module default_5_box_10() {
// 'None : _combine_solids_and_holes'
union() {
// 'default : _combine_solids_and_holes'
union() {
// 'box'
multmatrix(m=[[1.0, 0.0, 0.0, -10.0], [0.0, 1.0, 0.0, -10.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]) {
// 'box : _combine_solids_and_holes'
union() {
// 'box'
cube(size=[20.0, 20.0, 10.0]);
}
}
}
}
} // end module default_5_box_10
// 'PartMaterial undef-default - non_physical 20 non-physical'
module default_5_non_physical_20_non_physical_non_physical() {
// 'None : _combine_solids_and_holes'
union() {
// 'default : _combine_solids_and_holes'
union() {
// 'sphere_non_physical'
multmatrix(m=[[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 19.0], [0.0, 0.0, 0.0, 1.0]]) {
// 'sphere_non_physical : _combine_solids_and_holes'
union() {
// 'sphere_non_physical'
sphere(r=10.0);
}
}
}
}
} // end module default_5_non_physical_20_non_physical_non_physical
// 'PartMaterial undef-default - sphere 9'
module default_5_sphere_9() {
// 'None : _combine_solids_and_holes'
union() {
// 'default : _combine_solids_and_holes'
union() {
// 'sphere'
multmatrix(m=[[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 14.0], [0.0, 0.0, 0.0, 1.0]]) {
// 'sphere : _combine_solids_and_holes'
union() {
// 'sphere'
sphere(r=10.0);
}
}
}
}
} // end module default_5_sphere_9
// 'PartMaterial undef-default - sphere 9'
module default_5_sphere_9_cured() {
difference() {
default_5_sphere_9();
default_5_box_10();
}
} // end module default_5_sphere_9_cured
Physical vs. Combined Output
AnchorSCAD can also generate output containing only the physical parts. This is useful if the non-physical elements (like anchors or slicer modifiers) are not desired in the final geometry file sent to the slicer.
The "physical" output file's lazy_union section would omit calls to modules generated from non-physical materials. Note the absence of the default_5_non_physical_20_non_physical_non_physical() call below compared to the full output shown previously:
// Start: lazy_union
default_5_box_10();
default_5_sphere_9_cured();
// End: lazy_union
... rest of definitions
Slicer Support
Both Prusa and Orca slicers will import geometry from an OpenSCAD generated 3mf file, however to ensure the Z axis is not broken when loading the file you must respond "Yes" to the dialog that says:
This file contains several objects positioned at multiple heights.
Instead of considering them as multiple objects, should
the file be loaded as a single object having multiple parts?
Sadly, this limits the ability to turn off and on various parts of the model. For example, example-anchors should in theory not be sliced as they're just for a visual cues however the "part type" setting can be used to cause it not to be rendered in the final gcode.
Also, Orca's method for selecting a part is somewhat unintuitive compared to Prusa slicer. Some more serious UI work needs to be done here.
Here is a screenshot of Orca showing the sliced test model.

Future
If OpenSCAD keeps the experimental lazy-union support into the next release, I think this level of part and material support is sufficient for AnchorSCAD to be useful.
I want to automate the workflow even more. AnchorSCAD has a generic 3mf file reader/writer that extends the datatrees/dataclasses module into xdatatrees. The goal is to make it simpler to generate 3mf project files directly. I'm hoping to one day render directly to printer without having to fire up the GUI for the slicer. In particular, filament to material mapping somewhat difficult since material names are lost using the lazy-union feature, though part information might help mitigate this.