Let me start of by saying I’m a CAD freak and those who know me might be a little surprised by this post, but there are aspects of the CAD system that require some calculus and this is my goal and by creating these posts I’m aiming at teaching myself and maybe at the same time helping others. So I look forward to any comments on this subject.

We will look at integrals which help us find areas and volumes and derivatives which help us find the slope of a curve at a given point.

So we have a curve that has a function f(x) = X^2 and we want to find the area under the curve between X= 4 and X=7 how do we do this?

First we have to write the integral, remember we raise the power by 1 and divide by the result of raising the power, in this case I’m assuming mm;

Area = \int_{4}^7 x^{2}dx \\ =\int_{4}^7x^{3}/3 \\ So \\ = (7^{3}/3 ) - (4^{3}/3 ) \\ = 114.333 - 21.333 \\ = 93mm^{2} \\

Let’ see this in action within a CAD model;

To do this we will create a Law where Y=X^2.

Notice that we multiply the result by 1000 to scale up the curve, since X is evaluated from 0 to 1.

Then we will create a sketch that contains a horizontal line 1000mm long, then from this create a parallel curve that uses the law previously defined.

Next we will add two points on the horizontal line at 400mm and 700mm from the origin.

Next we will create two vertical lines 1000mm long from each of the two points.

Finally a fill surface can be created to represent the area under the curve.

Now we will add an Area parameter and add the following formula to the parameter.

And finally we can use the equation we derived earlier.

So we now have an exact comparison between what was measured and what was calculated by our equation, cool.

So we know the function of the curve f(x) = X^2, actually we have f(x) = X^2 *1000 and we have to remember this. We want to create a point by coordinated that always intersects the curve.

We know that the curve is on the YZ plane in the CAD system, therefor X = 0mm , we will give Y an arbitrary value 300mm and to calculate Z well use the following equation;

We can see that the point lays on the curve, but notice in the equation we had to divide by 1m (one meter) since we squared the Y value and the original law was scaled by 1000. So this means that we can do a little more math to determine the slope of the curve at a given point.

So we have a curve that has a function f(x) = X^2 and we want to find a tangent line for a given X value, in this case we have a given X value of 300 and a Y value of 90, this is where we use Derivatives.

The first thing we have to do is calculate the Y value, remembering the division by 1000;

f(x) = x^{2} / 1000\\ f(300) = 300^{2} / 1000\\ f(300) = 90,000 / 1000 \\ Y = 90

Next step is to determine F-Prime of X f'(x)

f'(x) = 2x^{1} \\ f'(x) = 2x \\

Next well plug in our X coordinate into f'(x) which defines our slope number

f'(x) = 2x / 1000 \\ f'(300) = 2 \cdot 300 / 1000 \\ f'(300) = 600 /1000 \\ f'(300) = 0.6

And we can prove this in the CAD model, if we draw a tangent line through the point on the curve, and then divide the rise by the run;

( 205.798mm / 342.997mm ) / 1000 = 0.599999 \\ \approxeq 0.6

The last step is to use the point slope formula for a line.

Y-Y_1 = M(X-X_1) \\ Y-90 = 0.6(X-300) \\ Y-90 = 0.6X - 180 \\ Y = 0.6X -180 + 90 \\ Y = 0.6X - 90\\

Since we know the slope number 0.6 we can calculate the Angle. To do this we will create a right angled triangle based on the slope number and do some simple trigonometry to determine the angle;

Run = 10mm \\ Rise = 10 * 0.6 = 6mm \\ Hypotenuse = \sqrt{ 10^2 + 6^2} \\ Hypotenuse = 11.6619mm \\ Angle = Cos^{- 1}(10mm/11.6619mm) \\ Angle = 31.021 degrees

And here we have the the CAD representation of the angle.

]]>So far we have covered Point Loads and Distributed Loads on a Beam, we now need to look at Non-Uniform distributed loads.

1 **Tonnes in Kips** = 2.2046

Initially we will simplify this case and assume that the two loads are 20Kip and 0Kip creating a true triangle. Now it’s easy to convert to a point load this is achieved by multiplying (( 20kip * 18 ) / 2 ), the area of a triangle, which equals a point load of 180kip. However where is the point load, well there is a general rule of thirds that can be applied to determine the centroid of a triangle. So in this case we can say that the point load is at 18 / 3 = 6m.

Based on this we can calculate the resulting loads at **AY** and **BY**;

Load * Span / TotalSpan \\ AY = (180 * 6 ) / 18 \\ AY = 1080 / 18 \\ AY = 60 \\ BY = (180 * 12 ) / 18 \\ BY = 2160 / 18 \\ BY = 120 \\ Load = 60kip + 120kip = 180kip

However we have to take into account the remaining load the rectangular area. We know this area is (5Kip * 18) = 90Kip point load symmetrical, so we can add 45kip to each side. this results in a solution **AY** = 105Kip and **BY** = 165Kip.

