Skip to main content

Configurators

In this tutorial, you will learn the basics of one of the most powerful features in DynaMaker: Configurators. Here the parameters play an important role together with the internal rules that make every custom product possible. As a product example, we will use a three-point load test in a beam to calculate automatically the bending moment & max deflection for a given cross-section & material.

For this we will follow 5 steps:

  1. Prepare components
  2. Parameters
  3. Rules
  4. Formulas
  5. Metrics

Don't forget to check the common mistakes if you get stuck. Good luck!

1. Prepare Components

For this project we will create 3 components: Beam, Load Tester & Assembly. You can start with a new app with 3 new components, as done in previous tutorials.

A. Beam

Let's start with the beam. It will contain 2 different profiles: I & O. Apart from being able to modify the main properties (width, depth & height), in the I-profile we should be able to modify the thickness of its web and flanges. For the latter, we could make it hollow like a pipe and also modify its thickness.

Ideally, it's better to make different components (I-Beam & O-Beam) since they will not have the same properties. For simplification in this tutorial, we will have all properties in the same component because they will use similar formulas for calculating the bending moment and deflection. Note that this might not be very scalable long-term speaking when there are e.g. 20 different types of beams, not only with different cross-sections but also with different geometry throughout their length.

This said, let's start with the properties.

Properties

  • In CONSTANTS, make sure you have the type of profile as a constant and include the properties types:
export enum PROFILE {
TYPE_I = 'profile-type-i',
TYPE_O = 'profile-type-o'
}

export interface Properties {
depth: number
height: number
width: number
profileType: PROFILE // with this, profileType can only be 'profile-type-o' or 'profile-type-i'

webThickness: number,
flangeThickness: number,

hollow: boolean,
pipeThickness: number,
}
What is enum?

You can think of enum as const but with the type you want. It doesn't need to be strictly either number or string. In this case PROFILE can only be either 'profile-type-i' or 'profile-type-o'. Later on you'll see why this is more useful than having it as const.

  • In API, export the CONSTANTS to be able to use PROFILE in other components. Make sure you have
export * from './component.js'
export * from './constants.js'
  • In COMPONENT, you should have:
export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> { // see that is CONSTANTS.Properties now
constructor() {
super()

this.properties = {
depth: 80,
height: 100,
width: 300,
profileType: CONSTANTS.PROFILE.TYPE_I,

// properties for type I:
webThickness: 10,
flangeThickness: 5,

// properties for type O:
hollow: true,
pipeThickness: 5,
}
}

generateGeometry(){
// logic to generate geometry
}
}

Notice that we moved interface Properties to CONSTANTS to clean the editor tab COMPONENT and to be able to use later in GEOM3D.

Good, let's create the profile as a sketch for each type of beam.

Profiles

In GEOM2D, you could create the following profile sketches:

export function generateProfileTypeISketch(profileWidth: number, profileHeight: number, webThickness: number, flangeThickness: number) {
const sketch = new SKYCAD.Sketch()

sketch.moveTo(0, 0)
sketch.lineTo(profileWidth, 0)
sketch.lineTo(profileWidth, flangeThickness)
sketch.lineTo((profileWidth + webThickness) / 2, flangeThickness)
sketch.lineTo((profileWidth + webThickness) / 2, profileHeight - flangeThickness)
sketch.lineTo(profileWidth, profileHeight - flangeThickness)
sketch.lineTo(profileWidth, profileHeight)
sketch.lineTo(0, profileHeight)
sketch.lineTo(0, profileHeight - flangeThickness)
sketch.lineTo((profileWidth - webThickness) / 2, profileHeight - flangeThickness)
sketch.lineTo((profileWidth - webThickness) / 2, flangeThickness)
sketch.lineTo(0, flangeThickness)
sketch.lineToId(0)
sketch.translate(-profileWidth / 2, 0)

return sketch
}

export function generateProfileTypeOSketch(profileWidth: number, profileHeight: number, hollow: boolean, pipeThickness: number) {
const sketch = SKYCAD.generateEllipseSketch(profileWidth / 2, profileHeight / 2)

if (hollow) {
const holeSketch = SKYCAD.generateEllipseSketch(profileWidth / 2 - pipeThickness, profileHeight / 2 - pipeThickness)
sketch.mergeSketch(holeSketch)
}

sketch.translate(0, profileHeight / 2)

return sketch
}

Let's use these sketches to create 3D geometry!

