Skip to main content

SKYPARAM

This library is intended to handle the configuration model and its parameters.


Example​

As example, let's think about a configuration model for a T-shirt with 9 parameters (only the configuration model is shown, no visualization). Try it out!


Parameter​

Parameters represent the user interaction with the component for creating a good product.

These are all the available:

3 tips:
  1. Don't mix them up with Properties!​

Properties represent the values that a component can have. They are found in COMPONENTS within the constructor of each component, whose type is defined in a interface.

interface Properties {
material: CONSTANTS.MATERIAL
size: number
sleeveLength: number
style: number
mainColor: SKYCAD.RalColorCode
detailsColor: SKYCAD.RgbColor
logoColor: SKYCAD.RgbColor
logoName: string
hasPlayerNumber: boolean
}

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

this.properties = {
material: CONSTANTS.MATERIAL.LINEN,
size: 40,
sleeveLength: 20,
style: 3,
mainColor: 9005,
detailsColor: new SKYCAD.RgbColor(255, 50, 50),
logoColor: new SKYCAD.RgbColor(50, 50, 50),
logoName: '💡 SkyMaker',
hasPlayerNumber: true,
}
}

generateGeometry() {
/* ... */
}
}
  1. Unique parameter IDs​

Each parameter requires an ID to be able to use it. To simplify things, these IDs could match the property name (as it is done in the templates). However misspelling errors are frequent and it's best to have them unique or at least different from the property connected to. Example:

COMPONENTS
interface Properties {
width: number
}

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

this.properties = {
width: 123,
}
}

generateGeometry() {
/* ... */
}
}
PARAMETERS
const PARAMETER_ID = {
WIDTH = 'box-width',
}

const widthParameter = SKYPARAM.generateInputParameter(PARAMETER_ID.WIDTH, 'Width', {
defaultValue: component.getProperty('width'),
})

configurator.addCompletionCallback((validUserInput, values) => {
if (validUserInput) {
component.setProperties({
width: values[PARAMETER_ID.WIDTH], // values['box-width']
})
}
})
  1. Bundle properties​

It's ideal to map 1 parameter to 1 property to avoid confusions, but there could be cases where more than 1 parameter could be connected to a single property. An example of a door that contains data of a handle:

COMPONENTS
interface DoorProperties {
size: SKYMATH.Vector3D
color: SKYCAD.RalColor // color of the door
handleData: {
type: CONSTANTS.HANDLE_TYPE
color: SKYCAD.RalColor // color of the door handle
}
}

export class DoorComponent extends STUDIO.BaseComponent<DoorProperties> {
constructor() {
super()

this.properties = {
size: new SKYMATH.Vector3D(900, 50, 2100), // width = 900, thickness = 50, height = 2100
color: new SKYCAD.RalColor(9005), // color of the door
handleData: {
type: CONSTANTS.HANDLE_TYPE.CYLINDRICAL
color: new SKYCAD.RalColor(3000), // color of the door handle
}
}
}

generateGeometry() {
/* ... */
}
}
PARAMETERS
const size = component.getProperty('size')
const widthParameter = SKYPARAM.generateInputParameter('door-width', 'Width', {
defaultValue: size.x,
})
const thicknessParameter = SKYPARAM.generateInputParameter('door-thickness', 'Thickness', {
defaultValue: size.y,
})
const heightParameter = SKYPARAM.generateInputParameter('door-height', 'Height', {
defaultValue: size.z,
})

const { type: handleType, color: handleColor } = component.getProperty('handleData')
const handleTypeParameter = SKYPARAM.generateDropdown('door-handle-type', 'Handle type', typeOptions, {
defaultValue: handleType,
})
const handleColorParameter = SKYPARAM.generateInputParameter('door-handle-color', 'Handle color', {
defaultValue: handleColor,
})

configurator.addCompletionCallback((validUserInput, values) => {
if (validUserInput) {
component.setProperties({
size: new SKYMATH.Vector3D(values['door-width'], values['door-thickness'], values['door-height']),
handleData: {
type: values['door-handle-type'],
color: values['door-handle-color'],
},
})
}
})
const dropdownParameter = SKYPARAM.generateDropdown(
'material',
'Material',
[
new SKYPARAM.DropdownItem('Linen', CONSTANTS.MATERIAL.LINEN, { thumbnail: ASSETS.URLS.LINEN_JPG }),
new SKYPARAM.DropdownItem('Cotton', CONSTANTS.MATERIAL.COTTON, { thumbnail: ASSETS.URLS.COTTON_JPG }),
new SKYPARAM.DropdownItem('Lycra', CONSTANTS.MATERIAL.LYCRA, { thumbnail: ASSETS.URLS.LYCRA_JPG }),
],
{
defaultValue: component.getProperty('material'),
showOptionThumbnails: true,
},
)