In this case we have a non uniform load that is offset to the side. Using the same process we will break the non-uniform load into two loads a uniform load and a non-uniform triangular load.

Let’s start off with the non-uniform portion of the load using the thirds rule. (( 20kip * 9 ) / 2 ), the area of a triangle, which equals a point load of 90kip. Again we know that the centroid is 1/3 from the end of the triangle, or 3m.

Load * Span / TotalSpan \\ AY = (90 * 3 ) / 18 \\ AY = 270 / 18 \\ AY = 15 \\ BY = (90 * 15 ) / 18 \\ BY = 1350 / 18 \\ BY = 75 \\ Load = 15kip + 75kip = 90kip

Now we have to calculate the rectangular portion of the nonuniform distributed load, we know this area is (5Kip * 9) = 45Kip point load at 4.5m from the end.

Load * Span / TotalSpan \\ AY = (45 * 4.5 ) / 18 \\ AY = 202.5 / 18 \\ AY = 11.25 \\ BY = (45 * 13.5 ) / 18 \\ BY = 607.5 / 18 \\ BY = 33.75 \\ Load = 11.25kip + 33.75kip = 45kip

Now we know the loads at either end we can now add them together. **AY** = (15 + 11.25) = 26.25kip and **BY** = (75 + 33.75) = 108.75kip so as a final check lets make sure that the sum of the point loads equals the sum of the resultants: (90 + 45 ) = ( 26.25 + 108.75) 135 = 135 so its correct.

No if we follow this process we can resolve any complex problem.

Here is a more complex problem, lets walk through it to resolve the loads for each end. we can quickly do the math on each non uniform load to determine what each one is and the sum total so we can validate our answer. Left-hand load = (((20*9)/2) +( 9*5)) = 135. and Right-hand load = (((40*4.5)/2)+(5*4.5)) = 112.5, the total load is (135 + 112.5) = 247.5kip. Well use this to ensure a correct answer at the end.

Lets do the left-hand non-uniform load first, in this case I’m going to calculate the both areas at once the rectangle and the triangle.

Load * Span / TotalSpan \\ AY = ((((20*9)/2) * 3 ) / 18 ) + ((5*9) * 4.5) /18 \\ AY = (270 / 18) + (202.5 / 18) \\ AY = 15 + 11.25 \\ AY = 26.25 \\ BY = ((((20*9)/2) * 15 ) / 18 ) + ((5*9) * 13.5) /18 \\ BY = (90 / 18) +(354.375 / 18) \\ BY = 75 + 33.75 \\ BY = 108.75 \\ Load = 26.25kip + 108.75kip = 135kip

Now the right-hand non-uniform load;

Load * Span / TotalSpan \\ AY = ((((40*4.5)/2) * 15 ) / 18 ) + (((5*4.5) * 15.75) /18)\\ AY = (1350 / 18) + (354.375 / 18) \\ AY = 75 + 19.6875 \\ AY = 94.6875 \\ BY = ((((40*4.5)/2) * 3 ) / 18 ) + ((5*4.5) * 2.25) /18\\ BY = (270 / 18) +(50.625 / 18) \\ BY = 15 + 2.8125 \\ BY = 17.8125 \\ Load = 94.6875kip + 20.625kip = 112.5kip

Finally we can add the resultant loads together **AY** + **BY** = 247.5kip = 135kip + 112.5kip = 247.5kip

In a previous post we looked at resolving point loads on a beam in this post we will look at distributed loads.

In this example we will see how to resolve the resultant loads at A and B due to a 12Kips/Meter distributed load.

1 **Tonnes in Kips** = 2.2046

First we will create a free body diagram, to represent our problem.

Itâ€™s important to note that the **AX** load is 0kips in this case since no loads have any horizontal component.

In the case of a distributed load, we can convert the distributed load to a point load this is achieved by multiplying distributed load by the distance it’s applied across. Then positioning the point load at the midpoint of the uniform distributed load (as long as its truly uniform).

In our example we have an 18meter span with 12 kips / meter = 18 * 12 = 216kip point load at the mid point of the span.

Since its at the mid point of the span we can simply divide this by 2 to find the resultant loads at **AY** and **BY** = 108Kips.

In this case the distributed load is offset to one side lets look at how we resolve this.

Using the same logic previously applied to convert a uniform distributed load, we can do the same for this example, first we multiple the uniform distributed load by its span 12kips * 9 = 108kip point load at the mid point of the distributed load this is shown below;

We can now resolve the resultant forces at **AY** and **BY** using the point load method.

1 / ( 4.5 + 13.5 ) = 0.0555555 \\ 0.0555555 * 4.5 = 0.25 \\ 108kips * 0.2 = 27kips \\

And we know that **BY** = Total Point Load – **AY**, so **BY** = 81Kips.

We can also do the calculation by;

Load * Span / TotalSpan \\ AY = (108 * 4.5 ) / 18 \\ AY = 486 / 18 \\ AY = 27 \\ BY = (108 * 13.5 ) / 18 \\ BY = 1,458 / 18 \\ BY = 81 \\ Load = 27 + 81 = 108

