I'll share here a collection of functions and code that will create the loft(s). On line 384 one can choose to enable the first codition (if i > 1 and i < 8:) and the script will not throw.
Question is: how can a loft be checked in advance (by script, to prevent a throw) so invalid profiles can be skipped or lofts can be created in smaller segments as the script error suggests.
NB: the example script enforces the error of a loft intersecting itself and it would be easy to generate profiles at a greater distance between each other. But sometimes the loft created has NO overlapping profiles and still throws an error because the loft created is intersecting itself, or for other reasons (like 'profile can not be tangent to profile curve' or a somewhat similar error message).
Example dynamic loft creating script:
import adsk.core, adsk.fusion, traceback
import json
import math
def run(context):
ui = None
# points as inline json string
points = '[{"x":0,"y":5.073,"z":0},{"x":0.009,"y":5.073,"z":0},{"x":0.019,"y":5.072,"z":0},{"x":0.062,"y":5.063,"z":0},{"x":0.11,"y":5.043,"z":0},{"x":0.219,"y":4.985,"z":0},{"x":0.378,"y":4.9,"z":0},{"x":0.865,"y":4.631,"z":0},{"x":2.398,"y":3.032,"z":0},{"x":2.813,"y":2.278,"z":0},{"x":3.056,"y":1.668,"z":0},{"x":3.267,"y":1.139,"z":0},{"x":3.406,"y":0.571,"z":0}]'
# curves 7 profiles as inline json string
curvesProfiles = '[{"logs":[],"curves":[{"controlpoints":[{"x":-17.164698747883282,"y":17.983441575918768},{"x":-19.276646783197553,"y":15.072531043913877},{"x":-19.842079309499557,"y":9.943955465135005},{"x":-16.167172213186305,"y":7.92705030228685}]},{"controlpoints":[{"x":-16.167172213186305,"y":7.92705030228685},{"x":-14.928784783562827,"y":7.247384140679769},{"x":-13.064351705933902,"y":7.180103779376454},{"x":-12.175987900344557,"y":8.469282960447748}]},{"controlpoints":[{"x":-12.175987900344557,"y":8.469282960447748},{"x":-11.793612183755853,"y":9.02418041357659},{"x":-11.65497624954449,"y":10.174654013358671},{"x":-12.497151171652042,"y":10.414568937788822}]},{"controlpoints":[{"x":-12.497151171652042,"y":10.414568937788822},{"x":-12.732758801110624,"y":10.481687757327245},{"x":-13.230486534171773,"y":10.319471218563942},{"x":-13.00189201028427,"y":10.003965490376801}]}],"profiles":[{"time":0,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.006582480615909264,"x":-17.164698747883282,"y":17.983441575918768,"normalX":-16.75999515809407,"normalY":17.689817659297987},{"time":0.12397340822138267,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.005316211641262808,"x":-18.261255115729526,"y":15.917366583246196,"normalX":-17.792936891358234,"normalY":15.742215103886977},{"time":0.3639686579083045,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.007973773790285842,"x":-18.705118373189233,"y":11.454742650640483,"normalX":-18.215106175664925,"normalY":11.554181306491191},{"time":0.6270766760372518,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.01268499359892902,"x":-15.727121422657488,"y":7.72173654893293,"normalX":-15.544793375893343,"normalY":8.187307685670564},{"time":0.8299716224098505,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.021737997980057003,"x":-12.220067619140753,"y":8.407499232010926,"normalX":-12.622273198607513,"normalY":8.704535714359151},{"time":0.9376861973507108,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.06545774344173169,"x":-12.210696540609026,"y":10.273496678370055,"normalX":-12.515329568971481,"normalY":9.877013743011268},{"time":0.9668013116203719,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.07200862608751893,"x":-12.725072984595046,"y":10.420408493599918,"normalX":-12.642924838193753,"normalY":9.927202976555723},{"time":0.9751009837152789,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.09190712828187159,"x":-12.875537879144028,"y":10.376731162758054,"normalX":-12.676150854235896,"normalY":9.918206556768055},{"time":0.9834006558101859,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.19038649718841621,"x":-13.00509802809042,"y":10.289876552236755,"normalX":-12.63890448608533,"normalY":9.94943229865951},{"time":0.9917003279050929,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.2657275421001583,"x":-13.06018020708229,"y":10.14754961150576,"normalX":-12.561040502134478,"normalY":10.176867778989628},{"time":0.9999999999999999,"breakTime":0.9668013116203719,"flip":-1,"curvature":-0.10940553166634062,"x":-13.00189201028427,"y":10.003965490376801,"normalX":-12.596997080386581,"normalY":10.297325500847123}]}]'
def createExtrude(rootComp, design, pointData, flipX):
SX = 0.1
# Create a new sketch on the xy plane.
sketch = rootComp.sketches.add(rootComp.xYConstructionPlane)
# Create an object collection for the points.
points = adsk.core.ObjectCollection.create()
# Define the points the spline with fit through.
for i in range(len(pointData)):
points.add(adsk.core.Point3D.create(SX * flipX * pointData[i]['x'], SX * pointData[i]['y'], SX * pointData[i]['z']))
for i in reversed(range(len(pointData))):
points.add(adsk.core.Point3D.create(SX * flipX * pointData[i]['x'], -1 * SX * pointData[i]['y'], SX * pointData[i]['z']))
points.add(adsk.core.Point3D.create(0, 0, 0))
points.add(adsk.core.Point3D.create(SX * flipX * pointData[0]['x'], SX * pointData[0]['y'], SX * pointData[0]['z']))
# Create the spline.
sketch.sketchCurves.sketchFittedSplines.add(points)
# Get the profile defined by the circle
prof = sketch.profiles.item(0)
# Extrude shortRight
distance = adsk.core.ValueInput.createByReal(SX * -1)
extrude1 = extrudes.addSimple(prof, distance, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
# Get the state of the extrusion
health = extrude1.healthState
if health == adsk.fusion.FeatureHealthStates.WarningFeatureHealthState or health == adsk.fusion.FeatureHealthStates.ErrorFeatureHealthState:
message = extrude1.errorOrWarningMessage
# Get the state of timeline object
timeline = design.timeline
timelineObj = timeline.item(timeline.count - 1)
health = timelineObj.healthState
message = timelineObj.errorOrWarningMessage
# Get the extrusion body
return extrude1.bodies.item(0)
def createNonRationalCurvesFromPathJSData(data, rootComp):
sketch = rootComp.sketches.add(rootComp.xZConstructionPlane)
sketch.name = 'pathCurve'
curves = adsk.core.ObjectCollection.create()
for i in range(len(data)):
knots = [0,0,0,0,1,1,1,1]
# list of controlpoints
curveControlPoints = getCurveControlPoints(data[i])
# Create the spline.
nonRational = adsk.core.NurbsCurve3D.createNonRational(curveControlPoints, 3, knots, False)
crv = sketch.sketchCurves.sketchFittedSplines.addByNurbsCurve(nonRational)
curves.add(crv)
return curves
def getCurveControlPoints(curveData):
create = adsk.core.Point3D.create
return [
create3DPointFromControlPoint(curveData['controlpoints'][0]),
create3DPointFromControlPoint(curveData['controlpoints'][1]),
create3DPointFromControlPoint(curveData['controlpoints'][2]),
create3DPointFromControlPoint(curveData['controlpoints'][3])
]
def create3DPointFromControlPoint(controlPoint):
return adsk.core.Point3D.create(
controlPoint['x'],
controlPoint['y'],
0
)
DEFAULT_NAME = 'planeOnCurve'
def createConstructionPlanesOnPathByName(constructionPlanes, curves, profiles):
planeNames = []
path = adsk.fusion.Path.create(curves[0], adsk.fusion.ChainedCurveOptions.connectedChainedCurves)
for i in range(len(profiles)):
planeInput = constructionPlanes.createInput()
planeInput.setByDistanceOnPath(path, adsk.core.ValueInput.createByReal(profiles[i]['time']))
plane = constructionPlanes.add(planeInput)
plane.name = getPlaneName(planeNames)
planeNames.append(plane.name)
return planeNames
def getPlaneName(planeNames):
return "%s-%02d" % (DEFAULT_NAME, len(planeNames))
def getConstructionPlanePositionsProfiles(profiles):
planePositions = []
for i in range(len(profiles)):
planePositions.append(getPathPointDict(profiles[i]))
return planePositions
def getPathPointDict(profileData):
dictionary = {}
point = create3DPointFromControlPoint(profileData)
normalVector = createNormalVectorFromCurveData(profileData)
time = profileData['time']
breakTime = profileData['breakTime']
flip = profileData['flip']
return {
'controlpoint': point,
'normalVector': normalVector,
'time': time,
'breakTime': breakTime,
'flip': flip
}
def createNormalVectorFromCurveData(curveDataControlPoint):
normalVector = adsk.core.Vector3D.create(
curveDataControlPoint['normalX'] - curveDataControlPoint['x'],
0,
-1 * curveDataControlPoint['normalY'] + curveDataControlPoint['y'],
)
normalVector.normalize()
return normalVector
def mapTime(positions, leftBreak):
for i in range(len(positions)):
timeValue = positions[i]['time']
breakTime = positions[i]['breakTime']
if (timeValue <= breakTime):
positions[i]['timeMapped'] = leftBreak * (timeValue / breakTime)
else:
positions[i]['timeMapped'] = leftBreak + (1 - leftBreak) * (timeValue - breakTime) / (1 - breakTime)
return positions
def mapScaleRight(positions, leftBreak):
for i in range(len(positions)):
timeValue = positions[i]['time']
breakTime = positions[i]['breakTime']
addEndScaleX = 0.5
endNArrowX = 0.1
addEndScaleY = 1
if (timeValue <= breakTime):
positions[i]['scaleXRight'] = 1 + mapX(timeValue) * addEndScaleX
positions[i]['scaleYRight'] = 1 + mapX(timeValue) * addEndScaleY
positions[i]['preBreak'] = True
else:
tt = (timeValue - breakTime)/(1 - breakTime)
# narrow = mapXInv(tt)
narrow = 1
positions[i]['scaleXRight'] = (1 + mapX(timeValue) * addEndScaleX) * (endNArrowX + (1-endNArrowX) * narrow)
positions[i]['scaleYRight'] = 1 + mapX(timeValue) * addEndScaleY
positions[i]['preBreak'] = False
return positions
def mapScaleLeft(positions, leftBreak):
for i in range(len(positions)):
timeValue = positions[i]['time']
breakTime = positions[i]['breakTime']
addEndScaleX = 1
addEndScaleY = 1
positions[i]['scaleXLeft'] = 1 + mapX(timeValue) * addEndScaleX
positions[i]['scaleYLeft'] = 1 + mapX(timeValue) * addEndScaleY
positions[i]['preBreak'] = None
return positions
def mapX(time):
return 0.5 * (1 + math.sin(math.pi * (time - 0.5)))
def mapXInv(time):
return 0.5 * (1 + math.sin(math.pi * ((1 - time) - 0.5)))
SAFE = 1
def createProfilesAlongPathShort(rootComp, planeAtPath, constructionPlanePosition, bodyName, scaleXName, scaleYName):
time = constructionPlanePosition['timeMapped']
flip = constructionPlanePosition['flip']
scaleX = constructionPlanePosition[scaleXName]
scaleY = constructionPlanePosition[scaleYName]
normalVector = constructionPlanePosition['normalVector']
crvPoint = constructionPlanePosition['controlpoint']
body = rootComp.bRepBodies.itemByName(bodyName)
if (scaleX == None or scaleY == None):
return None
# calc body depth
bodyBB = body.boundingBox
bodyDept = math.fabs(bodyBB.minPoint.z - bodyBB.maxPoint.z) * SAFE
newBody = body.copyToComponent(rootComp)
m1 = adsk.core.Matrix3D.create()
m1.translation = adsk.core.Vector3D.create(0, 0, 0.5 * bodyDept)
m2 = adsk.core.Matrix3D.create()
m2.setToRotateTo(
adsk.core.Vector3D.create(-1, 0, 0),
normalVector,
adsk.core.Vector3D.create(0, 1, 0)
)
m22 = adsk.core.Matrix3D.create()
m22.setToRotation(math.pi, rootComp.yConstructionAxis.geometry.getData()[2], rootComp.originConstructionPoint.geometry)
m3 = adsk.core.Matrix3D.create()
m3.translation = adsk.core.Vector3D.create(crvPoint.x, 0, -crvPoint.y)
m4 = adsk.core.Matrix3D.create()
m4.transformBy(m1)
m4.transformBy(m2)
if (flip == -1):
m4.transformBy(m22)
m4.transformBy(m3)
bodyColl = adsk.core.ObjectCollection.create()
bodyColl.add(newBody)
# scale non-uniform
scalePointInput = rootComp.constructionPoints.itemByName('Origin')
scaleFactor = adsk.core.ValueInput.createByReal(1)
scaleInput = rootComp.features.scaleFeatures.createInput(bodyColl, scalePointInput, scaleFactor)
xScale = adsk.core.ValueInput.createByReal(scaleX)
yScale = adsk.core.ValueInput.createByReal(scaleY)
zScale = adsk.core.ValueInput.createByReal(1)
scaleInput.setToNonUniform(xScale, yScale, zScale)
scale = rootComp.features.scaleFeatures.add(scaleInput)
# end scale
if (not m4.isEqualTo(adsk.core.Matrix3D.create())):
moveInput = rootComp.features.moveFeatures.createInput(bodyColl, m4)
moveFeat = rootComp.features.moveFeatures.add(moveInput)
sketchAtPath = rootComp.sketches.add(planeAtPath)
sketchAtPath.intersectWithSketchPlane([newBody])
# hide component
newBody.isLightBulbOn = False
# sketchAtPath.isLightBulbOn = False
return sketchAtPath.profiles.item(0)
def createScalePoint(rootComp, design):
# see https://forums.autodesk.com/t5/fusion-360-api-and-scripts/enviroment-not-supported-error-creating-a-construction-point/m-p/6368769
baseFeat = None
if design.designType == adsk.fusion.DesignTypes.ParametricDesignType:
baseFeat = rootComp.features.baseFeatures.add()
if baseFeat:
baseFeat.startEdit()
constructionPoints = rootComp.constructionPoints
pointInput = constructionPoints.createInput()
point = adsk.core.Point3D.create(0, 0, 0)
pointInput.setByPoint(point)
cp1 = constructionPoints.add(pointInput)
cp1.name = "Origin"
if baseFeat:
baseFeat.finishEdit()
def createLoftFromProfiles(rootComp, constructionPlanePositions, profileName, nonRationalCurves, inOneGo):
if (inOneGo == True):
createLoftFromProfilesOneGo(rootComp, constructionPlanePositions, profileName, nonRationalCurves)
else:
for i in range(len(constructionPlanePositions) - 1):
if profileName in constructionPlanePositions[i] and profileName in constructionPlanePositions[i + 1]:
createLoftFromTwoProfiles(
rootComp,
constructionPlanePositions[i][profileName],
constructionPlanePositions[i + 1][profileName],
nonRationalCurves[0]
)
def createLoftFromProfilesOneGo(rootComp, constructionPlanePositions, profileName, nonRationalCurves):
# Create loft feature input
loftFeats = rootComp.features.loftFeatures
loftInput = loftFeats.createInput(adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
loftSectionsObj = loftInput.loftSections
for i in range(len(constructionPlanePositions)):
if profileName in constructionPlanePositions[i]:
loftSectionsObj.add(constructionPlanePositions[i][profileName])
loftInput.isSolid = True
# create path from first curve
path = rootComp.features.createPath(nonRationalCurves[0])
loftInput.centerLineOrRails.addCenterLine(path)
# Create loft feature
loftFeats.add(loftInput)
def createLoftFromTwoProfiles(rootComp, profile0, profile1, nonRationalCurve):
# Create loft feature input
loftFeats = rootComp.features.loftFeatures
loftInput = loftFeats.createInput(adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
loftSectionsObj = loftInput.loftSections
loftSectionsObj.add(profile0)
loftSectionsObj.add(profile1)
loftInput.isSolid = True
# create path from first curve
path = rootComp.features.createPath(nonRationalCurve)
loftInput.centerLineOrRails.addCenterLine(path)
# Create loft feature
loftFeats.add(loftInput)
try:
app = adsk.core.Application.get()
ui = app.userInterface
# load points from a json string
pointData = json.loads(points)
doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
design = app.activeProduct
# Get the root component of the active design.
rootComp = design.rootComponent
# Get extrude features
extrudes = rootComp.features.extrudeFeatures
# create profile project bodies
bodyRight = createExtrude(rootComp, design, pointData, 1)
bodyRight.name = "shortRight"
bodyLeft = createExtrude(rootComp, design, pointData, -1)
bodyLeft.name = "shortLeft"
createScalePoint(rootComp, design)
# load points from a json string
jsonData = json.loads(curvesProfiles)
data = jsonData[0]['curves']
profiles = jsonData[0]['profiles']
# create sketch 'pathCurve' with nonRational spline(s) from PathJS-data
nonRationalCurves = createNonRationalCurvesFromPathJSData(data, rootComp)
# create planes along path(s) and return their names
planeNames = createConstructionPlanesOnPathByName(rootComp.constructionPlanes, nonRationalCurves, profiles)
# get list of points and normal vectors from PathJS-data curve segements
constructionPlanePositions = getConstructionPlanePositionsProfiles(profiles)
# extend constructionPlanePositions with plane names
for i in range(len(planeNames)):
constructionPlanePositions[i]['planeName'] = planeNames[i]
# prepare data
LEFT_BREAK = 83.368/100
constructionPlanePositions = mapTime(constructionPlanePositions, LEFT_BREAK)
constructionPlanePositions = mapScaleLeft(constructionPlanePositions, LEFT_BREAK)
constructionPlanePositions = mapScaleRight(constructionPlanePositions, LEFT_BREAK)
# create profiles on planes along path
for i in range(len(constructionPlanePositions)):
# this condition succeeds:
# if i > 1 and i < 8:
# this condition fails because the created loft will intersect itself:
if i > 1 and i < 9:
constructionPlanePositions[i]['profileRight'] = createProfilesAlongPathShort(
rootComp,
rootComp.constructionPlanes.itemByName(constructionPlanePositions[i]['planeName']),
constructionPlanePositions[i],
'shortRight',
'scaleXRight',
'scaleYRight'
)
createLoftFromProfiles(rootComp, constructionPlanePositions, 'profileRight', nonRationalCurves, False)
except:
if ui:
ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))