I have a problem I have been trying to find a solution to for a while now. Is there a way to calculate the silhouette of a mesh body? Fusion is able to calculate the silhouette in the CAM environment, I am just wondering if their is a method to get a mesh silhouette as a sketch.
Edit: I have tried many things, but I think what I need to calculate is the concave hull. I have gotten this far but I am struggling with how to calculate the point with the greatest angle.
def getConcaveHull():
design:adsk.fusion.Design = app.activeProduct
root = design.rootComponent
newSketch:adsk.fusion.Sketch = root.sketches.add(root.xYConstructionPlane)
closestNPoints = 20
mesh = root.meshBodies.item(0)
nodes = mesh.displayMesh.nodeCoordinates
yMin = None
yIndex = None
pList = []
for i, node in enumerate(nodes):
pList.append([node.x,node.y,node.z])
if yMin == None or yMin.y > node.y:
yMin = node
yIndex = i
#remove first point from list
pList.pop(yIndex)
#set current point to the yMin point
currentPoint = adsk.core.Point3D.create(yMin.x,yMin.y,yMin.z)
previousPoint = adsk.core.Point3D.create(yMin.x,yMin.y-0.1,yMin.z)
for i in range(20):
#sort list by distance to current point
sortedList = sorted(pList,key=lambda node: adsk.core.Point3D.create(node[0],node[1],node[2]).distanceTo(currentPoint))
#get the Nth closes points
closestPoints = sortedList[:closestNPoints]
#filter closestPoints for ccw angle points (these are points to the left of the current point)
leftPoints = []
for checkPoint in closestPoints:
angle = (previousPoint.x-currentPoint.x)*(checkPoint[1]-currentPoint.y)-(checkPoint[0]-currentPoint.x)*(previousPoint.y-currentPoint.y)
if angle <= 0:
leftPoints.append([checkPoint,angle])
#sort ccwPoints for largest angle between the previous 2 points (not getting the largest angle)
sortedAngles = sorted(leftPoints,key=lambda ang: ang[1],reverse=False)
#remove the largest angle point draw the line and make it the next point
pList.remove([sortedAngles[0][0][0],sortedAngles[0][0][1],sortedAngles[0][0][2]])
nextPoint = adsk.core.Point3D.create(sortedAngles[0][0][0],sortedAngles[0][0][1],sortedAngles[0][0][2])
newSketch.sketchCurves.sketchLines.addByTwoPoints(currentPoint,nextPoint)
currentPoint = nextPoint
It seems to be working until after line 35. I am sorting the closest 20 points to the current point and filtering out only the points to the left of that point. Now I am trying to find which one will create the largest angle, and thus the profile of the mesh.
you can see that it doesn't always get the point that is closest to the outside
I am ultimately trying to get this line. or as close as possible to it.
I have adapted some code I found online to make this, which calculates the convex hull.
def getConvexHull():
design:adsk.fusion.Design = app.activeProduct
root = design.rootComponent
features = root.features
def convexHull(pList: list) -> list:
#define resulting conver hull list
convexHullList = []
#find left-most and right-most points and add to result
leftPoint, rightPoint = min(pList), max(pList)
convexHullList.append(leftPoint)
convexHullList.append(rightPoint)
#call upperHull algorithm for upper part of convex hull
allPointsUpper = upperHull(leftPoint, rightPoint, pList)
#call upperHull algorithm for lower part of convex hull (lower hull)
allPointsLower = upperHull(rightPoint, leftPoint, pList)
convexHullList += allPointsUpper
convexHullList += allPointsLower
return convexHullList
def upperHull(a: list, b: list, pList: list):
#base case for when there are no points to the left of selected vector
if len(pList) == 0:
return []
upperHullPoints = []
resultPoints = []
#find p farthest from the line
maxDis = 0.0
furthestPoint = []
for p in pList:
if isLeft(a, b, p) == True:
upperHullPoints.append(p)
pDis = findDistance(a, b, p)
if(pDis > maxDis):
maxDis = pDis
furthestPoint = p
#add the furthest point to convexHull result (finalList)
if furthestPoint:
resultPoints.append(furthestPoint)
#calling upperHull algorithm on region 1 (left of vector a, furthestPoint) and region 3 (left of vector furthestPoint, b)
region1 = upperHull(a, furthestPoint, upperHullPoints)
region3 = upperHull(furthestPoint, b, upperHullPoints)
resultPoints += region1
resultPoints += region3
return resultPoints
# Geometric Calculation Functions
def findDistance(a: list, b: list, p: list):
#rewriting coordinates for simply geometric syntax
ax, ay, bx, by = a[0], a[1], b[0], b[1]
px, py = p[0], p[1]
d = 0
d = (abs(((bx - ax) * (ay - py)) - ((ax - px) * (by - ay)))) / math.sqrt((pow((bx - ax), 2)) + (pow((by - ay), 2)))
return d
def isLeft(a: list, b: list, c: list) -> bool:
#rewriting coordinates for simply geometric syntax
ax, ay, bx, by, cx, cy = a[0], a[1], b[0], b[1], c[0], c[1]
#we will take point a and point b and do the cross product of these points
z = ((bx - ax) * (cy - ay)) - ((cx - ax) * (by - ay))
if z > 0:
return True
else:
return False
newSketch:adsk.fusion.Sketch = root.sketches.add(root.xYConstructionPlane)
mesh = root.meshBodies.item(0)
nodes = mesh.displayMesh.nodeCoordinates
pList = []
for node in nodes:
pList.append([node.x,node.y])
points = convexHull(pList)
for i in range(len(points)):
newPoint = adsk.core.Point3D.create(points[i][0],points[i][1],0.0)
newSketch.sketchPoints.add(newPoint)
but I am looking for the concave hull of the mesh.
Solved! Go to Solution.
Solved by kandennti. Go to Solution.
Hi @Joshua.mursic .
It seemed like an interesting theme, so we gave it a try.
Other people on the Japanese forum gave me some ideas, but I decided to do it the steady way.
The following script creates a silhouette surface in the XY plane of the specified MeshBody component.
I believe a sketch can be created from this surface.
# Fusion360API Python script
import traceback
import adsk.core as core
import adsk.fusion as fusion
def run(context):
ui: core.UserInterface = None
try:
app: core.Application = core.Application.get()
ui = app.userInterface
msg: str = 'Select MeshBody'
selFilter: str = 'MeshBodies'
sel: core.Selection = selectEnt(msg, selFilter)
if not sel:
return
create_polygon_silhouette(sel.entity)
ui.messageBox('Done')
except:
if ui:
ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
def selectEnt(
msg: str,
filterStr: str
) -> core.Selection:
try:
app: core.Application = core.Application.get()
ui: core.UserInterface = app.userInterface
sel = ui.selectEntity(msg, filterStr)
return sel
except:
return None
def create_polygon_silhouette(
mesh: fusion.MeshBody
) -> list[fusion.BRepBody]:
tmpMgr: fusion.TemporaryBRepManager = fusion.TemporaryBRepManager.get()
# *******
def z_reset(
p: core.Point3D,
) -> core.Point3D:
p.z = 0
return p
def init_lines(
idxs: list,
nodePoints: list[core.Point3D],
) -> list[core.Line3D]:
idxs = list(idxs)
idxs.append(idxs[0])
lineLst = []
for i1, i2 in zip(idxs, idxs[1:]):
try:
line = core.Line3D.create(nodePoints[i1], nodePoints[i2])
lineLst.append(line)
except:
pass
return lineLst
def init_wireBodies(
crvs: list,
) -> list[fusion.BRepBody]:
try:
wireBody, _ = tmpMgr.createWireFromCurves(crvs)
return wireBody
except:
pass
def init_tri_faceBodies(
wireBodies: list[fusion.BRepBody],
) -> list[fusion.BRepBody]:
triBodies = []
for b in triWires:
try:
triBodies.append(tmpMgr.createFaceFromPlanarWires([b]))
except:
pass
return triBodies
def unionBodies(
bodies: list[fusion.BRepBody],
) -> fusion.BRepBody:
def union(
target: fusion.BRepBody,
tool: fusion.BRepBody,
) -> None:
try:
tmpMgr.booleanOperation(
target,
tool,
fusion.BooleanTypes.UnionBooleanType)
except:
pass
body: fusion.BRepBody = bodies.pop()
[union(body, b) for b in bodies]
return body
def dump_Bodies(
comp: fusion.Component,
bodyLst: list,
name: str = '',
) -> list[fusion.BRepBody]:
des: fusion.Design = comp.parentDesign
baseFeat: fusion.BaseFeature = None
if des.designType == fusion.DesignTypes.ParametricDesignType:
baseFeat = comp.features.baseFeatures.add()
bodies: fusion.BRepBodies = comp.bRepBodies
resBodies = []
if baseFeat:
try:
baseFeat.startEdit()
resBodies = [bodies.add(body, baseFeat) for body in bodyLst]
except:
pass
finally:
baseFeat.finishEdit()
else:
resBodies = [bodies.add(body) for body in bodyLst]
if len(name) > 0:
for b in resBodies:
b.name = name
return resBodies
# *******
triMesh: fusion.TriangleMesh = mesh.displayMesh
nodeIndices = triMesh.nodeIndices
triIndices = [nodeIndices[i: i + 3] for i in range(0, len(nodeIndices), 3)]
nodePoints = [z_reset(p) for p in triMesh.nodeCoordinates]
lineSets = [init_lines(idxs, nodePoints) for idxs in triIndices]
triWires = [init_wireBodies(lines) for lines in lineSets]
triBodies = init_tri_faceBodies(triWires)
body: fusion.BRepBody = unionBodies(triBodies)
return dump_Bodies(mesh.parentComponent, [body])
Please try with a small number of surfaces first, as processing is slow.
In some cases, an error may occur.
If you get an error, please attach the f3d file and we will work on it some more.
This works great for meshes with a small number of surfaces, thank you very much @kandennti
Hello @kandennti, I am wondering if you can help me. I am using this function to try and Get the silhouette of a mesh. I would like to expand it to be able to get the silhouette in the context of the world instead of the components matrix.
right now the function works well and gets the silhouette in the context of the component. Is there any way to get the silhouette that would be formed if we are looing at the part along the world Z axis? Something like this:
I have tried to transform the points but they are incorrect. I have also attached a sample fusion project.
import adsk.core, adsk.fusion, adsk.cam, traceback, os, sys
import math
try:
import shapely
except:
pass
# os.system('cmd /c "' + f'{sys.path[0]}\Python\python.exe -m pip install shapely' + '"')
# import shapely
import shapely.geometry
from shapely.geometry import mapping
def run(context):
try:
app = adsk.core.Application.get()
ui = app.userInterface
design:adsk.fusion.Design = app.activeDocument.products.itemByProductType("DesignProductType")
design.designType = adsk.fusion.DesignTypes.ParametricDesignType
root = design.rootComponent
occ = root.occurrences.item(0)
getMeshConvexSilhouette(occ)
except:
ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
def getMeshConvexSilhouette(occurrence:adsk.fusion.Occurrence, offset=0.0):
def graham_scan(points):
"""
Compute the convex hull of a set of 2D points using the Graham's scan algorithm.
Parameters:
points (list): A list of 2D points. Each point is represented as a tuple of two numbers (x, y).
Returns:
list: The vertices of the convex hull in counterclockwise order.
"""
# Function to check if the turn for three points is left or not
def left_turn(p1, p2, p3):
return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0]) > 0
# Step 1: Find the pivot, which is the point with the smallest y-coordinate.
pivot = min(points, key=lambda point: (point[1], point[0]))
points.remove(pivot)
# Step 2: Sort the points counterclockwise by the angle they and the pivot make with the x-axis.
points.sort(key=lambda point: (math.atan2(point[1] - pivot[1], point[0] - pivot[0]), (point[1] - pivot[1])**2 + (point[0] - pivot[0])**2))
# Step 3: Initialize the stack with the first three points.
stack = [pivot, points[0], points[1]]
# Step 4: Process the remaining points.
for point in points[2:]:
while len(stack) > 1 and not left_turn(stack[-2], stack[-1], point):
stack.pop()
stack.append(point)
return stack
#calculate the boundary points using the transformation matrix so points are calculated with correct position?
mesh = occurrence.component.meshBodies.item(0)
component = occurrence.component
nodes = mesh.displayMesh.nodeCoordinates
points = []
zTotal = 0.0
for node in nodes:
# node.transformBy(xxxx)
points.append([node.x,node.y])
zTotal+=node.z
averageZ = zTotal/len(nodes)
# Takes the convex hull, returning just the edge points in order
hullPoints = graham_scan(points)
# Simplify the boundary
simplification=0.0001
simplifiedPolygon = shapely.geometry.polygon.Polygon(hullPoints).simplify(simplification)
if offset != 0.0:
simplifiedPolygon = simplifiedPolygon.buffer(offset/10)
mappedPolygon = mapping((simplifiedPolygon))
coordinates = mappedPolygon['coordinates'][0]
sketch = component.sketches.add(component.xYConstructionPlane)
sketch.name = "ConvexBoundary"
sketch.isComputeDeferred = True
sketch.arePointsShown = False
splinePoints = adsk.core.ObjectCollection.create()
for i in range(0,len(coordinates)):
splinePoint = adsk.core.Point3D.create(coordinates[i][0],coordinates[i][1],averageZ)
# splinePoint.transformBy(xxxx)
splinePoints.add(splinePoint)
sketch.sketchCurves.sketchFittedSplines.add(splinePoints)
sketch.isComputeDeferred = False
Hi @Joshua.mursic -San.
I am sorry, but I have never used "shapely" before, so I do not know the details.
On a personal note, I have a new job this year, so I don't have as much time to work on the Fusion360API as I used to.
I am sure that once I get used to my new job, I will be able to find some time to work on it, but I am not at that stage yet.
Hi @kandennti , curious how hard it would be to adapt this script to work on any kind of body or even a component?
We often have to create the silhouette of a model to determine surface area for wind loading calculations, and this is currently done completely manually. A script like this would likely massively speed up the process.