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 4 steps:

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

This tutorial takes approximately 40 minutes to complete. Good luck!

1. Prepare Components

For this project we will create 3 components: Beam, Load Tester & Assembly. You can start with a new project 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:
export interface Properties {
depth: number
height: number
width: number
profileType: string

webThickness: number,
flangeThickness: number,

hollow: boolean,
pipeThickness: number,
}

export const PROFILE = {
TYPE_I: 'profile-type-i',
TYPE_O: 'profile-type-o',
}
  • then in COMPONENT, you should have:
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,
}

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()as:
generateGeometry() {
const model = GEOM3D.generateModel(this.properties)
// ...
}
  • 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 refPlane = new SKYCAD.Plane(1, 0, 0, 0) // perpendicular to x-axis, so the largest beam dimension is parallel to it.
model.addExtrude(profileSketch, refPlane, width)

return model
}

Great! You can create a preset for each type to easily switch between profiles.

tutorial-configurators-beam

  • Save & Update & Publish your _Beam_ component.

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).

Properties

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

  • In COMPONENT, you could try:
this.properties = {
depth: 80,
height: 100,
width: 300,
}

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.properties
const geometryGroup = new SKYCAD.GeometryGroup()

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

return geometryGroup
}
  • Save & Update & Publish your _Load Tester_ component.

tutorial-configurators-load-tester

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.

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:
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,
}

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

Remember to add this.update() to call it when the component is created in the presets. Also, you could add again the interface Properties & const PROFILE in CONSTANTS, same as in Beam.

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.properties

// 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.

tutorial-configurators-assembly

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:

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 profileTypeParameter = SKYPARAM.generateDropdown(PARAMETER.PROFILE_TYPE, 'Profile type', [
new SKYPARAM.DropdownItem('Type I', CONSTANTS.PROFILE.TYPE_I),
new SKYPARAM.DropdownItem('Type O', CONSTANTS.PROFILE.TYPE_O),
])
profileTypeParameter.setValue(component.getProperty('profileType'))

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

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

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

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

const flangeThicknessParameter = SKYPARAM.generateSlider(PARAMETER.FLANGE_THICKNESS, 'Flange thickness', {
defaultValue: component.getProperty('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(component.getProperty('hollow'))

const pipeThicknessParameter = SKYPARAM.generateSlider(PARAMETER.PIPE_THICKNESS, 'Thickness', {
defaultValue: component.getProperty('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)

// [completion callback]

return configurator
}

If you added them as a list (configurableParameters here) to the new SKYPARAM.Configurator() correctly, you should see them as follows:

tutorial-configurators-parameters

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

export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
// [parameters]

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 {
// [parameters]

const configurator = new SKYPARAM.Configurator(configurableParameters)

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

// [completion 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!

export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
// [parameters]

const configurator = new SKYPARAM.Configurator(configurableParameters)

configurator.setUpdateRule(PARAMETER.WEB_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
// [visibility]

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) {
// [visibility]

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

configurator.setUpdateRule(PARAMETER.PIPE_THICKNESS, (value, directUpdate, parameter, values) => {
if (!directUpdate) {
// [visibility]

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)
}
}
})

// [other rules & completion callback]

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. To avoid bugs, you shouldn't update the properties (in addCompletionCallback()) with parameters that don't exist in the configurator in order, but for now we can leave it like this.

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.

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:

tutorial-configurators-moment

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:

tutorial-configurators-deflection

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 * width
const loadPositionX = 0.50 * width
const rightSupportPositionX = 0.75 * width

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 CONSTANTS.PROFILE.TYPE_I: return getAreaMomentInertiaForProfileI(depth, height, webThickness, flangeThickness)
case CONSTANTS.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 project dashboard: one as BendingMoment and the other as Deflection.

tutorial-configurators-secret-1

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
// [rest of function]
}
  • in Deflection, FORMULA:
export async function getDeflectionAtX(input: { load: number, x: number, width: number, profileType: string, depth: number, height: number, webThickness: number, flangeThickness: number, pipeThickness: number, hollow: boolean }) {
const { load, x, width, profileType, depth, height, webThickness, flangeThickness, pipeThickness, hollow } = input
// [rest of function]
}

function getAreaMomentOfInertia(properties) {
// [implementation]
}

function getAreaMomentInertiaForProfileO(profileWidth: number, profileHeight: number, hollow: boolean, pipeThickness: number) {
// [implementation]
}

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 them to be able to use them later in your components.

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.

tutorial-configurators-secret-2

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, width: 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 App Maker 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.properties
    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.properties
    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, 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. The function getMetrics() can now be used in the final application and it could look something like this 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 App Maker in the next tutorial User Interface.