Geometry

We will use all the properties for the geometry and make the profile sketch used for the extrusion dependent on the type. For this:

  • In COMPONENT pass all properties to generateModel() for later. generateGeometry() could look like:
 generateGeometry() {
const properties = this.getProperties()

const model = GEOM3D.generateModel(properties)

const geometryGroup = new SKYCAD.GeometryGroup()
geometryGroup.addGeometry(model, {
materials: [new SKYCAD.Material({ color: new SKYCAD.RgbColor(83, 165, 199) })]
})

const layout = new SKYCAD.Layout()

const start = new SKYMATH.Vector2D(properties.width, 0)
const end = new SKYMATH.Vector2D(0, 0)
layout.addDimension(start, end, { decimals: 0 })

geometryGroup.addGeometry(layout)

return geometryGroup
}
  • In GEOM3D, make sure you choose the right profile for each type:
export function generateModel(properties: CONSTANTS.Properties) {
const { depth, height, width, profileType, webThickness, flangeThickness, hollow, pipeThickness } = properties
const model = new SKYCAD.ParametricModel()

let profileSketch: SKYCAD.Sketch
switch (profileType) {
default:
case CONSTANTS.PROFILE.TYPE_I:
profileSketch = GEOM2D.generateProfileTypeISketch(depth, height, webThickness, flangeThickness)
break
case CONSTANTS.PROFILE.TYPE_O:
profileSketch = GEOM2D.generateProfileTypeOSketch(depth, height, hollow, pipeThickness)
break
}

const plane = new SKYCAD.Plane(1, 0, 0, 0) // perpendicular to x-axis, so the largest beam dimension is parallel to it.
model.addExtrude(profileSketch, plane, width)

return model
}
  • Save & Update & Publish your Beam component.
  • Optionally create presets each type to easily switch between profiles (review My First Component if you don't remember).

B. Load Tester

As for the three-point load tester, we will simplify the machine by creating a model that consists of 2 supports, one at 1/4 and the other at 3/4 of the beam length, and the load head where the force is applied from. All these parts are supposed to be cylindrical so they ideally make contact with the beam at one point (in x-axis). Now that we are done with the component Beam, let's switch to the second component we created LoadTester.

Properties

We could change the default values to make it clearer how the parts are placed.

  • Back in the app dashboard, go into the component LoadTester.
  • In COMPONENT, you could try:
this.properties = {
width: 300,
depth: 80,
height: 100,
}

Geometry

We start by creating the models and then adding them to the geometry.

  • In GEOM3D, add a model for the supports and another one for the head or middle point:
const CYLINDER_RADIUS = 20

export function generateMiddlePointLoadModel(depth: number) {
const model = new SKYCAD.ParametricModel()

// cylinder
model.addExtrude(
SKYCAD.generateCircleSketch(0, CYLINDER_RADIUS / 2, CYLINDER_RADIUS),
new SKYCAD.Plane(0, 1, 0, -depth / 2),
depth,
)

// load cell connection
const headThickness = 15
const headHeight = 60
model.addExtrude(
SKYCAD.generateRectangleSketch(-headThickness / 2, -headThickness / 2, headThickness, headThickness),
new SKYCAD.Plane(0, 0, 1, CYLINDER_RADIUS / 2),
headHeight,
)

return model
}

export function generateSidePointLoadModel(depth: number) {
const model = new SKYCAD.ParametricModel()

model.addExtrude(
SKYCAD.generateCircleSketch(0, -CYLINDER_RADIUS / 2, CYLINDER_RADIUS),
new SKYCAD.Plane(0, 1, 0, -depth / 2),
depth,
)

return model
}
  • In COMPONENT, add these models to the geometry as:
generateGeometry() {
const { width, depth, height } = this.getProperties()
const geometryGroup = new SKYCAD.GeometryGroup()

const middlePointLoadModel = GEOM3D.generateMiddlePointLoadModel(depth)
const sidePointLodModel = GEOM3D.generateSidePointLoadModel(depth)

geometryGroup.addGeometry(middlePointLoadModel, {
position: new SKYMATH.Vector3D(0.5 * width, 0, height),
materials: [new SKYCAD.Material({ color: 0x555555 })],
})
geometryGroup.addGeometry(sidePointLodModel, {
position: new SKYMATH.Vector3D(0.25 * width, 0, 0),
materials: [new SKYCAD.Material({ color: 0x555555 })],
})
geometryGroup.addGeometry(sidePointLodModel, {
position: new SKYMATH.Vector3D(0.75 * width, 0, 0),
materials: [new SKYCAD.Material({ color: 0x555555 })],
})

return geometryGroup
}
  • Save & Update & Publish your Load Tester component.

Dimensions were added to the picture to help you understand the size.

C. Assembly

The Assembly component will just consist of both components Beam & Load Tester. It will also include the main configurator that will be used in the application, meaning its parameterS will update the properties of the subcomponents.

Import Subcomponents

Make sure to import the components Beam & LoadTester into the Assembly component via edit imports...

Properties

The properties of the Assembly will be the same as the Beam, so we can copy them for now. In COMPONENT have the following:

export interface Properties {
depth: number
height: number
width: number
profileType: BEAM.PROFILE // see that you can access CONSTANTS of component Beam (see note below)

webThickness: number,
flangeThickness: number,

hollow: boolean,
pipeThickness: number,
}

export class Component extends STUDIO.BaseComponent<Properties> {
constructor() {
super()

this.properties = {
depth: 80,
height: 100,
width: 300,
profileType: BEAM.PROFILE.TYPE_I,

// properties for type I
webThickness: 10,
flangeThickness: 5,

// properties for type O
hollow: true,
pipeThickness: 5,
}

this.update() // subcomponents will be handled here
}

update() {
// logic to add in next section
}

generateGeometry() {
// logic to generate geometry
}
}

Remember to add this.update() to call it when the component is created in the presets. Also, it is very dangerous to share the same properties in two different components, this is why we copied the interface Properties again, so that Beam and Assembly don't have the exact same properties.

Remember that in order to be able to use a constant from Beam, everything in the Beam editor tab CONSTANTS has to be exported via its API. Make sure to have export * from './constants.js' in the Beam as shown previously.

Subcomponents

Add the logic for the subcomponents (clear, create, set & add) in update(), so that:

update() {
const { depth, height, width, profileType, webThickness, flangeThickness, hollow, pipeThickness } = this.getProperties()

// Beam
this.componentHandler.clear(BEAM.Component)
const beamComponent = new BEAM.Component()
beamComponent.setProperties({ depth, height, width, profileType, webThickness, flangeThickness, hollow, pipeThickness })
this.componentHandler.add(beamComponent)

// Load tester
this.componentHandler.clear(LOADTESTER.Component)
const loadTesterComponent = new LOADTESTER.Component()
loadTesterComponent.setProperties({ depth, height, width })
this.componentHandler.add(loadTesterComponent)
}

Geometry

Simply add the subcomponents geometry in the Assembly in the same way you did in previous tutorials:

generateGeometry() {
return this.componentHandler.generateAllGeometry()
}

If you published Beam & LoadTester correctly, you should see them when Save & Update.

2. Parameters

When designing a configurator, the workflow for the parameters must be consistent and sometimes identifying the top-level parameters could be a challenge to make a user-friendly app.

In this case, we have 8 properties and we can easily make a parameter for each. Let's add all of them for now. In the Assembly component, go to PARAMETERS and create slider or dropdown parameters accordingly:

The configurator with these parameters should look like:

Try yourself first and compare your result with this gerateConfigurator():
const PARAMETER = {
PROFILE_TYPE: 'profile-type-parameter',
WIDTH: 'width-parameter',
DEPTH: 'depth-parameter',
HEIGHT: 'height-parameter',
WEB_THICKNESS: 'web-thickness-parameter',
FLANGE_THICKNESS: 'flange-thickness-parameter',
HOLLOW: 'hollow-parameter',
PIPE_THICKNESS: 'pipe-thickness-parameter',
}

export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
const { profileType, width, depth, height, webThickness, flangeThickness, hollow, pipeThickness } = component.getProperties()

const profileTypeParameter = SKYPARAM.generateDropdown(PARAMETER.PROFILE_TYPE, 'Profile type', [
new SKYPARAM.DropdownItem('Type I', BEAM.PROFILE.TYPE_I),
new SKYPARAM.DropdownItem('Type O', BEAM.PROFILE.TYPE_O),
])
profileTypeParameter.setValue(profileType)

const widthParameter = SKYPARAM.generateSlider(PARAMETER.WIDTH, 'Width (x)', {
defaultValue: width,
maxValue: 500,
minValue: 100,
stepSize: 1,
unit: 'mm',
})

const depthParameter = SKYPARAM.generateSlider(PARAMETER.DEPTH, 'Depth (y)', {
defaultValue: depth,
maxValue: 200,
minValue: 10,
stepSize: 1,
unit: 'mm',
})

const heightParameter = SKYPARAM.generateSlider(PARAMETER.HEIGHT, 'Height (z)', {
defaultValue: height,
maxValue: 200,
minValue: 10,
stepSize: 1,
unit: 'mm',
})

const webThicknessParameter = SKYPARAM.generateSlider(PARAMETER.WEB_THICKNESS, 'Web thickness', {
defaultValue: webThickness,
maxValue: 100,
minValue: 1,
stepSize: 1,
unit: 'mm',
})

const flangeThicknessParameter = SKYPARAM.generateSlider(PARAMETER.FLANGE_THICKNESS, 'Flange thickness', {
defaultValue: flangeThickness,
maxValue: 100,
minValue: 1,
stepSize: 0.5,
unit: 'mm',
})

const hollowParameter = SKYPARAM.generateDropdown(PARAMETER.HOLLOW, 'Hollow', [
new SKYPARAM.DropdownItem('Yes', true),
new SKYPARAM.DropdownItem('No', false),
])
hollowParameter.setValue(hollow)

const pipeThicknessParameter = SKYPARAM.generateSlider(PARAMETER.PIPE_THICKNESS, 'Thickness', {
defaultValue: pipeThickness,
maxValue: 100,
minValue: 1,
stepSize: 1,
unit: 'mm',
})

const configurableParameters = [
profileTypeParameter,
widthParameter,
depthParameter,
heightParameter,
webThicknessParameter,
flangeThicknessParameter,
hollowParameter,
pipeThicknessParameter,
]

const configurator = new SKYPARAM.Configurator(configurableParameters)

// configurator callback to add

return configurator
}

