Skip to main content

SKYPARAM

This library is intended to handle the configurators and its parameters.

Example

As an example, let's think about a configurator for a T-shirt.

Configuration

Property

Properties represent the values that a component can have. It is recommended to bundle governing properties in the same object. They are usually found in the constructor of each component under COMPONENT, like:

this.properties = {
material: 'material-cotton',
size: 40,
longSleeves: true,
sleeveLength: 14,
style: 7,
mainColor: 0xFF0000,
collarColor: 5012,
logoColor: 0xffffee,
text: '๐Ÿ’ก SkyMaker'
}

Also, their definition is usually found in CONSTANTS as an interface and should include the type of every property defined.

export interface Properties {
material: string,
size: number,
longSleeves: boolean,
sleeveLength: number,
style: number,
mainColor: number,
collarColor: number,
logoColor: number,
text: string
}

Although properties are merely objects and don't belong to any library, it is important to distinguish them from the parameters, that a configurator may have, and they could be related to a property in may ways (eg. through rules, combinaton of them, etc).

Parameter

Parameters represent the user interaction with the component for creating a good product. These are all the available:

const dropdownParameter = SKYPARAM.generateDropdown('my-dropdown-parameter', 'Material', [
new SKYPARAM.DropdownItem('Linen', 'material-linen'),
new SKYPARAM.DropdownItem('Cotton', 'material-cotton'),
new SKYPARAM.DropdownItem('Lycra', 'material-lycra')
])
SKYPARAM.generateDropdown(id: string, label: string, items: DropdownItem[], args: {
defaultIndex: number,
disableInfoMessage: string,
disabled: boolean,
glyphicon: any,
isOpen: boolean,
resettable: boolean,
thumbnail: string,
unit: string
})
const dropdownWithInputParameter = SKYPARAM.generateDropdownWithInput('my-dropdown-with-input-parameter', 'Size', [
new SKYPARAM.DropdownItem('XL', 48),
new SKYPARAM.DropdownItem('L', 44),
new SKYPARAM.DropdownItem('M', 40),
new SKYPARAM.DropdownItem('S', 36)
])
SKYPARAM.generateDropdownWithInput(id: string, label: string, standardOptions: DropdownItem[], args: {
defaultInputValue: number,
defaultValue: number,
disableInfoMessage: string,
disabled: boolean,
disabledDisplayValue: number,
glyphicon: any,
isOpen: boolean,
maxValue: number,
minValue: number,
stepSize: number,
thumbnail: string,
unit: string
})

Binary Dropdown

const binaryDropdownParameter = SKYPARAM.generateBinaryDropdownParameter('my-binary-parameter', 'Long sleeves', 'Yes', 'No')
SKYPARAM.generateBinaryDropdownParameter(id: string, label: string, optionTextA: string, optionTextB: string, args: {
defaultOption: number,
disableInfoMessage: string,
disabled: boolean,
glyphicon: any,
thumbnail: string,
unit: string
})

Input

const inputParameter = SKYPARAM.generateInputParameter('my-input-parameter', 'Sleeve length')
SKYPARAM.generateInputParameter(id: string, label: string, args: {
defaultValue: number,
disableInfoMessage: string,
disabled: boolean,
disabledDisplayValue: number,
excludedValues: number[],
glyphicon: any,
maxValue: number,
minValue: number,
stepSize: number,
thumbnail: string,
unit: string
})

Slider

const sliderParameter = SKYPARAM.generateSlider('my-slider-parameter', 'Style')
SKYPARAM.generateSlider (id: string, label: string, args: {
defaultValue: number,
disableInfoMessage: string,
disabled: boolean,
disabledDisplayValue: number,
excludedValues: number[],
glyphicon: any,
maxValue: number,
minValue: number,
stepSize: number,
thumbnail: string,
unit: string
})

Color

const colorParameter = SKYPARAM.generateColorParameter('my-color-parameter', 'Color', [
{ id: 'red-color', label: 'Red', value: 0xFF0000 },
{ id: 'green-color', label: 'Green', value: 0x00FF00 },
{ id: 'blue-color', label: 'Blue', value: 0x0000FF }
])
SKYPARAM.generateColorParameter(id: string, label: string, items: { id: string, label: string, value: number }[], args: {
disableCustomColor: boolean,
disabled: boolean,
glyphicon: any,
visible: boolean
})

RAL Color