In this case we have two uniform distributed loads lets see how we handle this;

Just as before we convert the uniform distributed loads to point loads;

Now we can do the calculations, lets start of with **AY**;

Load * Span / TotalSpan \\ AY1 = (108 * 4.5 ) / 18 \\ AY1 = 486 / 18 \\ AY1 = 27 \\ AY2 = (216 * 13.5 ) / 18 \\ AY2 = 2916 / 18 \\ AY2 = 162 \\ AY1 + AY2 = 189kips

Next **BY**;

Load * Span / TotalSpan \\ BY1 = (108 * 13.5 ) / 18 \\ BY1 = 1458 / 18 \\ BY1 = 81 \\ BY2 = (216 * 4.5 ) / 18 \\ BY2 = 972 / 18 \\ BY2 = 54 \\ BY1 + BY2 = 135kips

And as a final check 189 + 135 = 324kips

And as they say that is that, next post will cover Non-Uniform Distributed Loads.

]]>In this example we will see how to resolve the resultant loads at A and B due to the point load.

1 **Tonnes in Kips** = 2.2046

First we will create a free body diagram, to represent our problem.

It’s important to note that the **AX** load is 0kips in this case since no loads have any horizontal component.

We will use ratios to calculate the resulting loads AY and BY.

If the point load was centered along the span we know the loads at AY and BY are symmetrical and exactly half the value of the point load. We can use this logic of ratios to calculate the loads when the point load is not centered.

Lets do a calculation assuming the load is centered;

1 / ( 6 + 12 ) = 0.055555 \\ 0.055555 * ((6 + 12) / 2) = 0.5 \\ 12kips * 0.5 = 6kips \\

So we have a 6Kip load at AY and BY.

Back to our example let’s do** AY** first using the same equation;

1 / ( 6 + 12 ) = 0.055555 \\ 0.055555 * 6 = 0.333333 \\ 12kips * 0.333333 = 3.999996kips \\ \approxeq 4kips

and now **BY**;

1 / ( 6 + 12 ) = 0.055555 \\ 0.055555 * 12 = 0.666666 \\ 12kips * 0.666666 = 7.99992kips \\ \approxeq 8kips

We can verify the result by adding **AY** + **BY** which must equal the point load 12 Kips and indeed 4kips + 8kips = 12kips, success.

If we had multiple loads we would resolve for each one and total the loads up at the end for AY and BY, let’s try one.

Let’s Calculate **AY** first;

1 / ( 6 + 10 + 2) = 0.055555 \\ 0.055555 * 6 = 0.333333 \\ 12kips * 0.333333 = 3.999996kips \\ and \\ 0.055555 * (6 + 10) = 0.888888 \\ 24kips * 0.888888 = 21.33333kips \\ so \\ 3.999996 + 21.33333 = 25.333329kips \\ \approxeq 25.3kips

and now **BY**;

1 / ( 6 + 10 + 2) = 0.055555 \\ 0.055555 * 12 = 0.666666 \\ 12kips * 0.666666 = 7.99992kips \\ and \\ 0.055555 * 2 = 0.11111 \\ 24kips * 0.11111 = 2.66664kips \\ so \\ 7.99992 + 2.66664 = 10.66656kips \\ \approxeq 10.7kips

and again finally to test we can verify the result by adding **AY** + **BY** which must equal the total point load 36Kips and indeed 25.3kips + 10.7kips = 36kips, success.

Since we know that the sum of **AY** and **BY** must equal the sum of the point loads we can use this to our advantage.

If we calculate **AY** all we have to do to calculate **BY** is subtract **AY** from the sum of the point loads.

BY = \sum PointLoads - AY \\ BY = 36kips - 25.3kips \\ BY = 10.7kips

In this example we need to replicate the design feature to the other four locations, we could just redesign it which would be a lot of work. We could create a Power Copy and quickly replicate the feature to the other positions, but if we needed to make a change we would have to make the same change five times. We could make a User Defined feature, and instantiate it five times, then change the exposed parameters which would expedite design changes.

In this case were going to use Internal links to ensure we only have one source of truth.

To do this correctly it’s important to base the features design on an axis system. As you can see the sketches that define this feature are supported by the Axis System planes.

Let’s start of with one and the rest will be the same process.

Right Mouse Click on the Feature Body and select **Copy** from the contextual menu.

Right Mouse Click on ‘Shape’ and select **Paste Special** from the contextual menu.

In the ‘Paste Special’ dialogue window select **As Result With Link**, the pick ‘Ok’ to complete the command.

We now have an internally linked copy of the feature but its in the wrong location. We could do an axis to axis transformation, but this hides the copy and creates a clone in the new location, for small features you could argue this is ok. However for larger features this has a huge impact on data size and should be avoided at all cost, so we will look at a cleaner solution.