Great! Now we need to update the properties according to the values of the parameters. This is usually done in the completion callback:

// PARAMETER keys

export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
// configurableParameters

const configurator = new SKYPARAM.Configurator(configurableParameters)

configurator.addCompletionCallback((valid, values) => {
if (valid) {
component.setProperties({
depth: values[PARAMETER.DEPTH],
height: values[PARAMETER.HEIGHT],
width: values[PARAMETER.WIDTH],
profileType: values[PARAMETER.PROFILE_TYPE],
webThickness: values[PARAMETER.WEB_THICKNESS],
flangeThickness: values[PARAMETER.FLANGE_THICKNESS],
hollow: values[PARAMETER.HOLLOW],
pipeThickness: values[PARAMETER.PIPE_THICKNESS],
})
}
})

return configurator
}

Notice that addCompletionCallback gives two arguments to use:

  • valid: as a boolean, true when all parameters are valid (e.g. within the max-min range)
  • values: as an object with the keys of all parameters, containing their values from the configurator.

You can try different values for the parameters and check that they are all correctly connected. However, you will immediately realize that sometimes some parameters are not needed (e.g. Hollow for a Type I profile) or their max-min range makes self-colliding shapes. This can be easily fixed with rules!

3. Rules

To set up a rule in a configurator you need to add a setUpdateRule() for each parameter you want a rule for. Let's start with the simplest: hiding those that are not needed depending on the profile, like:

export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
// configurableParameters

const configurator = new SKYPARAM.Configurator(configurableParameters)

configurator.setUpdateRule(PARAMETER.WEB_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(true); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(false); break
}
}
})
configurator.setUpdateRule(PARAMETER.FLANGE_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(true); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(false); break
}
}
})
configurator.setUpdateRule(PARAMETER.HOLLOW, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(false); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(true); break
}
}
})
configurator.setUpdateRule(PARAMETER.PIPE_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(false); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(values[PARAMETER.HOLLOW]); break
}
}
})

// configurator callback

return configurator
}

See that the callback needed in setUpdateRule() gives 4 arguments:

  • value: as the current value of the parameter.
  • directUpdate: as a boolean, true when the parameter is being updated (e.g. changing the value for slider or option for dropdown). Usually, you want to trigger the rule when something else is updated (so that directUpdate = false).
  • parameter: as the parameter with all its SKYPARAM properties (e.g. visibility, max/min values, etc).
  • values: as an object with the keys of all parameters above this parameter, so that the rules are always triggered downstream according to their order within the configurator.

Also, you need to remember that visibility doesn't mean that you remove the parameter, you simply hide it. That means that you will get its value if you try to read it, it will update the property connected to it accordingly and even make the configurator invalid if you forgot to update its value properly.