const ralColorParameter = SKYPARAM.generateRalParameter('my-ral-color-parameter', 'Collar color', [
new SKYPARAM.DropdownItem('Blue (RAL 5012)', 5012),
new SKYPARAM.DropdownItem('Yellow (RAL 1018)', 1018),
new SKYPARAM.DropdownItem('Black (RAL 9005)', 9005)
])
SKYPARAM.generateRalParameter (id: string, label: string, standardOptions: DropdownItem[], args: {
defaultCustomColor: number
defaultValue: number
disableInfoMessage: string
disabled: boolean
disabledDisplayValue: number
isOpen: boolean
})

Palette Color

const paletteParameter = SKYPARAM.generatePaletteParameter('my-palette-parameter', 'Logo color', [
new SKYPARAM.DropdownItem('Light', 0xffffee),
new SKYPARAM.DropdownItem('Dark', 0x7a7980),
new SKYPARAM.DropdownItem('Middle', 0xaeabab)
])
SKYPARAM.generatePaletteParameter(id: string, label: string, items: DropdownItem[], args: {
defaultIndex: number,
deselectOnSelection: boolean,
disabled: boolean,
glyphicon: any,
resettable: boolean,
unit: string
})

Text

const textParameter = SKYPARAM.generateTextParameter('my-text-parameter', 'Logo name', '๐Ÿ’ก SkyMaker')
SKYPARAM.generatePaletteParameter(id: string, label: string, text: string, args: {
disabled: boolean,
glyphicon: any,
thumbnail: string
})


Configurator

A configurator allows us to customize the product through parameters. Since this is the key feature of all CAD configurators done in DynaMaker, the workflow of the app 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 important their order.

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


In this case, we have up to 4 different configurators:

  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 app, we have created these 4 configurators according to specific behaviors:

  1. to update the camera when updading 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 app should have, creating configurators should be straightforward!

How To Create A Configurator

In your component, under PARAMETERS, create a function that creates a configurator 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 configurators 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 configurator in the component-maker (using the dropdown at the top-left corner). After creating the functions that generate configurators, you should be able to preview them like this:

Update Properties From Parameters

After creating the configurator, 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. Here the function needs a function as argument which uses two variables (with whatever name you give them):

  • valid as a boolean gives true if all parameter have valid input (eg. out of range value in an input-parameter)
  • values with all the parameter values existing in the configurator.
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().

Rules

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

Parameters within the same configurator

Use setUpdateRule to create a rule between multiple parameters of the same configurator. 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) => { // @dm-autofold
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 (eg. "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
}
}
})

*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, ie. Width, is being changed.
    • parameter contains all the info about the parameter where the rule is being set to, ie. Height here.
    • values contains all the values of the parameters above the current one, ie. 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, ie. Width or Height, is being changed.
    • parameter contains all the info about the parameter where the rule is being set to, ie. Depth here.
    • values contains all the values of the parameters above the current one, ie. Width and Height here.

If you start realizing that some parameters are being affected by something below it, then the design of the configurator 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 app.

Parameters in different configurators

When we want to update parameters that belong to different configurators, there is no need to use configurator.setUpdateRule(). If we want to update a parameter from a configurator in the tab A, based on the values from parameters of a different configurator in the 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 configurator in Modules) not to be larger than the cabinet width (from a configurator in Cabinet), the parameter for the module width would look like:

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

So see that there's no need for a configurator 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 configurators are in the same or different tab, and that's because when they are created. As mentioned before, in the component-maker we can test the configurators right away in the preview, but individually. It's in the app where we should test the behavior between configurators, 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:

Configurators in different tabs

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 configurator 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 configurator rule but extra logic in the configurator callback that calls a class function in the component.

Configurators in same tab

There is a special case when the configurators are in the same tab. As we said, when we switch tab in the app, the buttons and configurators are created again, so the rules will be triggered automatically. Here, for example we add callbacks for both configurators 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: STUDIO.ICONS.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 configurators that are in the same tab. In this case, configurators are already created and we can't take advantage of the behavior of regenerating configurators when switching tabs, because again both configurators are in the same tab.

In this case, we still don't need a configurator rule, so in order to force a regeneration of the configurators, 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: STUDIO.ICONS.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 configurator callback, we make sure that both configurators 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 about rules, you might run into in an even more complex one. Reach us at support@dynamaker.com if you need help with a specific behavior between configurators that is not explained here!