Notice each option value could come from CONSTANTS, making a cleaner setup to avoid misspelling if reused somewhere else:

CONSTANTS
const MATERIAL = {
LINEN = 'linen',
COTTON = 'cotton',
LYCRA = 'lycra',
}

You can adjust its style, especially around the image, with some CSS snippets.

const dropdownWithInputParamater = SKYPARAM.generateDropdownWithInput(
'size',
'Size',
[
new SKYPARAM.DropdownItem('XL', 48),
new SKYPARAM.DropdownItem('L', 44),
new SKYPARAM.DropdownItem('M', 40),
new SKYPARAM.DropdownItem('S', 36),
],
{ defaultValue: component.getProperty('size') },
)

Input​

const inputParameter = SKYPARAM.generateInputParameter('sleeveLength', 'Sleeve length', {
defaultValue: component.getProperty('sleeveLength'),
minValue: 0,
maxValue: 50,
stepSize: 1,
unit: 'mm',
})

Slider​

const sliderParameter = SKYPARAM.generateSlider('style', 'Style', {
defaultValue: component.getProperty('style'),
minValue: 1,
maxValue: 7,
stepSize: 1,
})

RAL Color​

const ralColorParameter = SKYPARAM.generateRalParameter(
'mainColor',
'Main color',
[
new SKYPARAM.DropdownItem('Blue (RAL 5012)', 5012),
new SKYPARAM.DropdownItem('Yellow (RAL 1018)', 1018),
new SKYPARAM.DropdownItem('Black (RAL 9005)', 9005),
],
{ defaultValue: component.getProperty('mainColor') },
)

How to remove RAL colors​

In case you want to remove some of the default colors because of certain reasons (e.g. RAL 2005 is not manufacturable), you can remap the list of predefined RAL colors that come with the parameter, updating the parameter's colorMap from a list of RAL colors as string as:

const defaultRalMap = SKYPARAM.getDefaultRalMap()
const excludedRalList = ['1026', '2005']
const filteredRalMap = Object.keys(defaultRalMap)
.filter((key) => !excludedRalList.includes(key))
.reduce((obj, key) => {
obj[key] = defaultRalMap[key]
return obj
}, {})

const ralColorParameter = SKYPARAM.generateRalParameter(
'mainColor',
'Main color',
[
new SKYPARAM.DropdownItem('Blue (RAL 5012)', 5012),
new SKYPARAM.DropdownItem('Yellow (RAL 1018)', 1018),
new SKYPARAM.DropdownItem('Black (RAL 9005)', 9005),
],
{
defaultValue: component.getProperty('mainColor'),
colorMap: filteredRalMap,
},
)

How to add RAL colors​

In case you want to add some custom RAL colors because of certain reasons (e.g. RAL 4847 is manufacturable), you can remap the list of predefined RAL colors that come with the parameter, updating the parameter's colorMap with a combined list of default RAL colors together with a list of your own RAL colors in a object with their RGB code, as:

const defaultRalMap = SKYPARAM.getDefaultRalMap()
const customRalList = {
4847: '166, 115, 89',
1234: '250, 32, 165',
}
const combinedRalMap = { ...defaultRalMap, ...customRalList }

const ralColorParameter = SKYPARAM.generateRalParameter(
'mainColor',
'Main color',
[
new SKYPARAM.DropdownItem('Blue (RAL 5012)', new SKYCAD.RalColor(5012)),
new SKYPARAM.DropdownItem('Yellow (RAL 1018)', new SKYCAD.RalColor(1018)),
new SKYPARAM.DropdownItem('Black (RAL 9005)', new SKYCAD.RalColor(9005)),
],
{
defaultValue: component.getProperty('mainColor'),
colorMap: combinedRalMap,
},
)

Color Picker​

