Announcements
Attention for Customers without Multi-Factor Authentication or Single Sign-On - OTP Verification rolls out April 2025. Read all about it here.

Can't get correct instance of occurance of child component

Anonymous

Can't get correct instance of occurance of child component

Anonymous
Not applicable

When attemping to use the third occurance of a child component to create a circular pattern, the first child, not the third child, is what actually ends up being rotated.

 

I've been trying to debug this for ages and would really very greatful if anyone could help.

 

In a very brief overview of the following script I draw one section of a geodesic dome with patches, and then rotate it 3 times using CircularPatternFeatures. This would then create something like a segment of an orange that could be rotated 5 times around the Z axis to create a complete sphere.

 

To create the second and third sections I can just rotate the first section around different axis, however to get the fourth and bottom section I need to rotate the third section I just created and this is where I am failing.

 

I use a function to rotate an occurance of a component and it prints out the name of the occurance that it is rotating. This confirms I'm passing in the correct occurance, yet still that is not what is actually rotated.

 

The bug must either be on line 109 (section3 = self.root.occurrences.item(2)) or in the function rotateTriangleSegment() which is on line 421.

 

I realise this is not a debugging service, however if anyone can help me resolve this then I'll give back to the community by releasing a plugin to generate geodesic domes.

 

(I can make this work - but only by abandoning having a top level component (3v geodesic sphere) - and it seems like that would clutter up users document tree too much so would really love to get this to work.)

 

Here is an image in an attempt to help communicate clearly:

 

This shows what is going wrong and what I'm trying to achiveThis shows what is going wrong and what I'm trying to achiveDoc tree. Segment 4 is in the wrong place.Doc tree. Segment 4 is in the wrong place.

 

 

Here is my script:

 

# Author - Felix Eve
# Description - Draw a 3v geodesic dome

import adsk.core, adsk.fusion, adsk.cam, traceback, math