We can use the ‘Add Position’ method to correctly locate the linked copy of the feature. This is achieved by Right Mouse Clicking on the linked solid ( Not the Body) and selecting **Add Position** from the contextual menu.

This function will create a special Geometrical Set below the linked solid called ‘Positioning Set’. Within the Positioning Set a ‘Datum’ (Isolated) Axis System is created at the Center of Gravity of the Solid and Orthogonal to the Part. This is just a placeholder and can be replaced with our own axis system, to ensure a good parametric solution.

This process is basically identical to the Axis to Axis transformation we need to supply the ‘From’ and the ‘To’ Axis Systems into the Add Position geometrical set. We have two choices, we can either move the Axis Systems into the Positing Set for each instance (but we only have one ‘From’ Axis System), or we can use internal links again. For this example we will use internal links.

Let’s Copy the Axis System that the initial feature was design on.

There is an odd behavior with the ‘Positing Set’, we can’t just Paste Special Directly into the Positing Set, we have to ensure that it’s the Defined In Work Object first. To do this Right Mouse Click on the ‘Positing Set’ and select **Define In Work Object** from the contextual menu.

Again Right Mouse Click on the ‘Positing Set’ and select **Paste Special** from the contextual menu.

In the ‘Paste Special’ dialogue window select **As Result With Link**, the pick ‘Ok’ to complete the command.

We now have a linked copy of the Reference Axis System, the next step is to delete the Axis System the CAD system created for us.

To Delete the Axis System, Right Mouse Click on it and Select **Delete** from the contextual menu.

Using the same steps we can create a linked copy of the Target Axis System. Once the Defined In Work Object is set to the Copied Body, the Re-Positioned copy can be seen.

We can repeat the process for the other three locations, as shown below:

The final step is to group these feature branches into a single feature branch. To do this a new body was inserted and the five features added to it. Then a single split operation was created to trim the features to the supporting surface. It’s best practice to ensure that the first feature added is the feature that contains the definition for the feature and not one of the four linked copies.

]]>When we create geometry or consume geometry from another part, we have no idea what the Surface Normal orientation is, but who cares? If it’s our own geometry in our own part we tend not to care too much and live with the consequences during modification, and most of the time we don’t stop to think about why the child geometry offset the wrong way, we just fix it.

When it comes to geometry consumed from another part we tend to care a little more, but we never stop to ask why did the Surface Normal change, and again we just fix the problem. This is probably because it’s completely out of our own control, it’s somebody else’s geometry.

I’m going to show you how to control the Surface Normal’s orientation automatically.

If we look at this surface we can’t tell easily what it’s Normal orientation is, but we can use some simple geometry functions to do this.

Most CAD systems have the capability to invert a surface, this is not the same as changing the offset side during an offset operation or any other type of operation. The Invert creates a clone of the surface and inverts it’s Normal direction. In the case below we can see that the Invert operation shows us the resulting Surface’s Normal orientation and gives us the capability to reset it to the ‘Initial’ extruded Surface’s Normal orientation.

The other operation that can be used to visualize the Surface’s Normal orientation is the Join function. It’s used to concatenate many surface(s) together. When the ‘Preview’ button is selected the Surface’s Normal orientation is shown but not inverted. We can use either of these two geometrical functions as an additional step, either after creating a complex surface or consuming a surface from another part. This way, if the Surface’s Normal is inadvertently changed we have a geometrical step where we can intervene.

When creating key design surfaces in a design it’s always a good idea to create a join directly afterwards, because a Surface’s Normal can be inverted accidently by modifying the underlying geometry. For example, drawing a new profile clockwise, instead of anti-clockwise, and not using the replace function. Another opportunity for a Surface Normal to be changed during a parts update is due to angular inversion. This happens when the angle between two lines, for example, changes from 170 degrees to 190 degrees. If there was a plane generated from the two lines, its normal orientation will be flipped, then this change would cascade down the child parent relationships into the surface and may invert its Normal. So how do we control this automatically?

When looking at the topology of the surface, we can group the surface into one of two groups; Continuously (but not necessarily constant) Positive or Negative curvature, or Planar / S-Shaped. If the surface falls into the first category, for example a leading edge wing surface, we can automatically control it’s Normal by using the offset surface area rule.

Let’s look at some pseudo code to understand this:

Start: Get the Input Surface Create a 1mm offset of the Input Surface If the Surface Area of the input Surface is larger than the offset Surface Return the Input Surface Else Return an Invert of the Input Surface End:

Below is a working example of this, but we are hard coding the fact that we always want the Surface Normal to point outwards from the curvature of the surface. We could manually edit the code and change the greater than symbol to less than, but who wants to do this?

Completed Code.

/*Rule created by me 1/9/2021*/ // Get the Original Surface Let iSurface ( Surface ) iSurface = `Geometrical Set.1\Join.1` // Create an Offset of the Original Surface Let ioOffset ( Surface ) ioOffset = offset( iSurface , 1mm , True ) // Compare the Surface Areas Let iResultantSurface ( Surface ) iResultantSurface = iSurface If( area( iSurface ) > area( ioOffset )) { Set iResultantSurface = invert( iSurface ) } // Return the Result `Geometrical Set.1\Normal_Controlled_Surface` = iResultantSurface