const colorPickerParameter = SKYPARAM.generateColorPickerParameter('detailsColor', 'Details color', [
{
title: 'Predefined options',
options: [
{ label: 'Red', value: new SKYCAD.RgbColor(225, 0, 0) },
{ label: 'Green', value: new SKYCAD.RgbColor(0, 225, 0) },
{ label: 'Blue', value: new SKYCAD.RgbColor(0, 0, 225) },
],
},
])
colorPickerParameter.setValue(component.getProperty('detailsColor')) // sets default value

Palette Color​

const paletteParameter = SKYPARAM.generatePaletteParameter('logoColor', 'Logo color', [
new SKYPARAM.DropdownItem('Light', new SKYCAD.RgbColor(200, 200, 200).toRgbNumber()),
new SKYPARAM.DropdownItem('Neutral', new SKYCAD.RgbColor(125, 125, 125).toRgbNumber()),
new SKYPARAM.DropdownItem('Dark', new SKYCAD.RgbColor(50, 50, 50).toRgbNumber()),
])
paletteParameter.setValue(component.getProperty('logoColor').toRgbNumber()) // sets default value

Text​

const textParameter = SKYPARAM.generateTextParameter('logoName', 'Logo name', component.getProperty('logoName'))

Checkbox​

The value given by a checkbox parameter is a boolean.

const checkboxParameter = SKYPARAM.generateCheckbox(PARAMETER_ID.HAS_PLAYER_NUMBER, 'Add player number?', {
defaultValue: component.getProperty('hasPlayerNumber'),
})

Configuration Model​

A configuration model allows us to customize the product through parameters. Since this is the key feature of all CAD product configurators done in DynaMaker, the workflow of the product configurator will depend heavily on them. Therefore, in order to have a good user experience (UX), one needs to design them accordingly (complemented with other UI items like tabs and buttons of course), including the rules that affect the parameters with one another, and most importantly their order.

To show the difference that having rules makes, here are two types of configuration models:

Configuration Model without rules​

A configuration model without rules is rare to have since you probably want to use the full potential of what they can offer. However, sometimes a configuration model with 1 to 3 parameters should be enough for certain parts of your product configurator. Here is an example of the previous Shirt configuration model that doesn't contain any rule. See how simple is despite having 9 parameters (its structure is explained later):

Show configuration model without rules:
const PARAMETER_ID = {
MATERIAL: 'material-parameter',
SIZE: 'size-parameter',
SLEEVE_LENGTH: 'sleeve-length-parameter',
STYLE: 'style-parameter',
MAIN_COLOR: 'main-parameter',
LOGO_COLOR: 'logo-parameter',
DETAILS_COLOR: 'details-parameter',
LOGO_NAME: 'logo-name-parameter',
HAS_PLAYER_NUMBER: 'has-player-number-parameter',
}

export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
const { material, size, sleeveLength, style, mainColor, logoColor, detailsColor, logoName, hasPlayerNumber } =
component.getProperties()

const dropdownParameter = SKYPARAM.generateDropdown(
PARAMETER_ID.MATERIAL,
'Material',
[
new SKYPARAM.DropdownItem('Linen', CONSTANTS.MATERIAL.LINEN),
new SKYPARAM.DropdownItem('Cotton', CONSTANTS.MATERIAL.COTTON),
new SKYPARAM.DropdownItem('Lycra', CONSTANTS.MATERIAL.LYCRA),
],
{ defaultValue: material },
)

const dropdownWithInputParamater = SKYPARAM.generateDropdownWithInput(
PARAMETER_ID.SIZE,
'Size',
[
new SKYPARAM.DropdownItem('XL', 48),
new SKYPARAM.DropdownItem('L', 44),
new SKYPARAM.DropdownItem('M', 40),
new SKYPARAM.DropdownItem('S', 36),
],
{ defaultValue: size },
)

const inputParameter = SKYPARAM.generateInputParameter(PARAMETER_ID.SLEEVE_LENGTH, 'Sleeve length', {
defaultValue: sleeveLength,
minValue: 0,
maxValue: 50,
stepSize: 1,
unit: 'mm',
})

const sliderParameter = SKYPARAM.generateSlider(PARAMETER_ID.STYLE, 'Style', {
defaultValue: style,
minValue: 1,
maxValue: 7,
stepSize: 1,
})

const ralColorParameter = SKYPARAM.generateRalParameter(
PARAMETER_ID.MAIN_COLOR,
'Main color',
[
new SKYPARAM.DropdownItem('Blue (RAL 5012)', 5012),
new SKYPARAM.DropdownItem('Yellow (RAL 1018)', 1018),
new SKYPARAM.DropdownItem('Black (RAL 9005)', 9005),
],
{ defaultValue: mainColor },
)

