Magics SDK
Load parts from the Phasio production backlog into Magics with the correct quantities and orientation constraints applied automatically.
Parts in the Phasio production backlog can be exported as a .camspec file — an archive containing the CAD files and all the manufacturing constraints attached to them. The Magics SDK reads that file and writes a .matamx with the correct quantities and orientations already applied, ready to open in Magics.
💡 The SDK is a Python package from Materialise. You need Python 3 on a Windows (64-bit) machine, a valid Materialise SDK licence, and the
.whlfile from Materialise directly.
python -m pip install magicssdkbasepreview10-1.0.2-py3-none-win_amd64.whlprepare.py
Reads a .camspec file, loads every part with the correct orientation applied, and writes a .matamx ready to open in Magics. No nesting is run — the operator opens the file in Magics, adjusts sinter boxes or supports as needed, and nests from there. Parts inside the CAMSPEC can be STL, STEP, IGES, or OBJ.
# prepare.py
# Usage: python prepare.py order.camspec output.matamx
import sys, zipfile, json, os, tempfile, logging
import numpy as np
from magicssdkbase10 import importexport, cadimport, matamxdb
logging.basicConfig(
format="%(asctime)-15s %(name)-10s %(levelname)-8s %(message)s",
level=logging.INFO,
)
# Map CAMSPEC format strings to temp-file extensions.
# "STL" covers both ASCII and binary — the SDK detects encoding automatically.
SUFFIXES = {
"STL": ".stl", "STL_ASCII": ".stl", "STL_BINARY": ".stl",
"OBJ": ".obj", "STEP": ".step", "IGES": ".igs",
}
def load_mesh(data: bytes, fmt: str):
"""Write raw bytes to a temp file and load into the SDK."""
suffix = SUFFIXES.get(fmt)
if suffix is None:
raise ValueError(f"Unsupported CAMSPEC format: {fmt}")
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
f.write(data)
tmp = f.name
try:
if fmt in ("STL", "STL_ASCII", "STL_BINARY"):
r = importexport.load_from_stl(tmp)
elif fmt == "OBJ":
r = importexport.load_from_obj(tmp)
elif fmt == "STEP":
r = cadimport.load_from_step(tmp, import_as_one_part=True)
elif fmt == "IGES":
r = cadimport.load_from_iges(tmp, import_as_one_part=True)
finally:
os.unlink(tmp)
return np.asarray(r.vertices, dtype=np.float64), np.asarray(r.triangles)
def apply_transform(vertices, ref):
"""Apply a CAMSPEC referenceTransform (flat 9-element rotation + 3-element
translation) to an Nx3 vertex array using plain numpy."""
rot = np.array(ref["rotation"], dtype=np.float64).reshape(3, 3)
trans = np.array(ref["translation"], dtype=np.float64)
return (vertices @ rot.T) + trans
def prepare(camspec_path: str, output_path: str, container=(200.0, 200.0, 300.0)):
db = matamxdb.MatAMX()
# Create the build platform — adjust dimensions to match your machine.
platform = matamxdb.add_platform(
db,
container_dimensions=container,
container_origin=(0.0, 0.0, 0.0),
container_shape="BOX",
)
with zipfile.ZipFile(camspec_path) as zf:
manifest = json.loads(zf.read("camspec.json"))
for entry in manifest["manifest"]:
fmt = entry.get("format", "STL")
data = zf.read(f"models/{entry['file']}")
vertices, triangles = load_mesh(data, fmt)
# Apply the CAMSPEC reference orientation if present
rt = entry.get("referenceTransform")
if rt:
vertices = apply_transform(vertices, rt)
# Register the geometry once as a unique part
unique = matamxdb.add_unique_part(db, vertices, triangles,
part_name=entry.get("name", entry["file"]))
# Place one instance per quantity on the platform
qty = entry.get("quantity", 1)
for i in range(qty):
matamxdb.add_part_instance_from_unique_part(
db,
tag=unique.tag,
transformation=np.eye(4),
platform_index=platform.platform_index,
part_name=f"{entry.get('name', entry['file'])} [{i + 1}/{qty}]",
)
matamxdb.save_to_file(output_path, db)
logging.info("Wrote %s — open in Magics to review before nesting", output_path)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python prepare.py <input.camspec> <output.matamx>")
sys.exit(1)
prepare(sys.argv[1], sys.argv[2])Reference
nest.py
To drive nesting programmatically after the operator has reviewed the .matamx, use nest.py. You can also chain prepare() and nest() back to back to skip the review step entirely.
# nest.py
# Usage: python nest.py order.camspec prepared.matamx nested.matamx
import sys, zipfile, json, time, logging
import numpy as np
from magicssdkbase10 import buildprep, matamxdb
logging.basicConfig(
format="%(asctime)-15s %(name)-10s %(levelname)-8s %(message)s",
level=logging.INFO,
)
CONSTRAINT_MAP = {
"FIXED_ORIENTATION": "FIX_ALL",
"FIXED_LOCATION": "FIX_ALL",
"RANGE_ORIENTATION": "FIX_Z_DIRECTION",
"FORBIDDEN_ORIENTATION": "FIX_BOTTOM_PLANE",
}
STRATEGY_MAP = {
"MINIMIZE_HEIGHT": "OPTIMIZE_HEIGHT",
"MAXIMIZE_DENSITY": "OPTIMIZE_HEIGHT_AND_SLICE_VOLUME_DISTRIBUTION",
"MINIMIZE_SUPPORT": "OPTIMIZE_HEIGHT",
}
def nest(camspec_path: str, prepared_path: str, output_path: str,
container=(200.0, 200.0, 300.0), timeout_s=120):
db = matamxdb.MatAMX()
matamxdb.load_from_file(prepared_path, db)
info = matamxdb.load_info(db)
verts, tris = [], []
for idx in range(info.number_of_part_instances):
pi = matamxdb.load_part_instance(
db,
platform_index=info.part_instance_platform_indices[idx],
part_instance_index=info.part_instance_indices[idx],
)
verts.append(np.asarray(pi.vertices, dtype=np.float64))
tris.append(np.asarray(pi.triangles))
with zipfile.ZipFile(camspec_path) as zf:
manifest = json.loads(zf.read("camspec.json"))
id_to_constraint = {
c["manifestId"]: CONSTRAINT_MAP.get(c["type"], "FREE")
for c in manifest.get("constraints", [])
}
# Expand constraints by quantity to match the instance order from prepare.py
t_cons = []
for entry in manifest["manifest"]:
for _ in range(entry.get("quantity", 1)):
t_cons.append(id_to_constraint.get(entry["id"], "FREE"))
min_dist = next(
(float(gc["value"]["distance"])
for gc in manifest.get("globalConstraints", [])
if gc["type"] == "MIN_PART_DISTANCE"),
2.0,
)
opt = manifest.get("optimization") or {}
mode = STRATEGY_MAP.get(opt.get("strategy", "MINIMIZE_HEIGHT"), "OPTIMIZE_HEIGHT")
try:
result = buildprep.nest_3d(
list_of_vertices=verts,
list_of_triangles=tris,
transformation_constraints=t_cons,
minimal_distance_between_parts=min_dist,
accuracy=min_dist / 2,
container_dimensions=container,
solution_mode=mode,
number_of_iterations=1,
)
nester = result.nester
t0 = time.time()
while not all(result.parts_processed) and (time.time() - t0) < timeout_s:
result = buildprep.continue_nest_3d(nester, number_of_iterations=20)
except RuntimeError as e:
raise RuntimeError(f"Nesting failed: {e}") from e
failed = sum(result.parts_nesting_failed)
if failed:
logging.warning("%d part(s) could not be nested — check container dimensions", failed)
# Write nested positions back — re-save as a new file
# (For a full implementation, update each part instance's transformation
# in the database before saving. The exact API depends on how the operator
# workflow feeds back into Magics.)
matamxdb.save_to_file(output_path, db)
logging.info(
"Nested %d/%d parts — saved to %s",
info.number_of_part_instances - failed,
info.number_of_part_instances,
output_path,
)
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: python nest.py <order.camspec> <prepared.matamx> <nested.matamx>")
sys.exit(1)
nest(sys.argv[1], sys.argv[2], sys.argv[3])What is a CAMSPEC file?
A .camspec file is a ZIP archive produced by Phasio when exporting jobs from the production backlog. It contains the CAD files and a camspec.json manifest describing every part — quantities, units, orientation constraints, spacing rules, and a build optimisation goal.
The format is open (MIT licence) and fully documented at camspec.org.
Supported file formats
CAMSPEC can carry STL, STEP, OBJ, and IGES geometry. The SDK routes each format to the right loader automatically based on the format field in the manifest — not the file extension — so .STP, .stp, .step all work without any extra configuration.
| Format field | Extensions | Loader |
|---|---|---|
STL, STL_ASCII, STL_BINARY | .stl, .STL | importexport.load_from_stl |
STEP | .step, .stp, .STP | cadimport.load_from_step |
OBJ | .obj | importexport.load_from_obj |
IGES | .igs, .iges, .IGES | cadimport.load_from_iges |
STEP and IGES tessellation
STEP and IGES files are tessellated on import. The default surface_accuracy_mm=0.01 is fine for most parts but can be slow for large assemblies. Raise it to reduce triangle count:
r = cadimport.load_from_step(
tmp_path,
surface_accuracy_mm=0.05, # coarser — faster, less memory
import_as_one_part=True, # merge multi-body assemblies into one mesh
stitch_automatic=True,
)Use import_as_one_part=False only if you need to apply different constraints to individual bodies within a single STEP file.
CAMSPEC constraint mapping
| CAMSPEC type | SDK transformation_constraint | Effect |
|---|---|---|
FIXED_ORIENTATION | FIX_ALL | Part does not move or rotate |
FIXED_LOCATION | FIX_ALL | Part is pinned at its coordinates |
RANGE_ORIENTATION | FIX_Z_DIRECTION | Part rotates only around Z |
FORBIDDEN_ORIENTATION | FIX_BOTTOM_PLANE | Bottom face is kept down |
| (none) | FREE | Full 6-DOF freedom |
Global MIN_PART_DISTANCE maps to minimal_distance_between_parts. For VOLUME_BASED_SPACING, use smallParts.minSpacing as a conservative default.
Optimisation strategy mapping
| CAMSPEC strategy | SDK solution_mode |
|---|---|
MINIMIZE_HEIGHT | OPTIMIZE_HEIGHT |
MAXIMIZE_DENSITY | OPTIMIZE_HEIGHT_AND_SLICE_VOLUME_DISTRIBUTION |
MINIMIZE_SUPPORT | OPTIMIZE_HEIGHT + run buildprep.optimize_orientation per part first |
Error codes
| Code | Cause |
|---|---|
INVALID_DATABASE | Database handle is invalid or None |
INVALID_VERTICES | Vertices are not Nx3 doubles in C-contiguous order |
INVALID_TRIANGLES | Triangle indices out of range or not Mx3 uint64s |
INVALID_TAG | Unique part with the given tag not found in the database |
INVALID_PLATFORM_INDEX | Platform index does not exist |
INVALID_FILE_PATH | File path does not exist or is not writable |
TOO_MANY_TRIANGLES | STEP/IGES import exceeded max_number_of_triangles — raise surface_accuracy_mm |
Logging
The SDK writes to Python's standard logging framework:
import logging
logging.basicConfig(
format="%(asctime)-15s %(name)-10s %(levelname)-8s %(message)s",
level=logging.INFO, # use DEBUG for per-iteration nesting progress
)Related
Last updated on