So let’s take it one more step further. We have added a simple Boolean parameter that when set to true the Surface Normal will point outward from the surface’s curvature, and when set to False will point inwards towards the surface’s curvature. Then, in the code we expanded the ‘If Statement’ to handle the parameter. Now the user has the capability to choose which side should maintain the Surface Normal (we can see this in the video below).

The video shows that when the Surface Normal of the Join is changed, the offset will maintain its offset direction until the user changes the parameter from ‘True’ to ‘False’, or vice-versa.

Completed Code.

/*Rule created by me 1/9/2021*/ // Get the Original Surface Let iSurface ( Surface ) iSurface = `Geometrical Set.1\Join.1` // Get the Parameters Let iBoolean ( Boolean ) iBoolean = Normal_Orientation_to_Largest_Offset_Side // Create an Offset of the Original Surface Let ioOffset ( Surface ) ioOffset = offset( iSurface , 1mm , True ) // Compare the Surface Areas Let iResultantSurface ( Surface ) iResultantSurface = iSurface If( area( iSurface ) > area( ioOffset )){ Set iResultantSurface = ( iBoolean == True ) ? invert( iSurface ) ; iSurface } Else{ Set iResultantSurface = ( iBoolean == False ) ? invert( iSurface ) ; iSurface } // Return the Result `Geometrical Set.1\Normal_Controlled_Surface` = iResultantSurface

Let’s now look at the other group where the surface is either Planar or S-Shaped. This is a little harder as an offset surface will have the same surface area, so we have to add a reference geometry to aid in determining the Surface Normal orientation. For the reference geometry a good place to start is the center of gravity of the surface. This tends to be one side or another, otherwise you could use a point on the vehicle’s center line plane or on an adjoining part.

In the video below we can see that the Surface’s Normal is controlled by the Reference Point. And again, as before, if we changed the Surface Normal by editing the Join operation, this would have no affect on the Surface’s Normal orientation. It’s purely driven by the Reference point.

Completed Code.

/*Rule created by me 1/9/2021*/ // Get the Original Surface Let iSurface ( Surface ) iSurface = `Geometrical Set.1\Join.1` // Get the Sample Point Let iPoint ( Point ) iPoint = `Geometrical Set.1\Ref_Point` // Get the Parameters Let iBoolean ( Boolean ) iBoolean = `Closest to Reference Point` // Create an Offset of the Original Surface Let ioOffset ( Surface ) ioOffset = offset( iSurface , 1mm , True ) // Compare the Surface Areas Let iResultantSurface ( Surface ) iResultantSurface = iSurface If( distance( iSurface , iPoint ) < distance( ioOffset , iPoint )){ Set iResultantSurface = ( iBoolean == True ) ? invert( iSurface ) ; iSurface } Else{ Set iResultantSurface = ( iBoolean == False ) ? invert( iSurface ) ; iSurface } // Return the Result `Geometrical Set.1\Normal_Controlled_Surface` = iResultantSurface

We’ve all created surfaces that don’t like to be offset, so how do we handle this case? Normally somewhere on the surface is a simpler area, so we can leverage the inheritance rule to our advantage. We know if we were to cut out a little piece of the surface, the resulting surface would inherit the parents Surface Normal orientation. So we can change our code to consume a point on the surface that then creates a small circular sample of the surface with a 1mm radius. We will then test this surface by offsetting it and applying the result back to the parent surface. Again, the surface must have a different surface area when it’s offset so the sample point cannot be on a planar portion of the complex surface.

In the image below I have also shown the sample point and the resulting sample patch and offset.

Complete Code…

/*Rule created by me 1/9/2021*/ // Get Test Geometrical Set Let ioTestGeoSet ( OpenBodyFeature ) // Get the Original Surface Let iSurface ( Surface ) iSurface = `Geometrical Set.1\Join.1` // Get the Parameters Let iBoolean ( Boolean ) iBoolean = `Maintain Normal to Largest Surface Area` // Get the Sample Point Let iPoint ( Point ) iPoint = `Geometrical Set.1\Sample_point` // Create the Sample Patch Let ioPlane ( Plane ) ioPlane =planetangent( iSurface , iPoint ) Let ioCircle ( Circle ) ioCircle = circleCtrRadius( iPoint , ioPlane , 1mm , 1 , 0deg , 180deg ) Let ioProject ( Curve ) ioProject = project( ioCircle , iSurface , NULL ) Let ioSplitSurface ( Surface ) ioSplitSurface = split( iSurface , ioProject , True ) If( distance( iPoint, ioSplitSurface ) <> 0mm ){ ioSplitSurface = split( iSurface , ioProject , True ) } // Create an Offset of the Original Surface Let ioOffset ( Surface ) ioOffset = offset( ioSplitSurface , 1mm , True ) // Compare the Surface Areas Let iResultantSurface ( Surface ) iResultantSurface = iSurface If( area( ioSplitSurface ) > area( ioOffset )){ Set iResultantSurface = ( iBoolean == True ) ? invert( iSurface ) ; iSurface } Else{ Set iResultantSurface = ( iBoolean == False ) ? invert( iSurface ) ; iSurface } // Return the Result `Geometrical Set.1\Normal_Controlled_Surface` = iResultantSurface