const paletteParameter = SKYPARAM.generatePaletteParameter(PARAMETER_ID.LOGO_COLOR, 'Logo color', [
new SKYPARAM.DropdownItem('Light', new SKYCAD.RgbColor(200, 200, 200).toRgbNumber()),
new SKYPARAM.DropdownItem('Neutral', new SKYCAD.RgbColor(125, 125, 125).toRgbNumber()),
new SKYPARAM.DropdownItem('Dark', new SKYCAD.RgbColor(50, 50, 50).toRgbNumber()),
])
paletteParameter.setValue(logoColor.toRgbNumber()) // sets default value

const colorPickerParameter = SKYPARAM.generateColorPickerParameter(PARAMETER_ID.DETAILS_COLOR, 'Details color', [
{
title: 'Predefined options',
options: [
{ label: 'Red', value: new SKYCAD.RgbColor(225, 0, 0) },
{ label: 'Green', value: new SKYCAD.RgbColor(0, 225, 0) },
{ label: 'Blue', value: new SKYCAD.RgbColor(0, 0, 225) },
],
},
])
colorPickerParameter.setValue(detailsColor)

const textParameter = SKYPARAM.generateTextParameter(PARAMETER_ID.LOGO_NAME, 'Logo name', logoName)

const checkboxParameter = SKYPARAM.generateCheckbox(PARAMETER_ID.HAS_PLAYER_NUMBER, 'Add player number?', {
defaultValue: hasPlayerNumber,
})

const configurator = new SKYPARAM.Configurator([
dropdownParameter,
dropdownWithInputParamater,
inputParameter,
sliderParameter,
ralColorParameter,
colorPickerParameter,
paletteParameter,
textParameter,
checkboxParameter,
])

configurator.addCompletionCallback((validUserInput, values) => {
if (validUserInput) {
component.setProperties({
material: values[PARAMETER_ID.MATERIAL],
size: values[PARAMETER_ID.SIZE],
sleeveLength: values[PARAMETER_ID.SLEEVE_LENGTH],
style: values[PARAMETER_ID.STYLE],
mainColor: values[PARAMETER_ID.MAIN_COLOR],
detailsColor: values[PARAMETER_ID.DETAILS_COLOR],
logoColor: values[PARAMETER_ID.LOGO_COLOR],
logoName: values[PARAMETER_ID.LOGO_NAME],
hasPlayerNumber: values[PARAMETER_ID.HAS_PLAYER_NUMBER],
})
}
})

return configurator
}

Configuration Model with rules​

So what could you do when you want to have a connection between parameters or that they can interact with each other (e.g. by disabling certain dropdown options, changing their max/min values, etc)? Use rules!

As an example we take the configuration models from the open template Electronic Cabinet:


In this case, we have up to 4 different configuration models:

  1. In tab Cabinet, for the three first parameters Width, Height and Depth.
  2. In tab Cabinet, for the two last parameters Min rail spacing and Cabinet color.
  3. In tab Modules, when adding a module through the flyout Add module.
  4. In tab Modules, when configuring a selected module through the button Configure.

Since UX is a key factor in every product configurator, we have created these 4 configuration models according to specific behaviors:

  1. To update the camera when updating the size.
  2. Not to update the camera when updating the rail spacing or color.
  3. Not to limit any module size parameter when creating a new one.
  4. To limit the width when re-configuring a selected module, so no extra collision detection needs to be made.

Once you have very clear what behaviors your product configurator should have, creating configuration models should be straightforward!

How To Create A Configuration Model​

In your component, under PARAMETERS, create a function that creates a configuration model from a list of parameters, like:

export function generateConfigurator1(component: COMPONENT.Component): SKYPARAM.Configurator {
// create parameters

const configurableParameters = [
// parameters are shown in the configurator with the same order as in this list
widthParameter,
heightParameter,
depthParameter,
]

const configurator = new SKYPARAM.Configurator(configurableParameters)

// add callback and rules

return configurator
}

You can keep adding more configuration models to your component as long as they are in different functions. Whatever you export in PARAMETERS must return a SKYCAD.Configurator, so that you can preview the configuration model in the component-editor (using the dropdown at the top-left corner). After creating the functions that generate configuration models, you should be able to preview them like this:

Update Properties From Parameters​

