# Scripting in Fusion (/docs/scripting)



`fusionctl exec` and `fusionctl run` send Python to the add-in, which runs it on Fusion's main thread with the full `adsk` API. These notes were checked on Fusion 2705.1.30.

## Globals [#globals]

| Name         | What it is                                                                     |
| ------------ | ------------------------------------------------------------------------------ |
| `app`        | `adsk.core.Application`                                                        |
| `ui`         | `app.userInterface` (do not open dialogs with it, see below)                   |
| `design`     | the active `adsk.fusion.Design`                                                |
| `root`       | `design.rootComponent`                                                         |
| `adsk`       | the `adsk` package (`adsk.core`, `adsk.fusion`, …)                             |
| `mm(v)`      | millimetres to the API's centimetres: `mm(5)` returns `0.5`                    |
| `P(x, y, z)` | `adsk.core.Point3D.create`                                                     |
| `V(v)`       | `adsk.core.ValueInput.createByReal` (API units)                                |
| `VS(s)`      | `adsk.core.ValueInput.createByString`, e.g. `VS("12 mm")` or `VS("width / 2")` |

Anything else a script defines (variables, functions) stays available to the next `exec` or `run` until `fusionctl reset` or `fusionctl new`. The globals above are refreshed on every call, so `design` and `root` always point at the active document.

## Returning values [#returning-values]

`print()` output is captured and printed by the CLI. Set `result` to return a value as JSON:

```sh
fusionctl exec 'result = {"bodies": root.bRepBodies.count, "features": design.timeline.count}'
```

A value that is not JSON-serialisable is returned as its `repr()`. With `--json`, the CLI prints the whole reply, `stdout` and `result` included.

## Units [#units]

The API works in **centimetres** and radians, whatever the document's display units. Writing millimetre numbers into `Point3D` makes the part ten times too large and raises no error.

```python
P(mm(40), 0, 0)          # 40 mm along X
VS("12 mm")              # a length from an expression, stays parametric
VS("width / 2")          # an expression over user parameters
```

Prefer `VS(...)` with an expression for feature sizes, so the feature follows the parameters.

## Sketch planes [#sketch-planes]

For sketches created through the API on the origin planes:

| Plane                 | sketch +X    | sketch +Y    | extrude (+) |
| --------------------- | ------------ | ------------ | ----------- |
| `xYConstructionPlane` | world +X     | world +Y     | +Z          |
| `xZConstructionPlane` | world +X     | **world −Z** | +Y          |
| `yZConstructionPlane` | **world −Z** | world +Y     | +X          |

On XZ, a point at world height `z` is `P(x, -z, 0)` in sketch coordinates. The bracket script keeps that in one helper:

```python
xz = lambda x, z: P(x, -z, 0)  # world (x, z) -> XZ sketch coordinates
```

Sketches on body faces choose their own axes: check `sketch.xDirection` or convert with `sketch.modelToSketchSpace(...)`.

## Orientation [#orientation]

`fusionctl ping` reports `orientation`. On a **Z-up** install the print bed is the XY plane and +Z is up. On a **Y-up** install the floor is XZ. The bracket script assumes Z-up; check before you reuse it.

## Parameters [#parameters]

Create user parameters first and refer to them by name in dimensions and features. Then `fusionctl params --set` changes the part and the timeline regenerates.

```python title="examples/bracket.py"
params = {
    "length": "40 mm",      # base, along X
    "height": "30 mm",      # wall, along Z
    "width": "24 mm",       # along Y
    "thick": "4 mm",
    "hole_d": "4.5 mm",     # M4 clearance, ISO 273 medium
    "bend_r": "3 mm",       # inner fillet: stiffens the bend
    "bed_chamfer": "0.6 mm" # chamfer, not fillet, on edges touching the bed (elephant foot)
}
for name, expr in params.items():
    p = design.userParameters.itemByName(name)
    if p is None:
        design.userParameters.add(name, VS(expr), "mm", "")
```

The `itemByName` check makes the script safe to run twice in the same design.

## Recipes from the bracket [#recipes-from-the-bracket]

### Dimension a sketch to a parameter [#dimension-a-sketch-to-a-parameter]

Place the geometry at its current size, then dimension it and set the dimension's expression:

```python
d = dims.addDistanceDimension(seg.startSketchPoint, seg.endSketchPoint, orient, at)
d.parameter.expression = "length"
```

### Extrude symmetrically by a parameter [#extrude-symmetrically-by-a-parameter]

```python
ext_in = root.features.extrudeFeatures.createInput(
    sk.profiles.item(0), adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
ext_in.setSymmetricExtent(VS("width"), True)
ext = root.features.extrudeFeatures.add(ext_in)
```

### Holes that follow the parameters [#holes-that-follow-the-parameters]

A bare sketch point does not move when parameters change. Dimension it to the sketch origin with an expression:

```python
hs.sketchDimensions.addDistanceDimension(
    hs.originPoint, a, H_, P(x0, -W / 2 - 0.5, 0)
).parameter.expression = "thick + (length - thick) / 2"
```

Two sketch points that share an X cannot both get a horizontal dimension from the origin: Fusion reports `VCS_SKETCH_OVER_CONSTRAINTS` with no visible constraint. Dimension the first point fully, give the second only its Y, then tie the X:

```python
hs.geometricConstraints.addVerticalPoints(a, b)
```

A hole drills against its sketch normal. From a sketch on the XY plane at the bed, that is −Z, away from the part. Drill up through the base with:

```python
h_in.setAllExtent(adsk.fusion.ExtentDirections.NegativeExtentDirection)
```

### Find a face or an edge by geometry [#find-a-face-or-an-edge-by-geometry]

```python
def face_at(normal, pred):
    for f in body.faces:
        ok, n = f.evaluator.getNormalAtPoint(f.pointOnFace)
        if ok and n.isEqualTo(normal) and pred(f.pointOnFace):
            return f
    raise RuntimeError("face not found")

outer = face_at(adsk.core.Vector3D.create(-1, 0, 0), lambda p: abs(p.x) < 1e-6)
```

The script selects the bend edge (a line along Y at x = z = `thick`) and the bed edges (lines at z = 0) the same way, then fillets and chamfers them with `VS("bend_r")` and `VS("bed_chamfer")`. It returns the edge counts in `result`, so a wrong selection shows up as a number.

## Things that break a session [#things-that-break-a-session]

* **Modal dialogs.** `ui.messageBox`, selection prompts and other dialogs block Fusion's main thread, so the bridge cannot answer and the CLI times out. Print instead.
* **Occurrences.** New occurrences are grounded to their parent: setting `occurrence.transform2` is ignored until `occurrence.isGroundToParent = False`. Then call `design.snapshots.add()` if `design.snapshots.hasPendingSnapshot`.
* **3MF export.** The API method is `createC3MFExportOptions`. `fusionctl export part.3mf` handles it.
* **Renamed methods.** Older samples use names that have changed. Check with `fusionctl apidoc ExtrudeFeatures.addSimple` against the installed build instead of guessing.