This code can always be wrapped up into a User Defined feature and the resulting template added to a catalog. So when we would we use a template like this? The first best place is directly after consuming geometry from another part. This will ensure that any geometry we define from this will always be correct and not impacted by another designer. Also, after defining a key design surface, we should control its Normal since it has the capability to impact our complete design. Finally, any geometry we want to share with other designers should be controlled. This is mostly out of design courtesy, but since we’re defining the topology it’s good practice to standardize the Surface Normal orientation, since were defining intent. This is true even though the consuming designer will probably control it’s orientation. Even if they don’t, at least we know we’re passing on stable geometry.

]]>I wanted to show you how you could navigate the CAD Part Structure to create a New Geometrical Set and then add a New Point into the New Geometrical Set.

The first step is to get the Root UI object, which is the Part Feature

Let ioPartFeature ( PartFeature ) Set ioPartFeature = GetRootUI()

Now we have the Part Feature we can get the VPMRepreference / Shape ( I will write a blog about these objects ). Here I have changed the V_Name to show you where we are in the specification tree, see image above.

Let ioVPMRepReference ( VPMRepReference ) ioVPMRepReference = GetPLMOwner( ioPartFeature ) ioVPMRepReference.V_Name = "RepRef - V_Name"

We can now step further up the tree to VPMCoreReference / Representation. Again I have changed the V_Name, see image above.

Let ioPLMCorereference( PLMCoreReference ) ioPLMCorereference = ioVPMRepReference.AggregatingReference ioPLMCorereference.V_Name = "CoreRef - V_Name"

We can now filter all the children below the Part Feature specifically looking for a Geometrical Set ( OpenBodyFeature ) called Points. If the list that is returned by the Filter function is empty we will create a New Geometrical Set and Append it to the list.

Let ioGeoSetList ( List ) ioGeoSetList = ioPartFeature.Children->Filter( "OpenBodyFeature" , "x.Name==\"Points\"" ) If ( ioGeoSetList ->Size() == 0 ) { Let ioGeoSet ( OpenBodyFeature ) ioGeoSet = new("OpenBodyFeature" , "Points" , ioPartFeature ) ioGeoSetList ->Append( ioGeoSet ) }

Finally we can create a New Point in the Geometrical Set.

Let ioPoint ( Point ) ioPoint = new("Point" , "Point.1" , ioGeoSetList[ 1 ] ) ioPoint = point( 10mm , 20mm , 30mm )

Its important to know that the New function will create a New feature every time the code is executed, if a feature already exists with the same name within the container the feature is updated, so any child geometry will be preserved. Its important to understand that if you use a loop to build many points or lines or… if the loop executed many time with varying loop iterations when the loop iterations are larger than the first or a previous execution then additional New features will be created. If the loop iterations is smaller the existing features will be updated but the excess features will not be deleted, you will have to write code to delete the extraneous features. This is shown below we can see there are 10 points in the specification tree created from a previous execution. The new execution is only looping 5 times and we can see that the five points have been modified and require an update, the last five points have not been edited.

Complete Code.

/*Rule created by me 1/8/2021*/ Let ioPartFeature ( PartFeature ) Set ioPartFeature = GetRootUI() Let ioVPMRepReference ( VPMRepReference ) ioVPMRepReference = GetPLMOwner( ioPartFeature ) ioVPMRepReference.V_Name = "RepRef - V_Name" Let ioPLMCorereference( PLMCorereference ) ioPLMCorereference = ioVPMRepReference.AggregatingReference ioPLMCorereference.V_Name = "CoreRef - V_Name" Let ioGeoSetList ( List ) ioGeoSetList = ioPartFeature.Children->Filter( "OpenBodyFeature" , "x.Name==\"Points\"" ) If ( ioGeoSetList ->Size() == 0 ) { Let ioGeoSet ( OpenBodyFeature ) ioGeoSet = new("OpenBodyFeature" , "Points" , ioPartFeature ) ioGeoSetList ->Append( ioGeoSet ) } Let ioPoint ( Point ) ioPoint = new("Point" , "Point.1" , ioGeoSetList[ 1 ] ) ioPoint = point( 10mm , 20mm , 30mm )

