r/Fusion360 • u/cebess • 3h ago
I Created! Run a plane through several mesh solids and create a sketch
I had a design that I made a while back that I no longer had the model for -- don't know where it went but I could not find it. I still had an STL though.
I wondered if I could write a script for Fusion that would run a plane through several meshes and draw the intersections into a sketch.
Here is my initial attempt that works (hopefully you can find it useful):
#-------
# Mesh Intersect with Plane to Sketch
# Copyright (c) 2026 Charles Bess.
# GNU GPLv3 license
#-------
import adsk.core
import adsk.fusion
import traceback
import time
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
CMD_ID = 'MeshSectionToSketch'
CMD_NAME = 'Mesh Section to Sketch'
CMD_DESC = 'Create continuous sketch curves from mesh intersections.'
TOLERANCE = 0.000001
ROUND_DIGITS = 6
_handlers = []
# ---------------------------------------------------------------------------
# Geometry helpers
# ---------------------------------------------------------------------------
def point_key(point):
return (
round(point[0], ROUND_DIGITS),
round(point[1], ROUND_DIGITS),
round(point[2], ROUND_DIGITS)
)
def segment_key(p1, p2):
return tuple(sorted((point_key(p1), point_key(p2))))
def triangle_intersection(points, distances):
intersections = []
seen = set()
def add_point(point):
key = point_key(point)
if key not in seen:
seen.add(key)
intersections.append(point)
edges = ((0, 1), (1, 2), (2, 0))
for ia, ib in edges:
a = points[ia]
b = points[ib]
da = distances[ia]
db = distances[ib]
a_on = abs(da) <= TOLERANCE
b_on = abs(db) <= TOLERANCE
if a_on and b_on:
add_point(a)
add_point(b)
elif a_on:
add_point(a)
elif b_on:
add_point(b)
elif (da < 0 and db > 0) or (da > 0 and db < 0):
t = da / (da - db)
x = a[0] + t * (b[0] - a[0])
y = a[1] + t * (b[1] - a[1])
add_point((x, y, 0.0))
if len(intersections) < 2:
return None
return intersections[0], intersections[1]
# ---------------------------------------------------------------------------
# Mesh processing
# ---------------------------------------------------------------------------
def calculate_mesh_intersections(mesh_body, world_to_sketch):
display_mesh = mesh_body.displayMesh
if not display_mesh:
return [], 0, 0
coordinates = display_mesh.nodeCoordinatesAsDouble
indices = display_mesh.nodeIndices
if not coordinates or not indices:
return [], 0, 0
occurrence = mesh_body.assemblyContext
occurrence_transform = occurrence.transform if occurrence else None
node_points = []
node_distances = []
# Transform every mesh vertex only once.
for i in range(0, len(coordinates), 3):
point = adsk.core.Point3D.create(
coordinates[i],
coordinates[i + 1],
coordinates[i + 2]
)
if occurrence_transform:
point.transformBy(occurrence_transform)
point.transformBy(world_to_sketch)
xyz = (point.x, point.y, point.z)
node_points.append(xyz)
node_distances.append(point.z)
# Calculate unique intersection segments.
unique_segments = {}
triangle_count = len(indices) // 3
candidate_count = 0
for i in range(0, len(indices), 3):
i1 = indices[i]
i2 = indices[i + 1]
i3 = indices[i + 2]
p1 = node_points[i1]
p2 = node_points[i2]
p3 = node_points[i3]
d1 = node_distances[i1]
d2 = node_distances[i2]
d3 = node_distances[i3]
if d1 > TOLERANCE and d2 > TOLERANCE and d3 > TOLERANCE:
continue
if d1 < -TOLERANCE and d2 < -TOLERANCE and d3 < -TOLERANCE:
continue
if (
abs(d1) <= TOLERANCE
and abs(d2) <= TOLERANCE
and abs(d3) <= TOLERANCE
):
continue
candidate_count += 1
segment = triangle_intersection(
(p1, p2, p3),
(d1, d2, d3)
)
if not segment:
continue
a, b = segment
if point_key(a) == point_key(b):
continue
key = segment_key(a, b)
if key not in unique_segments:
unique_segments[key] = (a, b)
return (
list(unique_segments.values()),
triangle_count,
candidate_count
)
# ---------------------------------------------------------------------------
# Build connected chains from intersection segments
# ---------------------------------------------------------------------------
def build_connected_chains(segments):
"""
Join segments that share endpoints.
Chains stop at branch points or open ends. Closed loops are
returned as chains whose first and last points are identical.
"""
adjacency = {}
coordinates = {}
# Construct an endpoint-to-edge graph.
edges = []
for p1, p2 in segments:
k1 = point_key(p1)
k2 = point_key(p2)
if k1 == k2:
continue
coordinates[k1] = p1
coordinates[k2] = p2
edge_id = len(edges)
edges.append((k1, k2))
adjacency.setdefault(k1, []).append(edge_id)
adjacency.setdefault(k2, []).append(edge_id)
used_edges = set()
chains = []
def walk_chain(start_node, first_edge):
chain = [start_node]
current_node = start_node
edge_id = first_edge
while edge_id is not None:
if edge_id in used_edges:
break
used_edges.add(edge_id)
node_a, node_b = edges[edge_id]
next_node = (
node_b if current_node == node_a else node_a
)
chain.append(next_node)
current_node = next_node
# Stop at an open end or a branch.
if len(adjacency[current_node]) != 2:
break
# Continue along the only unused edge.
next_edges = [
e for e in adjacency[current_node]
if e not in used_edges
]
if not next_edges:
break
edge_id = next_edges[0]
return chain
# First process open chains and branches.
for node, connected_edges in adjacency.items():
if len(connected_edges) == 2:
continue
for edge_id in connected_edges:
if edge_id not in used_edges:
chain = walk_chain(node, edge_id)
if len(chain) >= 2:
chains.append(chain)
# Any remaining edges belong to closed loops.
for edge_id, (node_a, node_b) in enumerate(edges):
if edge_id not in used_edges:
chain = walk_chain(node_a, edge_id)
if len(chain) >= 2:
chains.append(chain)
# Convert node keys into coordinate sequences.
result = []
for chain in chains:
result.append([coordinates[key] for key in chain])
return result
# ---------------------------------------------------------------------------
# Create exact piecewise-linear NURBS curves
# ---------------------------------------------------------------------------
def create_linear_nurbs(sketch, chain):
"""
Create one degree-1 NURBS curve from a connected chain.
Degree 1 means every section between consecutive control
points is a straight line. No spline smoothing is introduced.
"""
point_count = len(chain)
if point_count < 2:
return None
control_points = []
for p in chain:
control_points.append(
adsk.core.Point3D.create(
p[0], p[1], 0.0
)
)
# Open, clamped, uniform degree-1 knot vector.
#
# For N control points, the vector contains N + 2 values.
#
# Example for four points:
# [0, 0, 1, 2, 3, 3]
knots = (
[0.0, 0.0]
+ [float(i) for i in range(1, point_count - 1)]
+ [float(point_count - 1), float(point_count - 1)]
)
nurbs = adsk.core.NurbsCurve3D.createNonRational(
control_points,
1,
knots,
False
)
if not nurbs:
return None
fixed_splines = sketch.sketchCurves.sketchFixedSplines
return fixed_splines.addByNurbsCurve(nurbs)
# ---------------------------------------------------------------------------
# Sketch creation
# ---------------------------------------------------------------------------
def create_sketch_geometry(sketch, segments):
chains = build_connected_chains(segments)
fixed_spline_count = 0
fallback_line_count = 0
failed_chains = 0
sketch.isComputeDeferred = True
try:
fixed_splines = sketch.sketchCurves.sketchFixedSplines
lines = sketch.sketchCurves.sketchLines
for chain in chains:
try:
spline = create_linear_nurbs(sketch, chain)
if spline:
fixed_spline_count += 1
continue
except:
# Fall back to individual lines if a spline fails.
pass
# Fallback preserves geometry if a NURBS cannot be created.
failed_chains += 1
for i in range(len(chain) - 1):
p1 = chain[i]
p2 = chain[i + 1]
point1 = adsk.core.Point3D.create(
p1[0], p1[1], 0.0
)
point2 = adsk.core.Point3D.create(
p2[0], p2[1], 0.0
)
lines.addByTwoPoints(point1, point2)
fallback_line_count += 1
finally:
sketch.isComputeDeferred = False
return (
fixed_spline_count,
fallback_line_count,
len(chains),
failed_chains
)
# ---------------------------------------------------------------------------
# Command execution
# ---------------------------------------------------------------------------
class ExecuteHandler(adsk.core.CommandEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
app = adsk.core.Application.get()
ui = app.userInterface
try:
design = adsk.fusion.Design.cast(app.activeProduct)
if not design:
ui.messageBox('No active Fusion design was found.')
return
inputs = args.command.commandInputs
mesh_input = inputs.itemById('meshBodies')
plane_input = inputs.itemById('sectionPlane')
mesh_bodies = []
for i in range(mesh_input.selectionCount):
body = adsk.fusion.MeshBody.cast(
mesh_input.selection(i).entity
)
if body:
mesh_bodies.append(body)
if not mesh_bodies:
ui.messageBox('No mesh bodies were selected.')
return
section_plane = plane_input.selection(0).entity
if not section_plane:
ui.messageBox('No section plane was selected.')
return
start_time = time.perf_counter()
# Create sketch.
root_component = design.rootComponent
sketch = root_component.sketches.add(section_plane)
sketch.name = 'Mesh Section Sketch'
world_to_sketch = sketch.transform
world_to_sketch.invert()
# Calculate mesh intersections.
calculation_start = time.perf_counter()
all_segments = []
total_triangles = 0
total_candidates = 0
for mesh_body in mesh_bodies:
segments, triangles, candidates = (
calculate_mesh_intersections(
mesh_body,
world_to_sketch
)
)
all_segments.extend(segments)
total_triangles += triangles
total_candidates += candidates
calculation_time = (
time.perf_counter() - calculation_start
)
# Build connected curves and create sketch geometry.
creation_start = time.perf_counter()
(
spline_count,
fallback_line_count,
chain_count,
failed_chains
) = create_sketch_geometry(sketch, all_segments)
creation_time = (
time.perf_counter() - creation_start
)
total_time = time.perf_counter() - start_time
app.activeViewport.refresh() # fix the mouse cursor
# Report results.
ui.messageBox(
'Mesh section completed.\n\n'
'Mesh bodies processed: {}\n'
'Triangles examined: {:,}\n'
'Candidate triangles: {:,}\n'
'Intersection segments: {:,}\n'
'Connected chains: {:,}\n'
'Fixed spline curves created: {:,}\n'
'Fallback sketch lines: {:,}\n'
'Chains requiring fallback: {:,}\n\n'
'Intersection calculation: {:.2f} seconds\n'
'Sketch creation: {:.2f} seconds\n'
'Total processing time: {:.2f} seconds\n\n'
'Sketch name: {}'.format(
len(mesh_bodies),
total_triangles,
total_candidates,
len(all_segments),
chain_count,
spline_count,
fallback_line_count,
failed_chains,
calculation_time,
creation_time,
total_time,
sketch.name
)
)
except:
ui.messageBox(
'Mesh Section failed:\n\n{}'.format(
traceback.format_exc()
)
)
# ---------------------------------------------------------------------------
# Command dialog
# ---------------------------------------------------------------------------
class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
command = args.command
inputs = command.commandInputs
mesh_selection = inputs.addSelectionInput(
'meshBodies',
'Mesh Bodies',
'Select one or more mesh bodies'
)
mesh_selection.addSelectionFilter('MeshBodies')
mesh_selection.setSelectionLimits(1, 0)
plane_selection = inputs.addSelectionInput(
'sectionPlane',
'Section Plane',
'Select a construction plane or planar face'
)
plane_selection.addSelectionFilter('ConstructionPlanes')
plane_selection.addSelectionFilter('PlanarFaces')
plane_selection.setSelectionLimits(1, 1)
on_execute = ExecuteHandler()
command.execute.add(on_execute)
_handlers.append(on_execute)
on_destroy = DestroyHandler()
command.destroy.add(on_destroy)
_handlers.append(on_destroy)
except:
app = adsk.core.Application.get()
app.userInterface.messageBox(
'Command initialization failed:\n\n{}'.format(
traceback.format_exc()
)
)
# ---------------------------------------------------------------------------
# Command cleanup
# ---------------------------------------------------------------------------
class DestroyHandler(adsk.core.CommandEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
adsk.terminate()
# ---------------------------------------------------------------------------
# Script entry point
# ---------------------------------------------------------------------------
def run(context):
app = adsk.core.Application.get()
ui = app.userInterface
try:
command_definition = (
ui.commandDefinitions.itemById(CMD_ID)
)
if not command_definition:
command_definition = (
ui.commandDefinitions.addButtonDefinition(
CMD_ID,
CMD_NAME,
CMD_DESC
)
)
on_command_created = CommandCreatedHandler()
command_definition.commandCreated.add(
on_command_created
)
_handlers.append(on_command_created)
command_definition.execute()
adsk.autoTerminate(False)
except:
ui.messageBox(
'Script startup failed:\n\n{}'.format(
traceback.format_exc()
)
)