After creating the configuration model, use addUpdateCallback to enter this function instantly when updating a parameter, or use addCompletionCallback if you want a small time delay due to performance, user experience, etc. Provide a callback function that takes two arguments:

  • valid as a boolean gives true if all parameters have valid input (e.g. out of range value in an input parameter)
  • values with all the parameter values existing in the configuration model.
configurator.addCompletionCallback((valid, values) => {
if (valid) {
component.setProperties({
height: values[PARAMETER.HEIGHT],
width: values[PARAMETER.WIDTH],
depth: values[PARAMETER.DEPTH],
})
}
})

You can find the value of any parameter in values by matching it with the parameter ID. In order not to mix up the name of the property with the name of the parameter, you can create an object PARAMETER containing all parameter IDs as shown in the previous video, so that it is easier and safer to update properties accordingly.

Remember that component.setProperties() triggers automatically the function component.update(), which is empty in the default template. However, additional functions can be called within configurator.addCompletionCallback() by simply adding them after component.setProperties().

How To Use Configuration Model in UI​

Once we have the configuration model created, with or without rules, we would simply need to:

  • In the Component-editor tab PARAMETERS, make sure that the function is exported, by typing export before the function:

    export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
    // logic to create configurator
    }
  • In the Component-editor tab API, make sure that all from PARAMETERS is exported outside the component:

    export * from './parameters.js'
  • In the UI-editor tab ACTIONS, make sure to have a function that gets the component where the configuration model is located:

    export function getAssemblyComponent() {
    const componentHandler = manager.getComponentHandler()
    const assemblyComponent = componentHandler.getComponents(ASSEMBLY.Component)[0]!

    if (assemblyComponent === undefined) {
    throw new Error('No assembly component found')
    }

    return assemblyComponent
    }
  • In the UI-editor tab UI, get the assembly component from that action and send it as input to the function generateConfigurator(), so that the tab includes the configuration model as content, like:

    export function onInit() {
    const componentHandler = Studio.getComponentHandler()
    const newAssemblyComponent = new ASSEMBLY.Component()
    componentHandler.add(newAssemblyComponent)
    Studio.requestGeometryUpdate()

    const tab1 = new SKYUI.Tab({
    title: 'Configure',
    onInit: () => {
    const assemblyComponent = ACTIONS.getAssemblyComponent()
    const configurator = ASSEMBLY.generateConfigurator(assemblyComponent)
    Studio.setTabContent([configurator /*, configurator2, buttonA, etc*/])
    },
    })

    Studio.setTabs([tab1 /*, tab2, etc*/])
    }
  • As a last step, still in the UI-editor tab UI, create a new callback function after the configuration model, so that if valid, then update the geometry:

    export function onInit() {
    // create new assembly & update geometry

    const tab1 = new SKYUI.Tab({
    title: 'Configure',
    onInit: () => {
    const assemblyComponent = ACTIONS.getAssemblyComponent()
    const configurator = ASSEMBLY.generateConfigurator(assemblyComponent)
    configurator.addCompletionCallback((valid, values) => {
    // <-- this one
    if (valid) {
    Studio.requestGeometryUpdate()
    }
    })
    Studio.setTabContent([configurator])
    },
    })

    Studio.setTabs([tab1])
    }

Rules​

Usually some parameter values might get constrained due to others. In DynaMaker we use configuration model rules to update parameters according to others. However, there is a small difference in what a "rule" is depending on whether the parameters belong to the same configuration model or not. Then we have rules for:

Parameters within the same configuration model​

Use setUpdateRule to create a rule between multiple parameters of the same configuration model. In this case we can think of the following simple rules:

  1. The min value of height should be 0.5 times the width, and the max value of height should be 1.5 times the width.
  2. The depth option "Shallow (50 mm)" should be disabled when height is over 1000 mm".

Translating this into a few lines of code, we need a:

  • rule for the parameter Height:
configurator.setUpdateRule(PARAMETER.HEIGHT, (newValue, directUpdate, parameter, values) => {
if (!directUpdate) {
const currentWidth = values[PARAMETER.WIDTH]
const newHeight = parameter.getValue() // or newValue

// min value
const minValue = 0.5 * currentWidth
parameter.setMin(minValue)
if (newHeight < minValue) parameter.setValue(minValue) // (optional) if lower than the min, update its value to be minValue

// max value
const maxValue = 1.5 * currentWidth
parameter.setMax(maxValue)
if (newHeight > maxValue) parameter.setValue(maxValue) // (optional) if larger than the max, update its value to be maxValue
}
})
  • rule for the parameter Depth (might seem complex at first but what it does is resetting the options and keeps the old value if it is still available, otherwise it looks for the nearest option):
