A Fusion 360 add-in that fits a cylinder or a plane to a patch of surface you paint with a brush — the 3-matic Mark + Fit workflow, in Fusion.
Fusion can already tell you the radius of a face it knows is a cylinder. This is for the other case: a scan, an STL, a mesh from segmentation, a face that is only roughly cylindrical, or a region spanning several faces. You paint the part of the surface you mean, and it reports the best-fit primitive and how far the real surface deviates from it.
This is the distinction the whole tool turns on. Fusion's display mesh for a plain BRep cylinder has side facets spanning the entire height of the part, so a tool that marks whole triangles takes a 5 mm dab and marks 40 mm of surface — then fits to geometry you never painted.
| drag left mouse | paint |
| hold Shift | flip paint through while held |
| hold Ctrl | erase |
| [ ] | shrink and grow the brush |
Brush diameter is in millimetres of model, not pixels, so it means the same thing at any zoom. Paint through off, a dab marks only the surface facing you — a triangle is taken only if its normal opposes the ray, which is what keeps the brush off the far wall of a tube. On, the dab is a plain sphere and goes straight through.
Live, as you paint: for a cylinder the diameter, axis, a point on it, the painted length and how much of the circumference you covered; for a plane the normal and the patch size. For both, the rms and worst deviation of the painted surface from the fitted primitive — the number that says whether the thing you painted really is a cylinder.
Paint less than 45° around a cylinder and it says so. A narrow strip fits happily and reports a confident radius that the data barely constrains: measured, a 12° arc reported 10.40 mm where the truth was 12.34.
The cylinder diameter field is refilled with the fitted value after every stroke. Type over it to build a nominal size, and the report tells you what rounding costs:
Diameter 24.6420 mm (radius 12.3210) Creating at 24.0000 mm - against that size the painted surface is rms 0.3210, worst 0.3403 mm
Every normal of a cylinder is perpendicular to its axis, so with
M = Σ w n nᵀ the axis is M's
smallest eigenvector. The centred covariance of the normals looks
equivalent, and on perfect data it is — both are exact. The difference is the
eigenvalue gap protecting the axis, and it is large:
| seed | gap for a patch sweeping s radians | 25° strip, 0.6° normal noise |
|---|---|---|
| uncentred | s² / 12 | 0.12° from the true axis |
| centred | s⁴ / 720 | 81.7° from the true axis |
That noise level is less than any real tessellation carries. At 10° of sweep the centred seed returns a random answer, confidently. The tests measure the gap ratio and confirm the predicted 315×.
Centroids of a tessellated cylinder sit inside the true surface, so
fitting them under-reports the radius by r(1 − cos(θ/2)) per
facet — 0.12 mm on a 20-facet 10 mm cylinder, measured.
On a mesh body the samples are exactly on the surface, because the triangles are the surface — no approximation at all, which is the case that matters for scan work. On a BRep body they lie on the display tessellation, so a radius reads about 0.15% small; that shows up honestly in the reported deviation.
A brush has to keep up with the mouse. Three costs stood in the way, all measured by replaying a 200-dab drag through the exact path the mouse takes:
| per mouse event | before | after |
|---|---|---|
| paint (ray, hit, sampling) | 1.0 ms | 1.0 ms |
| drawing the painted quads | 5.3 ms | 0.25 ms |
| brush ring | 4.9 ms | 0.14 ms |
| total | 13.1 ms | 1.84 ms |
The geometry was never the problem. viewport.refresh() costs
3.5 ms against 0.16 ms for a transform write, and
creating any custom graphics entity costs ~5 ms regardless of size.
So the ring is built once and moved by a matrix, quads are batched, nothing forces a
redraw per event, and the fit runs once when the button lifts instead of on every dab.
A construction axis or plane, a body, both, or nothing. The two commands default differently and remember separately: Fit Cylinder makes the axis and a translucent cylinder, because one sitting on the surface it came from is how you judge the fit at a glance; Fit Plane makes the plane only, because a surface patch just buries it.
Everything is named Fit_Cylinder_<body> or
Fit_Plane_<body> after the body it was fitted to, and created
bodies are excluded from painting — a fitted cylinder encloses its own source, so
otherwise the next stroke lands on the tool's own output.
Asking for construction geometry creates construction geometry
and nothing else. The API reference calls setByPlane and
setByLine direct-modelling only, and they do fail in a parametric design
— but they are accepted inside a BaseFeature edit, which is a
direct-modelling context. Measured, not assumed. So no helper body is ever created.
git clone <this repo> powershell -File install.ps1
Then in Fusion: Utilities → ADD-INS → Scripts and Add-Ins → Add-Ins → Fit Primitives → Run. Two buttons appear in Inspect. No dependencies — it runs on Fusion's bundled Python, stdlib only.
The geometry is pure stdlib with no adsk import, so it is proved
outside Fusion: py -3.10 tests/test_fit.py, 57 checks
against synthetic patches with a known answer. Fusion has no headless mode, so the
rest is proved inside a running Fusion through a folder-watching dev bridge:
py -3.10 tools/livetest.py builds a scratch document of known geometry,
aims the camera, paints through the real unproject → ray → hit → brush path and
checks the fitted numbers against the truth — 39 checks.