As for the sliders, you noticed that we need to update the max & min values for the thickness parameters for both profiles. For example, it makes no sense to have a web wider than the actual profile width and so on. Let's add some rules for those!

Try yourself first and compare your result with this updated gerateConfigurator():
const PARAMETER = {
PROFILE_TYPE: 'profile-type-parameter',
WIDTH: 'width-parameter',
DEPTH: 'depth-parameter',
HEIGHT: 'height-parameter',
WEB_THICKNESS: 'web-thickness-parameter',
FLANGE_THICKNESS: 'flange-thickness-parameter',
HOLLOW: 'hollow-parameter',
PIPE_THICKNESS: 'pipe-thickness-parameter',
}

export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
const { profileType, width, depth, height, webThickness, flangeThickness, hollow, pipeThickness } = component.getProperties()

const profileTypeParameter = SKYPARAM.generateDropdown(PARAMETER.PROFILE_TYPE, 'Profile type', [
new SKYPARAM.DropdownItem('Type I', BEAM.PROFILE.TYPE_I),
new SKYPARAM.DropdownItem('Type O', BEAM.PROFILE.TYPE_O),
])
profileTypeParameter.setValue(profileType)

const widthParameter = SKYPARAM.generateSlider(PARAMETER.WIDTH, 'Width (x)', {
defaultValue: width,
maxValue: 500,
minValue: 100,
stepSize: 1,
unit: 'mm',
})

const depthParameter = SKYPARAM.generateSlider(PARAMETER.DEPTH, 'Depth (y)', {
defaultValue: depth,
maxValue: 200,
minValue: 10,
stepSize: 1,
unit: 'mm',
})

const heightParameter = SKYPARAM.generateSlider(PARAMETER.HEIGHT, 'Height (z)', {
defaultValue: height,
maxValue: 200,
minValue: 10,
stepSize: 1,
unit: 'mm',
})

const webThicknessParameter = SKYPARAM.generateSlider(PARAMETER.WEB_THICKNESS, 'Web thickness', {
defaultValue: webThickness,
maxValue: 100,
minValue: 1,
stepSize: 1,
unit: 'mm',
})

const flangeThicknessParameter = SKYPARAM.generateSlider(PARAMETER.FLANGE_THICKNESS, 'Flange thickness', {
defaultValue: flangeThickness,
maxValue: 100,
minValue: 1,
stepSize: 0.5,
unit: 'mm',
})

const hollowParameter = SKYPARAM.generateDropdown(PARAMETER.HOLLOW, 'Hollow', [
new SKYPARAM.DropdownItem('Yes', true),
new SKYPARAM.DropdownItem('No', false),
])
hollowParameter.setValue(hollow)

const pipeThicknessParameter = SKYPARAM.generateSlider(PARAMETER.PIPE_THICKNESS, 'Thickness', {
defaultValue: pipeThickness,
maxValue: 100,
minValue: 1,
stepSize: 1,
unit: 'mm',
})

const configurableParameters = [
profileTypeParameter,
widthParameter,
depthParameter,
heightParameter,
webThicknessParameter,
flangeThicknessParameter,
hollowParameter,
pipeThicknessParameter,
]

const configurator = new SKYPARAM.Configurator(configurableParameters)

configurator.setUpdateRule(PARAMETER.WEB_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]

// visibility rule
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(true); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(false); break
}

// max rule
if (parameter.getVisible() === true) {
const maxValue = values[PARAMETER.DEPTH]
parameter.setMax(maxValue)
if (value > maxValue) parameter.setValue(maxValue)
}
}
})

configurator.setUpdateRule(PARAMETER.FLANGE_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]

// visibillity rule
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(true); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(false); break
}

// max rule
if (parameter.getVisible() === true) {
const maxValue = values[PARAMETER.HEIGHT] / 2 - 1
parameter.setMax(maxValue)
if (value > maxValue) parameter.setValue(maxValue)
}
}
})

configurator.setUpdateRule(PARAMETER.HOLLOW, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]

// visibility rule
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(false); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(true); break
}
}
})