configurator.setUpdateRule(PARAMETER.DEPTH, (newValue, directUpdate, parameter, values) => {
if (!directUpdate) {
const currentHeight = values[PARAMETER.HEIGHT]
if (parameter.disabled || !parameter.visible) return // stops the rule here if parameter is disabled or hidden

const outdatedCurrentOption = parameter.getOption()
const newOptions = getDepthOptions(currentHeight) // see function in note below*
parameter.setItems(newOptions) // updates options but chooses the first option by default, regardless of its availability or the previous selected one

const updatedCurrentOption = parameter.getOptionByValue(outdatedCurrentOption.value)
const newOptionsValues = newOptions.map((option) => option.value)
if (updatedCurrentOption !== undefined) {
// it could happen that the outdated option is not in the options anymore

if (updatedCurrentOption.disabled) {
// checks if the updated option is disabled (e.g. "Shallow (50 mm)" can be disabled according to the height value)
const currentOptionId = newOptionsValues.indexOf(updatedCurrentOption.value)

for (let id = currentOptionId; id < newOptionsValues.length; id++) {
// goes through the options looking for the next available option in ascending order
const newOptionId = parameter.getOptionByValue(newOptions[id + 1].value).id
parameter.setOptionById(newOptionId)
const newOption = parameter.getOption()
if (!newOption.disabled) return
}

// for (let id = currentOptionId; id >= 0; id--) { // goes through the options looking for the next available option in descending order
// const newOptionId = parameter.getOptionByValue(newOptions[id - 1].value).id
// parameter.setOptionById(newOptionId)
// const newOption = parameter.getOption()
// if (!newOption.disabled) return
// }
} else {
parameter.setOptionById(updatedCurrentOption.id)
}
} else {
parameter.setValue(newOptionsValues[0]!) // extra logic can be added here to choose a specific default value, instead of the first available one
}
}
})
note

See that getDepthOptions() should be a function outside PARAMETERS.generateConfigurator1() like:

function getDepthOptions(height: number) {
const isShallowOptionDisabled = height > 1000
return [
new SKYPARAM.DropdownItem('Shallow (50 mm)', 50, { disabled: isShallowOptionDisabled }),
new SKYPARAM.DropdownItem('Standard (80 mm)', 80),
new SKYPARAM.DropdownItem('Deep (120 mm)', 120),
]
}

Having the options in a separate function allows you to reuse it for the parameter and its rule, without duplicating code.

Here we see some arguments that configurator.setUpdateRule() uses:

  • rule for the height:

    • newValue, same as parameter.getValue(), is the current value of the parameter Height.
    • directUpdate becomes false if any of the parameters above, i.e. Width, is being changed.
    • parameter contains all the info about the parameter where the rule is being set to, i.e. Height here.
    • values contains all the values of the parameters above the current one, i.e. Width here.
  • rule for the depth:

    • newValue, same as parameter.getValue(), is the current value of the parameter Depth.
    • directUpdate becomes false if any of the parameters above, i.e. Width or Height, is being changed.
    • parameter contains all the info about the parameter where the rule is being set to, i.e. Depth here.
    • values contains all the values of the parameters above the current one, i.e. Width and Height here.

If you start realizing that some parameters are being affected by something below it, then the design of the configuration model is wrong. Therefore, you would need to update the order of the parameters, so that no parameter is affected by another below it. Taking this into account is crucial for a good user experience and workflow of the product configurator.

Parameters in different configuration models​

When we want to update parameters that belong to different configuration models, there is no need to use configurator.setUpdateRule(). If we want to update a parameter from a configuration model in tab A, based on the values from parameters of a different configuration model in tab B, then the value we want should come from the properties of the component.

For example, if we want the module width (from a configuration model in Modules) not to be larger than the cabinet width (from a configuration model in Cabinet), the parameter for the module width would look like this:

const maxModuleWidth = component.getProperty('width')

const moduleWidthParameter = SKYPARAM.generateInputParameter(PARAMETER.MODULE_WIDTH, 'Module Width', {
defaultValue: component.getProperty('moduleWidth'),
maxValue: maxModuleWidth
minValue: 10,
stepSize: 0.1,
unit: 'mm',
})