Many parametric CAD systems have the capability to allow the user to create and manage their own parameters. Which can then be used to drive the CAD parameters through simple formulas X = Y or more complex formulas that use standard algebraic constructs i.e. PI, Square Root, Sin, Cos etc. Many CAD systems often have other ways to drive parameter values though objects that contain complex code. Many of these objects have behaviors that can be Synchronous or Asynchronous, prior to update, or executions driven by specific behaviors like cut, copy, paste, save close etc. These objects can also interact directly or indirectly with each other through parameters or even directly through geometry.

Below is as example of creating a parameter of type length with a value of 0mm.

Parameters don’t have to be numeric in nature they can also be geometric in nature, in the case shown below a datum surface with no geometric description can be generated through the same manor. It’s important to understand that since no geometric description has been defined the surface can not be visualized. A simple geometric description could be an extrude, sphere or cylinder, and this description has to be applied either by direct equation Surface1 = Extrude1 or via and object that fully defines the surface.

The following code creates a geometrical description for a cylindrical extrude surface. If we think about a cylinder its made from a circle that is extruded along a direction by a value. The circle is defined by a center point, supported by a plane with a given radius. These steps are shown below, we can see that we first define the center point of the circle, then the support plane. The circle can then be defined, which in combination with a direction defines the extrusion. In the very last step we simply equate the parametric surface to extruded surface, this is how the surfaces geometric definition could be defined using a code based object.

/*Rule created by me 1/7/2021*/ Let ioPoint ( Point ) ioPoint = point( 10mm, 20mm , 30mm ) Let ioPlane ( Plane ) ioPlane = plane( 0 , 0 , 1 , 30mm ) Let ioCircle ( Circle ) ioCircle = circleCtrRadius( ioPoint , ioPlane , 20mm , 1 , 0deg , 180deg ) Let ioDirection ( Direction ) ioDirection = direction( ioPlane ) Let ioExtrude ( Surface ) ioExtrude = extrude( ioCircle , ioDirection , 0mm , 50mm , True ) `Geometrical Set.1\Surface.1` = ioExtrude

We can now see that the parametric surface has been rendered, this is because the rendering engine knows how to render the object based on its geometrical definition.

So why would we just not create it through simple geometrical methods? Great Question. in this case its a simple shape but if we combine this with additional user define parameters we can control this parametrically. In the image below we can see that additional parameters have been created and are referenced in the scripting object, instead of hard coded values.

We could take it one step further and add control logic to the scripting object, in this case a simple If Else If block. Allowing the user to change the orientation of the object orthographically.

The last step would be to package this up into a custom feature that could be used like any other parametric feature. Typically CAD systems will allow users to create custom features and then publish them into catalogues or add them into custom toolbars so the end user may not even know that its a custom feature. These types of features will auto implement all of the other out of the box coded behaviors, such as cut, copy, paste etc., so the custome user feature will completely behave like any other out of the box feature.

Final Code.

/*Rule created by me 1/7/2021*/ Let ioPoint ( Point ) ioPoint = point( X , Y , Z ) Let ioPlane ( Plane ) If( Orientation == "X" ){ ioPlane = plane( 1 , 0 , 0 , X ) } Else If( Orientation == "Y" ){ ioPlane = plane( 0 , 1 , 0 , Y ) } Else { ioPlane = plane( 0 , 0 , 1 , Z )} Let ioCircle ( Circle ) ioCircle = circleCtrRadius( ioPoint , ioPlane , Cylinder_Radius , 1 , 0deg , 180deg ) Let ioDirection ( Direction ) ioDirection = direction( ioPlane ) Let ioExtrude ( Surface ) ioExtrude = extrude( ioCircle , ioDirection , 0mm , Cylinder_Length , True ) `Geometrical Set.1\Surface.1` = ioExtrude

To do this we will cerate an Action, that has three inputs. These will be an input Geometrical Set, an input Message to be displayed for Logging and an input Boolean to define if the input Message should be logged.

First we’ll check to see if the Geometrical Set contains any features by getting the count of the children, using the Size method. if there are any children, we log the number of elements being deleted if the logging is turned on ( if the input Boolean is True), using the input Message. Then we’ll loop through each feature, deleting each one in turn.

/* Action created by me 1/24/2020 */ Let GeoSetFeature ( Feature ) If iGeoSet.Children->Size() > 0 { If iTracingOnOff == True{Trace( 3,iMessage, iGeoSet.Children->Size())} For GeoSetFeature inside iGeoSet.Children { GeoSetFeature.Delete() } }

So the problem is we have a series of lines and a reference AxisSystem, we want to sort these from left to right and bottom to top, in respect to the AxisSystem. The problem is some of the lines are colinear, and we want to ensure that the solution can handle multiple colinear lines.

We know that the Z Axis will always be perpendicular to the plane that the lines exist on. So how do we do this.

To do this we will cerate an Action, that has two inputs and one output. These will be an Input List of lines to sequence, the reference AxisSystem and an Output List of Lines.

The first thing which is best practice is to create a comments section for the inputs and outputs and what the Actions name will be, this will help future development.