configurator.setUpdateRule(PARAMETER.PIPE_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
const newProfileType = values[PARAMETER.PROFILE_TYPE]

// visibility rule
switch (newProfileType) {
case BEAM.PROFILE.TYPE_I: parameter.setVisible(false); break
case BEAM.PROFILE.TYPE_O: parameter.setVisible(values[PARAMETER.HOLLOW]); break
}

// max rule
if (parameter.getVisible() === true) {
const maxValue = Math.min(values[PARAMETER.DEPTH], values[PARAMETER.HEIGHT]) / 2 - 1
parameter.setMax(maxValue)
if (value > maxValue) parameter.setValue(maxValue)
}
}
})

configurator.addCompletionCallback((valid, values) => {
if (valid) {
component.setProperties({
depth: values[PARAMETER.DEPTH],
height: values[PARAMETER.HEIGHT],
width: values[PARAMETER.WIDTH],
profileType: values[PARAMETER.PROFILE_TYPE],
webThickness: values[PARAMETER.WEB_THICKNESS],
flangeThickness: values[PARAMETER.FLANGE_THICKNESS],
hollow: values[PARAMETER.HOLLOW],
pipeThickness: values[PARAMETER.PIPE_THICKNESS],
})
}
})

return configurator
}

See that the max value is only updated if the parameter is visible. This could be sometimes ambiguous and to make it clearer the right profile type could be checked instead of the visibility.

Also to avoid bugs, you shouldn't update the properties (in addCompletionCallback()) with parameters that don't exist (or are visible) in the configurator, but for now we can leave it like this.

A very common mistake is to mix properties with parameter values. In a rule, having newProfileType as the value that comes from the parameter could help you understand that this is not the property, which should come from component.getProperties() instead.

Great! All parameters are connected correctly to the properties and have proper ranges. All solutions given by the configurator should be possible and perhaps manufacturable. Now it's time to add some calculations for the three-point bending test.

There is an extensive guide with detailed examples of how rules can affect other configurators in the library SKYPARAM.

4. Formulas

Two of the most interesting outputs given by a three-point test are the bending moment and how much the beam bends vertically in the middle, also known as the max deflection. Since both outputs concern the beam and the load, we could create functions in the Assembly that return them.

A. Bending Moment

Let's start with the bending moment. We would need to calculate the bending moment at every distance or section of the beam depending on the forces (F as the load, Rᵣ & Rₗ as the reactions on the right and left supports) together with the distances (x starting from the left edge of the beam). But don't worry, here are the equations:

As you see the bending moment depends only on the beam length, the forces and where these are located. Then, in the Assembly component, we can write a function within COMPONENT (outside class Component), for example as:

function getBendingMomentAtX(load: number, x: number, beamLength: number) {
const leftSupportPositionX = 0.25 * beamLength
const loadPositionX = 0.50 * beamLength
const rightSupportPositionX = 0.75 * beamLength

const leftReaction = load / 2
const rightReaction = load / 2

let bendingMoment = 0
if (x <= leftSupportPositionX) {
bendingMoment = 0
} else if ((x > leftSupportPositionX) && (x <= loadPositionX)) {
bendingMoment = leftReaction * (x - leftSupportPositionX)
} else if ((x > loadPositionX) && (x <= rightSupportPositionX)) {
bendingMoment = leftReaction * (x - leftSupportPositionX) - load * (x - loadPositionX)
} else {
// bendingMoment = leftReaction * (x - leftSupportPositionX) - load * (x - loadPositionX) + rightReaction * (x - rightSupportPositionX)
bendingMoment = 0
}

return bendingMoment
}

I'm sure you did your calculations correctly and didn't copy the code above... Great! now let's go with the deflection.

B. Deflection