There's no need for a configuration model rule for it since the max value of the property can be directly retrieved from the property cabinetWidth.

However, there is a small difference in behavior when the configuration models are in the same or different tab, and that's because when they are created. As mentioned before, in the component-editor we can test the configuration models right away in the preview, but individually. It's in the product configurator where we should test the behavior between configuration models, since "more things" can happen than just rather changes in parameters max values, like geometry or camera updates and so on.

So then we have these cases when:

Configuration Models in different tabs​

The most common case here applies the same thing as mentioned before: it is enough if we get the values we want from the properties.

As a more complex example, let's think about this case: since the cabinet width affects the width of all the existing modules, we want not to remove the invalid modules but update their properties. In that case, we need to add this logic in the callback of the configuration model that is affecting the rest, so like:

// PARAMETERS

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

const configurator = new SKYPARAM.Configurator(configurableParameters)

// configurator rules

configurator.addCompletionCallback((valid, values) => {
if (valid) {
component.setProperties({
height: values[PARAMETER.HEIGHT],
width: values[PARAMETER.WIDTH],
depth: values[PARAMETER.DEPTH],
})

component.updateExistingModules() // extra logic to add the mentioned "rule"
}
})

return configurator
}

where component.updateExistingModules() could do something like:

// COMPONENT

export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
constructor() {
// ...
}

updateExistingModules() {
const cabinetWidth = this.getProperty('width')
const moduleInstances = this.componentHandler.getInstances(MODULE.Component)
moduleInstances.forEach((instance) => {
const moduleComponent = instance.getComponent()
const moduleWidth = moduleComponent.getProperty('width')
if (moduleWidth > cabinetWidth) {
moduleComponent.setProperties({ width: cabinetWidth })
}
})
}

generateGeometry() {
// ...
}

// more possible functions
}

So again there is no configuration model rule but extra logic in the configuration model callback that calls a class function in the component.

Configuration Models in same tab​

There is a special case when the configuration models are in the same tab. As we said, when we switch between tabs in the product configurator, the buttons and configuration models are created again, so the rules will be triggered automatically. Here for example, we add callbacks for both configuration models to update the camera in the first case and the geometry in both. And yes, the configurator.addCompletionCallback() in the component that sets the properties accordingly is also triggered, so they are not exclusive and multiple callbacks can coexist.

// App / UI

const cabinetTab = new SKYUI.Tab({
title: 'Cabinet',
icon: 'modal-window',
onInit: () => {
const configurator1 = ACTIONS.generateConfigurator1()
configurator1.addCompletionCallback((valid, values) => {
if (valid) {
Studio.setCameraToFitBounds({ bounds: ACTIONS.getCabinetBounds(), travelTime: 500 })
Studio.requestGeometryUpdate()
}
})

const configurator2 = ACTIONS.generateConfigurator2()
configurator2.addCompletionCallback((valid, values) => {
if (valid) {
Studio.requestGeometryUpdate()
}
})

Studio.setTabContent([configurator1, configurator2])

Studio.requestGeometryUpdate()
Studio.requestLightweightGeometryUpdate()
},
})

But to add even more complexity, let's say that the height limits the min rail spacing, or in other words, parameters that affect each other that belong to different configuration models that are in the same tab. In this case, configuration models are already created and we can't take advantage of the behavior of regenerating configuration models when switching tabs, because again both configuration models are in the same tab.

In this case, we still don't need a configuration model rule, so in order to force a regeneration of the configuration models, there's no other way than regenerating the tab, as if we switched the tab. And that can be done through Studio.reloadActiveTab(). So the tab would look like this:

const cabinetTab = new SKYUI.Tab({
title: 'Cabinet',
icon: 'modal-window',
onInit: () => {
const configurator1 = ACTIONS.generateConfigurator1()
configurator1.addCompletionCallback((valid, values) => {
if (valid) {
// camera and geometry update
Studio.reloadActiveTab()
}
})

const configurator2 = ACTIONS.generateConfigurator2()
// configurator2 callback

// rest of functions
},
})

Now with Studio.reloadActiveTab() in the configuration model callback, we make sure that both configuration models are created again, right after setting the properties of configurator1, and therefore the parameters of configurator2 (including their rules) will be created again.

Although these are the most common examples of rules, you might run into an even more complex one. Reach us at support@dynamaker.com if you need help with a specific behavior between configuration models that is not explained here!