def run(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui  = app.userInterface

        # Create a document
        fusionDocType = adsk.core.DocumentTypes.FusionDesignDocumentType
        doc = app.documents.add(fusionDocType)

        product = app.activeProduct
        design = adsk.fusion.Design.cast(product)

        # Get the root component of the active design
        root = design.rootComponent
        
        geodesic = Geodesic(root, 2)
        geodesic.render()
        
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


class Geodesic:
    """Creates 3 frequency a geodesic sphere"""
    
    def __init__(self, rootComponent, radius=1):
        """Constructor to setup references and create the component everything 
        will live in and run setup()"""
        
        self.realRoot = rootComponent        
        
        # Set the radius as a property of the class
        self.radius = radius
        
        # Set the frequency
        self.frequency = 3
        
        # Set dihedral angle
        self.dihedralAngle = (63.434948823 / self.frequency) * (math.pi / 180)
        
        # The angle to rotate everything to get a flat base
        self.offsetAngle = 58.xxx-xxxxxxxx
        
        # Create the component everything else will live in
        location = adsk.core.Matrix3D.create()
        occurrence = rootComponent.occurrences.addNewComponent(location)
        self.root = occurrence.component
        self.root.name = '3v Geodesic sphere'
        
        # Create a reference to sketches
        self.sketches = self.root.sketches
        
        # Run setup
        self.setup()

    def setup(self):
        """Run setup tasks - creates sketches, set scale"""
        
        self.tLines = {} # Triangle lines

        # Create sketch to hold a segment of the geodesic sphere
        self.segmentSketch = self.sketches.add(self.root.xYConstructionPlane)
        self.segmentSketch.name = 'Geodesic segment'
        
        # Create sketch to hold the icosahedron       
        self.icoSketch = self.sketches.add(self.root.xYConstructionPlane)
        self.icoSketch.name = 'Icosahedron'
        
        # Golden ratio
        self.GR = (1 + math.sqrt(5)) / 2
        
        # The radius of the sphere
        initialRadius = math.sqrt(1 + math.pow(self.GR, 2))
        
        # Set scale
        self.factor = self.radius / initialRadius
        
        # Set origin 3d point
        self.originPoint3D = adsk.core.Point3D.create(0, 0, 0)

    def render(self):
        """Actually draw the geodesic sphere"""
        
        self.drawIcosahedron()
        self.drawConstructionLines()
        self.drawSegment()
        self.rotateFlat(self.icoSketch)
        self.rotateFlat(self.segmentSketch)
        
        # Use segment sketch to create patches
        section1 = self.createPatches()
        
        # Create first rotated occurrence - rotate seg1 round axis H
        axis = self.constructionLines['H']
        self.rotateTriangleSegment(section1, axis, '(360 / 5) * 4')
        
        # Create second rotated occurrence - rotate seg1 round axis C
        axis = self.constructionLines['C']
        self.rotateTriangleSegment(section1, axis, '(360 / 5) * 2')
        
        # Create third rotated occurrence - rotate seg3 round axis L
        section3 = self.root.occurrences.item(2)
        axis = self.constructionLines['L']
        
        # Why does this line rotate the wrong segment‽‽‽‽‽‽‽‽‽‽‽
        self.rotateTriangleSegment(section3, axis, '(360 / 5) * 1')
        
        # Delete other axis so it's obvious which one axis L is that was used 
        # in the last optionation
        axis = self.constructionLines['C'].deleteMe()
        axis = self.constructionLines['D'].deleteMe()
        axis = self.constructionLines['H'].deleteMe()
        
    def drawIcosahedron(self):
        """Create all the points of the icosahedron"""

        icoSketch = self.icoSketch

        # Create origin point on icosahedron sketch        
        self.icoOriginPoint = icoSketch.sketchPoints.add(self.originPoint3D)

        # Create dictionary to hold icosahedron points  
        self.icoPoints = {}
        
        GR = self.GR
        points = {
            'A': [0,    1,  GR],
            'B': [0,   -1,  GR], # Bottom
            'C': [0,   -1, -GR], 
            'D': [0,    1, -GR], # Top
            'E': [GR,   0,   1], 
            'F': [-GR,  0,   1], 
            'G': [-GR,  0,  -1], 
            'H': [GR,   0,  -1],
            'I': [1,   GR,   0], 
            'J': [-1,  GR,   0], 
            'K': [-1, -GR,   0], 
            'L': [1,  -GR,   0],
        }
        
        # Loop points and create them on the base sketch
        for (name, coords) in points.items():
            x = coords[0] * self.factor
            y = coords[1] * self.factor
            z = coords[2] * self.factor
            point = adsk.core.Point3D.create(x, y, z)
            self.icoPoints[name] = icoSketch.sketchPoints.add(point)
            
    def drawConstructionLines(self):
        """Create somes constructions lines from the origin to certain points 
        of the icosahedron that we'll later use as axis of rotation"""
        
        origin = self.icoOriginPoint
        sketchLines = self.icoSketch.sketchCurves.sketchLines
        
        lineC = sketchLines.addByTwoPoints(origin, self.icoPoints['C'])
        lineC.isConstruction = True        
        
        lineD = sketchLines.addByTwoPoints(origin, self.icoPoints['D'])
        lineD.isConstruction = True
        
        lineH = sketchLines.addByTwoPoints(origin, self.icoPoints['H'])
        lineH.isConstruction = True

        lineL = sketchLines.addByTwoPoints(origin, self.icoPoints['L'])
        lineL.isConstruction = True
        
        self.constructionLines = {}
        self.constructionLines['D'] = lineD
        self.constructionLines['H'] = lineH
        self.constructionLines['L'] = lineL
        self.constructionLines['C'] = lineC
        
    def drawSegment(self):
        """Draw one segment of the geodesic dome that sits within triangle 
        C.D.H. of the icosahedron"""
        
        # Create triangle sides - genereated (intermediary) points are returned
        sides = []
        sides.append(self.createTriangleSide('C', 'D', 's1'))
        sides.append(self.createTriangleSide('D', 'H', 's2'))
        sides.append(self.createTriangleSide('H', 'C', 's3'))
        
        # Create lines that go across the corners of the triangle
        self.createLine(sides[0][0], sides[2][1], 's3_s1_c')
        self.createLine(sides[0][1], sides[1][0], 's1_s2_c')
        self.createLine(sides[2][0], sides[1][1], 's2_s3_c')

        # Find the centre of the triangle pushed to the outer sphere
        centrePoint = self.getExtendedCentre('C', 'D', 'H')
        
        # Create lines to the centre
        for sideN, side in enumerate(sides):
            for pointN, point in enumerate(side):
                lineName = 's' + str(sideN + 1) + '_c' + str(pointN + 1)
                self.createLine(centrePoint, point, lineName)
                
        
    def createTriangleSide(self, point1Name, point2Name, sideName):
        """Create all the lines that make up one side of one of the icosahedron 
        triangles. Each point is pushed to the outside of the sphere. To do 
        this we need to make a plane through these three points and a sketch
        so when we draw an arc it knows which direction to go in."""
        
        lineName = point1Name + point2Name
        
        # Create plane through 2 points and origin and sketch
        plane, sketch = self.createSketchOnPlane(point1Name, point2Name)
        
        # Find where points in 3d space intersect with our new plane
        entities = []
        entities.append(self.icoPoints[point1Name])
        entities.append(self.icoPoints[point2Name])
        intersectPos = sketch.intersectWithSketchPlane(entities)
        
        # Draw arcs to find locations for new points
        sketchArcs = sketch.sketchCurves.sketchArcs
        centre = self.originPoint3D
        start = intersectPos[0]
        arcs = []
        for i in range(1, self.frequency):
            angle = self.dihedralAngle * i
            arc = sketchArcs.addByCenterStartSweep(centre, start, angle)
            arcs.append(arc)

        # Create all the points along the edge
        points = []
        limit = self.frequency + 1
        for i in range(0, limit):
            # If first or last item use the icosahedron point position
            if (i == 0 or i == limit-1):
                index = 0 if (i == 0) else 1
                point = self.create3dPointFromObject(intersectPos[index])
            # Else use the arc end points for the intermediary points
            else:
                index = i - 1
                point = self.create3dPointFromObject(arcs[index].endSketchPoint)
                
            points.append(point)

        # Initialize an array to hold the new generated points we've made
        generatedPoints = []        
        
        # Draw the lines        
        for i in range(0, 3):
            lineName = sideName + '_l' + str(i + 1)
            line = self.createLine(points[i], points[i + 1], lineName)
            
            if(i > 0):
                lnSwG = line.startSketchPoint.worldGeometry
                point3D = adsk.core.Point3D.create(lnSwG.x, lnSwG.y, lnSwG.z)
                point = self.segmentSketch.sketchPoints.add(point3D)
                generatedPoints.append(point)

        # Draw icosahedron line using first and last point
        self.icoSketch.sketchCurves.sketchLines.addByTwoPoints(points[0], points[-1])
        
        # Clean up undeeded lines, planes and sketches
        for arc in arcs:
            arc.deleteMe()

        plane.deleteMe()
        sketch.deleteMe()
        
        return generatedPoints
        
    def createSketchOnPlane(self, point1Name, point2Name):
        """Create a plane through the 2 points passed in and the origin and 
        create a sketch on that plane"""
        
        lineName = point1Name + point2Name        
        
        # Create construction plane input
        planeInput = self.root.constructionPlanes.createInput()
    
        # Add construction plane by three points
        point1 = self.root.originConstructionPoint
        point2 = self.icoPoints[point1Name]
        point3 = self.icoPoints[point2Name]
        planeInput.setByThreePoints(point1, point2, point3)
        plane = self.root.constructionPlanes.add(planeInput)
        plane.name = 'plane ' + lineName
        
        # Add sketch on new plane
        sketch = self.sketches.add(plane)  
        sketch.name = 'sketch ' + lineName
        
        return plane, sketch
    
    def create3dPointFromObject(self, object):
        """Returns a 3D point created from the world geometry of the object 
        passed in"""
        
        x = object.worldGeometry.x
        y = object.worldGeometry.y
        z = object.worldGeometry.z
        return adsk.core.Point3D.create(x, y, z)
        
    def createLine(self, point1, point2, name):
        """Create one line of the geodesic and store a named reference to it in
        the tLines dictionary"""
        
        sketchLines = self.segmentSketch.sketchCurves.sketchLines
        self.tLines[name] = sketchLines.addByTwoPoints(point1, point2)
        return self.tLines[name]
    
    def getExtendedCentre(self, point1Name, point2Name, point3Name):
        """Find the centre of the 3 points passed in, draw a line from the 
        origin through that point the length of the radius and then return a 
        point at that location"""
        
        # Find the centre of the triangle
        centrePoint = self.getCentrePoint(point1Name, point2Name, point3Name)
        
        # Create a vector through the triangle centre and extend it's length to the radius
        pointGeo = centrePoint.worldGeometry
        vector = adsk.core.Vector3D.create(pointGeo.x, pointGeo.y, pointGeo.z)
        vector.normalize()
        vector.scaleBy(self.radius)
        
        # Now we can delete the triangle centre point
        centrePoint.deleteMe()
        
        # Create a point at the end of the vector
        point3D = adsk.core.Point3D.create(vector.x, vector.y, vector.z)
        return self.segmentSketch.sketchPoints.add(point3D)
        
    
    def getCentrePoint(self, point1Name, point2Name, point3Name):
        """Find the centre of the 3 points passed in and return a sketchPoint
        located there"""
        
        points = []
        points.append(self.icoPoints[point1Name])
        points.append(self.icoPoints[point2Name])
        points.append(self.icoPoints[point3Name])
        
        ax = ay = az = 0
        for point in points:
            ax += point.worldGeometry.x
            ay += point.worldGeometry.y
            az += point.worldGeometry.z

        point3D = adsk.core.Point3D.create(ax / 3, ay / 3, az / 3)
        return self.segmentSketch.sketchPoints.add(point3D)
        
    def rotateFlat(self, sketch):
        """The icosahedron and first geodesic dome segment are initially 
        created on a bit of an angle. This function rotates them so the base on 
        the dome would be flat aginst the xy plane."""
        
        all = adsk.core.ObjectCollection.create()
        for c in sketch.sketchCurves:
            all.add(c)
        for p in sketch.sketchPoints:
            all.add(p)
            
        vector1 = adsk.core.Vector3D.create(0, 1, 0)
        vector2 = adsk.core.Vector3D.create(0, 0, 1)
        
        axis = vector1.crossProduct(vector2)
        axis.transformBy(sketch.transform)
        
        origin = sketch.origin
        origin.transformBy(sketch.transform)
        
        angle = (self.offsetAngle + 90) * (math.pi / 180)
        
        mat = adsk.core.Matrix3D.create()
        mat.setToRotation(angle, axis, origin)
        
        sketch.move(all, mat)
        
    def createPatches(self):
        """Create a patch for each triangle in the geodesic segment. Put in 
        patch in a new child component"""
        
        location = adsk.core.Matrix3D.create()
        occurrence = self.root.occurrences.addNewComponent(location)
        comp = occurrence.component
        comp.name = 'Segment'
        
        # Bottom row            
        self.createPatchFrom3Lines('s1_l1', 's3_l3', 's3_s1_c', comp)
        self.createPatchFrom3Lines('s3_s1_c', 's1_c1', 's3_c2', comp)
        self.createPatchFrom3Lines('s3_c2', 's3_c1', 's3_l2', comp)
        self.createPatchFrom3Lines('s3_c1', 's2_c2', 's2_s3_c', comp)
        self.createPatchFrom3Lines('s2_s3_c', 's3_l1', 's2_l3', comp)
        
        # Middle row
        self.createPatchFrom3Lines('s1_l2', 's1_c1', 's1_c2', comp)
        self.createPatchFrom3Lines('s1_c2', 's1_s2_c', 's2_c1', comp)
        self.createPatchFrom3Lines('s2_c1', 's2_c2', 's2_l2', comp)
        
        # Top row
        self.createPatchFrom3Lines('s1_l3', 's1_s2_c', 's2_l1', comp)
        
        return occurrence
        
    def createPatchFrom3Lines(self, line1Name, line2Name, line3Name, comp):
        """Create and return a patch based off the 3 line names passed in"""
        
        lines = adsk.core.ObjectCollection.create()        
        lines.add(self.tLines[line1Name])
        lines.add(self.tLines[line2Name])
        lines.add(self.tLines[line3Name])
        
        # Create a patch from our lines collection
        patches = comp.features.patchFeatures
        newBodyOp = adsk.fusion.FeatureOperations.NewBodyFeatureOperation
        patchInput = patches.createInput(lines, newBodyOp)
        return patches.add(patchInput)
        
    def rotateTriangleSegment(self, occurrence, axis, angleString):
        """Rotate the component around the axis by the angle and create a copy. 
        Returns the new component."""  
        
        print('Rotating occurrence with name ' + occurrence.name)
        
        # Create input entities for circular pattern
        inputEntites = adsk.core.ObjectCollection.create()
        inputEntites.add(occurrence)
            
        # Create the input for circular pattern
        circularFeats = self.root.features.circularPatternFeatures
        circularFeatInput = circularFeats.createInput(inputEntites, axis)
        
        # Set the quantity of the elements
        circularFeatInput.quantity = adsk.core.ValueInput.createByReal(2)
        
        # Set the angle of the circular pattern
        circularFeatInput.totalAngle = adsk.core.ValueInput.createByString(angleString)
        
        # Set symmetry of the circular pattern
        circularFeatInput.isSymmetric = False
        
        # Create the circular pattern
        return circularFeats.add(circularFeatInput)
        
0 Likes
Reply
Accepted solutions (1)
1,387 Views
5 Replies
Replies (5)

p.seem
Advocate
Advocate

Felix,

 

1. Nice. 

 

2. It does look like a bug in Fusion, to me.  When I run your script, all three of those circular patterns have a warning on them, "Failed to get owner occurrence transform."  That error is discussed some time ago in the forum, but not in a way that seems very helfpul.

 

3.  Interestingly, if I manually edit the circular feature and say OK without changing anything, each one computes correctly.  Recomputing isn't enough, though.  With some quick banging away at it, I haven't found a way to recreate that effect from within a script.

 

So I know none of that is helpful so far, but at least you know you're not alone!

 

4. The only workaround I can find is to run the whole thing in with the document set to direct modelling mode instead of parametric mode.  Whatever bug behind the scenes is causing the feature to lose its reference doesn't apply then, and the 'orange peel' comes out as it should.    If you can afford to lose the timeline, that might be a way forward.

 

Good luck!

1 Like

JesusFreke
Advocate
Advocate

As a possible workaround, you could try modifying the occurrence transform directly. Something roughly like:

 

transform = occurrence.transform
rotation_matrix = adsk.core.Matrix3D.create()
# assuming axis is a ConstructionAxis
# need to convert your input angle to a double first, of course rotation_matrix.setToRotation(angle, axis.geometry.direction, axis.geometry.origin) transform.transformBy(rotation_matrix) occurrence.transform = transform # only needed in parametric mode design.snapshots.add()

 

 

 

1 Like

Anonymous
Not applicable

For now I'm using a workaround where I keep all occurances at the top level of the tree to do all the rotating, and then move them all into the Geodesic child component once they are all in place. It's doesn't feel quite as clean and introduces an extra step but works for now.

 

Thanks for confirming it wasn't just me doing something stupid and does look like a bug 🙂

0 Likes

p.seem
Advocate
Advocate
Accepted solution

Ok, I think I've got it.

 

I think often Fusion360 expects inputs (i.e. the collections of entities you pass it) to be generated by the user clicking on things. The occurrence you are retrieving is the occurrence from the native component of '3v Geodesic sphere', not the particular occurrence of '3v Geodesic sphere' (that is, it's assemblyContext is null).  We can get the native object easily in the script, but their devs didn't anticipate it because the user could never get the native object by clicking on something (except where you're working on the top-level component, hence your workaround).  We can fake that in your code by making sure the occurrence we pass it has the same assembly context it would if we grabbed it from the tree.

 

replace

section1 = self.createPatches()

with:

 

section1 = self.createPatches().createForAssemblyContext(self.rootOcc)

 

 

and replace:

section3 = self.root.occurrences.item(2)

with:

section3 = self.root.occurrences.item(2).createForAssemblyContext(self.rootOcc)

where:

self.rootOcc = occurrence

is added to your __init__ routine.

 

 

That executes cleanly (no warnings on the feature) for me.

 

I hope that helps.

1 Like

Anonymous
Not applicable

Thanks p.seem. That is super helpful 🙂

0 Likes