Integrations

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 .whl file from Materialise directly.

python -m pip install magicssdkbasepreview10-1.0.2-py3-none-win_amd64.whl

prepare.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 fieldExtensionsLoader
STL, STL_ASCII, STL_BINARY.stl, .STLimportexport.load_from_stl
STEP.step, .stp, .STPcadimport.load_from_step
OBJ.objimportexport.load_from_obj
IGES.igs, .iges, .IGEScadimport.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 typeSDK transformation_constraintEffect
FIXED_ORIENTATIONFIX_ALLPart does not move or rotate
FIXED_LOCATIONFIX_ALLPart is pinned at its coordinates
RANGE_ORIENTATIONFIX_Z_DIRECTIONPart rotates only around Z
FORBIDDEN_ORIENTATIONFIX_BOTTOM_PLANEBottom face is kept down
(none)FREEFull 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 strategySDK solution_mode
MINIMIZE_HEIGHTOPTIMIZE_HEIGHT
MAXIMIZE_DENSITYOPTIMIZE_HEIGHT_AND_SLICE_VOLUME_DISTRIBUTION
MINIMIZE_SUPPORTOPTIMIZE_HEIGHT + run buildprep.optimize_orientation per part first

Error codes

CodeCause
INVALID_DATABASEDatabase handle is invalid or None
INVALID_VERTICESVertices are not Nx3 doubles in C-contiguous order
INVALID_TRIANGLESTriangle indices out of range or not Mx3 uint64s
INVALID_TAGUnique part with the given tag not found in the database
INVALID_PLATFORM_INDEXPlatform index does not exist
INVALID_FILE_PATHFile path does not exist or is not writable
TOO_MANY_TRIANGLESSTEP/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
)

Last updated on