/* Action created by me 1/6/2021 */ // iList : List // iAxisSystem : AxisSystem // oList : List // SortLinestoAxisSystem( iList : List , iAxisSystem : AxisSystem) : oList

We need to retrieve the origin of the AxisSystem to do this we will create 12 real variables and pass them into the **GetDirectionOfAxis** method which is a function belonging to the AxisSystem this will update these variables into four groups; Three variables for the Axis Origin, Three variables for the X Axis vector, Three variables for the Y Axis vector and Three variables for the Z Axis vector.

We can then create our own point from the Three variables for the Axis Origin, but they have to be converted from Real to Length by multiplying by 1mm.

// Get AxisSystem Origin Let ioXO , ioYO , ioZO , ioIX , ioIY , ioIZ , ioJX , ioJY , ioJZ , ioKX , ioKY , ioKZ ( Real ) iAxisSystem->GetDirectionOfAxis(ioXO , ioYO , ioZO , ioIX , ioIY , ioIZ , ioJX , ioJY , ioJZ , ioKX , ioKY , ioKZ) Let ioAxisSystemOrigin ( Point ) ioAxisSystemOrigin = point( ioXO * 1mm , ioYO * 1mm , ioZO * 1mm)

In the next section of code we’ll create three planes, one that is coplanar to the lines in the list, one that is perpendicular to the lines in the list and a final plane that is perpendicular to the first two planes.

Let ioRefLine1 , ioRefLine2 ( Line ) ioRefLine1 = iList[ 1 ] ioRefLine2 = iList[ iList->Size() ] Let ioBasePlane , ioRefPlane1 , ioRefPlane2 ( Plane ) ioBasePlane = plane( centerofgravity( ioRefLine1 ) , ioRefLine2 ) Let ioDirectionLine ( Line ) ioDirectionLine = lineangle( ioRefLine1 , ioBasePlane , ioAxisSystemOrigin , True , 0mm , 1000mm , 90deg , True) ioRefPlane1 = planenormal( ioDirectionLine , ioAxisSystemOrigin ) ioRefPlane2 = planenormal( ioRefLine1 , ioAxisSystemOrigin )

The next section allows us to group the elements in the list by a distance defined from the center point of a line to the reference plane that is parallel to the lines and perpendicular to the plane that is coplanar to the lines, this will naturally sub-group the coplanar lines. This is achieved by adding two length parameters to each line with a rounded distance from the lines center point back to the two reference planes (the second length parameter will be used later). Once we loop through each line adding the two length parameters and setting their values, we can sort the list into new list where the length measurement is increasing i.e. the list will start with the closest line to the AxisSystem and the last line will be the line that is furthest from the AxisSystem.

**Note: – There is a bug in the CAD system, when comparing length parameters that are not whole numbers. To avoid this bug in the second sort operation, the initial distance values are rounded to 0 decimal places.**

Let ioIndex ( Integer ) ioIndex = 1 Let ioLine ( Line ) For ioIndex While ioIndex<= iList->Size() { Let ioCurrentValue, ioInlineValue ( Length ) ioLine = iList[ ioIndex ] ioCurrentValue = round(distance( ioRefPlane1 , centerofgravity( ioLine )) , "mm" , 3 ) ioInlineValue = round(distance( ioRefPlane2 , centerofgravity( ioLine )) , "mm" , 3 ) ioLine->SetAttributeDimension( "RefPosition" , ioCurrentValue , "LENGTH" ) ioLine->SetAttributeDimension( "RefInLine" , ioInlineValue , "LENGTH" ) } Let ioInitalSort ( List ) ioInitalSort= iList ->Sort( "<" , "Line" , "Length" , "y=x->GetAttributeReal(\"RefPosition\")" )

In the last section we will loop through the lines looking for Sub-Groups, if a Sub-Group is found then the Sub-Group is sorted again using the second length parameter. Then the newly sorted Sub-Group is added to the output list, when there is a Sub-Group, the looping index is incremented by the number of elements in the Sub-Group. If there is no Sub-Group just a single line found, just the single line is added to the output list, and the index incremented by one (done naturally by the For While loop).

ioIndex = 1 oList->RemoveAll() For ioIndex While ioIndex<= ioInitalSort->Size() { ioLine = ioInitalSort[ ioIndex ] Let ioRefPositionValue ( Length ) ioRefPositionValue = ioLine->GetAttributeReal("RefPosition")*1000mm Let ioInLineList ( List ) Let ioQueryString ( String ) ioQueryString = "x->GetAttributeReal(\"RefPosition\") ==" + ioRefPositionValue + "" ioInLineList = ioInitalSort->Filter("Line", ioQueryString) if( ioInLineList->Size() == 1) { oList = oList + ioInLineList } Else { Let ioSequencedSubList ( List ) ioSequencedSubList = ioInLineList->Sort( "<" , "Line" , "Length" , "y=x->GetAttributeReal(\"RefInLine\")" ) oList = oList + ioSequencedSubList } ioIndex = ioIndex + ioInLineList->Size() }