The deflection depends on the cross section (I, area moment of inertia), the material (E, Young's modulus) and the load. Again this is the summary of equations needed for the formula:

function getDeflectionAtX(load: number, x: number, beamLength: number, profileType: string, depth: number, height: number, webThickness: number, flangeThickness: number, pipeThickness: number, hollow: boolean ) {
const leftSupportPositionX = 0.25 * beamLength
const loadPositionX = 0.50 * beamLength
const rightSupportPositionX = 0.75 * beamLength

const L = rightSupportPositionX - leftSupportPositionX

const momentInertia = getAreaMomentOfInertia(profileType, depth, height, webThickness, flangeThickness, pipeThickness, hollow)
const youngModulus = 210 * 1e3 // [N/mm2], common steel at room temperature

const a = loadPositionX - leftSupportPositionX
const b = rightSupportPositionX - loadPositionX

let deflection: number
if ((x > leftSupportPositionX) && (x <= loadPositionX)) {
deflection = load * b * x * (L ** 2 - x ** 2 - b ** 2) /
(6 * L * youngModulus * momentInertia)
} else if ((x > loadPositionX) && (x <= rightSupportPositionX)) {
const termInBrackets = L / b * (x - a) ** 3 + (L ** 2 - b ** 2) * x - x ** 3
deflection = load * b * termInBrackets /
(6 * L * youngModulus * momentInertia)
}

return deflection
}

function getAreaMomentOfInertia(profileType: string, depth: number, height: number, webThickness: number, flangeThickness: number, pipeThickness: number, hollow: boolean) {
switch (profileType) {
case BEAM.PROFILE.TYPE_I: return getAreaMomentInertiaForProfileI(depth, height, webThickness, flangeThickness)
case BEAM.PROFILE.TYPE_O: return getAreaMomentInertiaForProfileO(depth, height, hollow, pipeThickness)
}
}

function getAreaMomentInertiaForProfileI(profileWidth: number, profileHeight: number, webThickness: number, flangeThickness: number) {
const webHeight = profileHeight - 2 * flangeThickness

const momentInertia =
webThickness / 12 * webHeight ** 3 +
profileWidth / 12 * (profileHeight ** 3 - webHeight ** 3)

return momentInertia // [mm⁴]
}

function getAreaMomentInertiaForProfileO(profileWidth: number, profileHeight: number, hollow: boolean, pipeThickness: number) {
const A = profileWidth / 2
const B = profileHeight / 2

let momentInertia: number
if (hollow) {
const a = A - pipeThickness
const b = B - pipeThickness
momentInertia = Math.PI / 4 * (A * B ** 3 - a * b ** 3)
} else {
momentInertia = Math.PI / 4 * (A * B ** 3)
}
return momentInertia // [mm⁴]
}

Great! Imagine that these formulas are confidential and you don't want any user to have access to them somehow. To "protect" them we can move them to the Secret Formulas section.

To have access to Secret Formulas, you need to have a monthly subscription to DynaMaker. If you don't have it, you can still read the following section to get a sense of its potential.

C. Secret Formulas

Let's protect these formulas of the bending moment and deflection from the end-user. For that, we can create a couple of secret formulas in the app dashboard: one as BendingMoment and the other as Deflection.

If you go into one of them, you will be in the Secret Formulas Maker. The code editor is much simpler and has two tabs within CODE:

  • Tests: where you can test your formula and make sure it's giving the correct output.
  • Formula: where the function with the formula will be placed.

So the idea now is to move all the functions from Assembly to their secret formula and add tests for the edge cases to make sure we get the correct output, so:

  • in BendingMoment, FORMULA:
export async function getBendingMomentAtX(input: { load: number, x: number, beamLength: number }) {
const { load, x, beamLength } = input
// logic
}
  • in Deflection, FORMULA:
export async function getDeflectionAtX(input: { load: number, x: number, beamLength: number, profileType: string, depth: number, height: number, webThickness: number, flangeThickness: number, pipeThickness: number, hollow: boolean }) {
const { load, x, beamLength, profileType, depth, height, webThickness, flangeThickness, pipeThickness, hollow } = input
// logic
}

function getAreaMomentOfInertia(properties) {
// logic
}

function getAreaMomentInertiaForProfileO(profileWidth: number, profileHeight: number, hollow: boolean, pipeThickness: number) {
// logic
}

Notice that the formulas need one input, then we need to wrap all the arguments in an object input. Also, since these formulas are run in the server, remember to type async before the definition of the function and export it to be able to use them later in your components.

Since the secret formulas are meant to be decoupled from the components, you can't import components into the formulas and therefore use some component constants that you might be exporting. Either use hard-coded values to start with or preferably pass them as inputs together with the rest of the properties needed.

Good! Now that we have a formula we want to test it to make sure we are getting the right output given a certain input. We use the Tests tab for that and it's very easy to use and most important debuggable!

  • in BendingMoment, TESTS, you could add the following 5 edge-case examples:
describe('getBendingMomentAtX', function () {
const load = 50
const beamLength = 300
it('should return 0 at x = 75 (left support)', async () => {
const result = await FORMULA.getBendingMomentAtX({ load, x: 75, beamLength })
expect(result).equal(0)
expect(result).to.not.be.greaterThan(0)
})
it('should return 25 at x = 76 (a bit to the right of the left support', async () => {
const result = await FORMULA.getBendingMomentAtX({ load, x: 76, beamLength })
expect(result).equal(25)
expect(result).to.be.greaterThan(0)
})
it('should return 1875 at x = 150 (center of beam)', async () => {
const result = await FORMULA.getBendingMomentAtX({ load, x: 150, beamLength })
expect(result).equal(1875) // max bending moment
})
it('should return 25 at x = 224 (a bit to the left of the right support)', async () => {
const result = await FORMULA.getBendingMomentAtX({ load, x: 224, beamLength })
expect(result).equal(25)
})
it('should return 0 at x = 225 (right support)', async () => {
const result = await FORMULA.getBendingMomentAtX({ load, x: 225, beamLength })
expect(result).equal(0)
})
})
  • Save & Update to see the status of the tests on the left.
  • Publish to be able to use the formula in your components.

Repeat a similar process for the Deflection formula and the tests you consider they are the edge cases. You can use the following test to calibrate your deflection formula correctly:

describe('getDeflectionAtX', function () {
const load = 50
const beamLength = 300
const depth = 100
const height = 100
const profileType = 'profile-type-i'
const webThickness = 50
const flangeThickness = 10
const hollow = undefined
const pipeThickness = undefined

it('should return -0.000027 at x = 150 (center beam)', async () => {
const result = await FORMULA.getDeflectionAtX({ load, x: 150, beamLength, depth, height, profileType, webThickness, flangeThickness, hollow, pipeThickness })
expect(result).to.be.approximately(-0.0000027, 0.0000001)
expect(result).to.be.lessThan(0)
})
})

These tests have a chainable language, you can read more about the syntax needed with examples here using most of the common chainable getters like equal, greaterThan and much more.

5. Metrics

Now that we have the formulas ready, we want to visualize their output somehow. For example, we can add the values dynamically to the top-right corner of the application. Although, we won't go through the UI Editor in this tutorial we want to make sure we have a function ready that gives the max bending moment & deflection from the Assembly. We will also show the difference between using secret formulas and not.

In Assembly COMPONENT, within class Component, create a function named e.g. getMetrics():

  • if secret formulas weren't used:

    getMetrics() {
    const { width, profileType, depth, height, webThickness, flangeThickness, pipeThickness, hollow } = this.getProperties()
    const LOAD = 50 // [N]
    const maxValuePosX = width / 2
    return {
    maxBendingMoment: getBendingMomentAtX(LOAD, maxValuePosX, width),
    maxDeflection: getDeflectionAtX(LOAD, maxValuePosX, width, profileType, depth, height, webThickness, flangeThickness, pipeThickness, hollow),
    }
    }
  • if secret formulas were used:

    • import both formulas BendingMoment & Deflection into the component as if they were subcomponents.
    • use the function from the formula and give it the right input:
    async getMetrics() {
    const { width, profileType, depth, height, webThickness, flangeThickness, pipeThickness, hollow } = this.getProperties()
    const LOAD = 50 // [N]
    const maxValuePosX = width / 2
    return {
    maxBendingMoment: await BENDINGMOMENT.getBendingMomentAtX({ load: LOAD, x: maxValuePosX, beamLength: width }),
    maxDeflection: await DEFLECTION.getDeflectionAtX({ load: LOAD, x: maxValuePosX, beamLength: width, profileType, depth, height, webThickness, flangeThickness, pipeThickness, hollow }),
    }
    }

    Again, since these functions are run in the server and therefore "protected", getMetrics() needs to be async and has to await for the async functions. Also, you should see the functions when typing BENDINGMOMENT or DEFLECTION if you export them correctly within the secret formula.

As a challenge for this tutorial, you could try to connect the load to a parameter and see how much both the max bending moment and deflection are affected. Also, changing the material (carbon fiber, balsa wood, etc) could be another feature to make sure you understand how to connect parameters with properties.


Congratulations! You have learned the basics of how to work with configurators and formulas. The function getMetrics() can now be used in the final application and it could look something like this for visualization purposes if we add some extra dimensions:


Now that you know the basics of components, drawings & configurators, it's time to go through the user interface of the application that uses all this and for that, we will go through the UI Editor in the next tutorial User Interface.