Configurationformat

Kernelversion 2.24.0

Definitions

Catalogs

A catalog consists of items, components, tags and materials. It can be private (visible only to selected users and the owner) or public (available for all Roomle users).

Tag

Tags are the way items are organized in catalogs. Each catalog must have at least one root-tag. Tags can be structured hierarchically and one tag can be a child of multiple other tags. Each Tag is linked to one catalog and must have a (globally) unique identifier.

Item

An item is an object that can be added to a plan or loaded into a configurator. This can be a normal 3D object or a pre-defined configuration of components. Each item belongs to exactly one catalog but can be linked to multiple tags. Items which are not linked to any tag are not visible to the user. Every item must have a unique SKU (within the catalog). The terms item and product refer to the same thing and can be used interchangeably.

Component

A component is the basic element used for configurable objects. Each component contains a definition on how this component can be configured and used within configurations. Similar to items, components can be linked to multiple tags. Every component must have a unique identifier (within the catalog).

Mesh

Within each catalog you can define meshes to be used in the geometry-script of a component. Each mesh can have data for different formats and quality levels. At the moment only CRT (Corto compressed meshes) with Realtime quality (level 50) is used. Within the script the mesh can be inserted with the AddExternalMesh command. The MeshId is the combined id <catalogId>:<meshId>.

Defined meshes (external meshes in the script) improve loading performance since only those meshes are loaded that are currently needed. In contrast to scripted meshes where the whole mesh is part of the script and needs to be interpreted even if not shown. Furthermore defined meshes provide the client with the ability to reuse the mesh (since defined meshes have an id and are const by definition) within the scene which also improves performance and memory usage.

Material

Within each catalog you can define materials that can be used in the geometry script of a component. Each material can have no or multiple textures assigned to it for use. Each material must have a unique identifier (within the catalog).

Configurations

When talking about configurable items you must distinguish between what we call "configuring variants" (which is basically just changing colors, product dimensions, etc.) and "combining components to one object" (creation of new objects and products - real configuration). Components can be seen as templates for the basic elements of configurable objects. A component itself can not be used within a plan. To use a component within a plan it must be embedded into a configurated item, which is an item with a given configuration. The configuration (see section configuration format) defines the actual variant of the involved components used in the configuration.

Configuring variants

An example for configuring variants is a table where you can change the dimensions and the material of the surface. In Roomle this can be modeled as one component with parameters for the dimensions and the material. This table within a plan is defined through a configuration containing only one component.

Combining components

More complex configurations arise when multiple components can be combined together, e.g. a frame with different possible shelves that can be added. Every configuration must have exactly one root-component. Details on docking components can be found in 2.5{reference-type="ref" reference="dockingComponents"}

Roomle Configuration Format

The Roomle Configuration Format is serialized as JSON file.

Coordinate system

The coordinate system within the Roomle ConfigurationScript is a left-handed cartesian coordinate system with +Z as the up vector and +Y as the forward direction of the model. In the 3d file 1 Model Unit has to correspond to 1mm real world size of the object.

{
    "id": "catalog_id:coordinate_system",
    "geometry": "
        AddPrism(1000, Vector2f[{0, 0}, {2000, 0}, {0, 1000}]);
       SetObjSurface('roomle_script_test:yellow');
    "
}

Component definition

The component definition defines how a component should be displayed, which parameters are possible and the interaction with other components. It includes all information needed within the configurator. This includes:

  • A condition when the component is considered valid. This can be used for Components who must have specific parameters/dockings set to be valid.

  • A list of possible parameters including the possible values and a default value per parameter

  • A list of possible animations including the possible actions.

  • A list of possible ParentDockings and ChildDockings. These are the connectors to combine different components.

  • A list of possible SiblingPoints. These are connectors throu which components can transfer data (e.g. parameter values) regardless of the parent-child connection.

  • A list of possible AddOnSpots. These are visual aids for the User.

  • A list of possible Subcomponents which can be used within the geometry script and will be displayed in the partlist

  • The article number script written in RoomleScript

  • The geometry script written in RoomleScript

  • The geometryHD script written in RoomleScript. This version is used in special clients for higher resolution and quality. Besides thatit follows the same logic and definitions as the geometry script. Everything that can be done in the geometry script can be done in geometryHD too.

  • The previewGeometry script written in RoomleScript. When provided, this geometry is used to preview objects during adding of new children. If not provided, the geometry script is used. All geometry-script functions work the same in the previewGeometry. If provided the previewGeometry script should be less complex than the real geometry script to improve performance during the insertion.

  • The boundingGeometry is intended to define the simplified boundaries of the components geometry that can be used for collision detection and cutting holes of construction objects in walls.

  • The boundingGeometry script written in RoomleScript.

  • The packageSize contains a Array<int>, these are all the numbers of packages which are allowed for this component

  • The packaging contains a list of sizes with conditions for adding certain sizes if needed

  • The numberInPartList and sortInPartList properties which define how many and at which position the component should be shown in the partlist.

  • The price calculation written in RoomleScript

  • an onUpdate RoomleScript which is executed everytime something changes within the component. Within this script even parameters of the component may be changed.

Parameters

A component is completely defined by the values of its parameters. Parameters have multiple ways to control where and how it is shown and behaves.

If all connected parameters behind a global parameter have the same value, the global parameter is automatically set to this value.

  • visible: shown to the user as part of the component. Default value true.

  • enabled: user can modify the value. Default value true.

  • global: all global parameters with the same key within a configuration are combined together and shown globally. The global value for a parameter-key may differ from the actual value on the component (if the component-parameter is changed after the global value is set). New components get the global value assigned automatically on dock. If all connected parameters behind a global parameter have the same value, the global parameter is automatically set to this value. Default value false.

  • volatile: If this property is set, the parameter is not stored in the configuration. Default value false.

  • visibleAsGlobal: the global parameter is shown if any of the connected component parameters is set as "visibleAsGlobal", otherwise its invisible globally. visible and visibleAsGlobal can be completely independent. Default value true.

  • visibleInPartlist: if this parameter should be shown in the partlist. parameters not visible in the partlist are also ignored in the aggregation of components in the partlist. Default value true.

  • userTriggeredChange: is set to true, if onValueChange is triggered from user. Then for corresponding key respective values fetched from json will be set. if onValueChange is internal trigger, values are ignored.

  • an onValueChange RoomleScript which can be provided for every Parameter and is executed on startup (change from no value to the first/default value) and every time this parameter changes.

  • visibleInPlanner: If the parameter is "global" and "visibleInPlanner", the object parameter is delivered with the PlanObject when a plan overview or an individual plan object is requested. If the "visibleInPlanner" property of the parameter is set but not the "global" property, then a warning is generated when the component is read and the parameter is treated as if "visibleInPlanner" would be "false".

Parameter and possible children level

The visibility of parameters and possible children are restricted and are only visible if the user level exceeds the restriction. Therefore the property level needs to be added to parameters or possible children. The value of the property can only be an integral constant. The default restriction level is 0, so these parameters are visible to all users. The user level is set via an environment variable and is 0 by default. The core only populates the parameters and possible children that the user can access. Thus, a user with a higher level can access more parameters and more restricted parameters can be accessed by fewer users. As the default level of each parameter is 0, this new feature has no impact on the live content, as any user can access parameters with level 0 (unrestricted).

Parameter levelCondition

For parameters, you can define a levelCondition in addition to the level. This is a script that is evaluated in addition to the level. Both of these conditions must evaluate true that a parameter is visible for a specific user. If no levelCondition is provided it defaults to true. It is possible to access the current user level within the levelCondition script using the keyword level. Keep in mind that it is not possible to access a component parameter with the same name. It is not possible to write parameters within this script and therefore it is not possible to change the user level.

Example:

{
    "id": "catalogId:component_level_test",
    "parameters": [
        {
            "key": "one",
            "level": 0,
            "levelCondition": "true"
        },
        {
            "key": "two",
            "level": 0,
            "levelCondition": "false"
        },
        {
            "key": "three",
            "level": 30,
            "levelCondition": "true"
        },
        {
            "key": "four",
            "level": 30,
            "levelCondition": "false"
        },
        {
            "key": "five",
            "level": 30,
            "levelCondition": "
                _.a = 10;
                _.b = 20;
                if (level > _.a && level < _.b) {
                    return true;
                }
                return false;
            "
        }
    ]
}

This would result for a user of level 0 only seeing the parameter ["one"]. But a user of level 30 would see the parameters ["one", "three"]. The user of level 30 does not see parameters two, four and five even if their level would be sufficient, but the levelCondition scripts evaluate to false.

Animations

An animation looks similar to a parameter, but is actually something completely different. The actual state of the animation is not saved in the configuration and must not change the configuration hash. The animation does not change the product or the partial list of the product itself. It is merely a visual effect.

  • key: The key of the animation. This key is used to combine animations with the same key within a configuration. The key is also used to define the animation action in the geometry script.

  • label, labels: The label of the animation which is shown in the UI.

  • group: The key of the parameterGroup the animation belongs to.

  • visible: shown to the user as part of the component. Default value true.

  • enabled: user can trigger an action of the animation. Default value true.

  • global: all global animations with the same key within a configuration are combined together and shown globally. If the animations have different actions, all the actions are combined together.

  • visibleAsGlobal: the global animations is shown if any of the connected component animations is set as "visibleAsGlobal", otherwise its invisible globally. visible and visibleAsGlobal can be completely independent. Default value true.

  • visibleInPlanner: If the animations is "global" and "visibleInPlanner", the object animations is delivered with the PlanObject when a plan overview or an individual plan object is requested.

  • actions: A list of possible actions for the animation. Each action has a key a label and a type. The possible types are origin and matrix:

    • origin: The action animates all parts of the geometry back to its original position.

    • matrix: The action animates all parts of the geometry to the orientation and position defined in the geometry script with the animation matrix corresponding to the key of the action.

Subcomponents

Every component can contain multiple subcomponents. A subcomponent references a component with all of its scripts and computations. The main component may set parameters of the subcomponents via assignments. It's also possible for the main component to define one or more subcomponents as active. If defined, the main component can take parameters of the active subcomponents to supersede its own.

Supersedings

For active subcomponents the main component may define supersedings. If a superseding parameter is defined, it completly replaces the parameter of the main component with the same key if one exists. This means that from outside it behave as if the parameter were the parameter of the main component itself although the validValues and all calculations are done in the subcomponent.

This is specially useful for cases where the main component acts as a metacomponent which only decides which subcomponent is used. In this case all calculations can stay subcomponent specific without the need to copy them to the main component while still having all logic available.

The values of the superseded parameters are also available in the main component (and may override existing values in the main component) and can be used in geometry, docking etc. Be aware that the values of the superseding parameters may not be available on the initial executions of "onUpdate" since the component needs to initialize itself before knowing what parameters will be superseded. Consider using ifnull in such a case.

Optionally, the key used to access the substituted parameters in the component can be specified by adding an override object with a key attribute. This is useful for avoiding parameter shadowing in the main component and for superseding parameters with the same name from different subcomponents. In the script, the substituted parameter can be used just as if there were a parameter with the used override key.

By default, the substituted parameter maps to the same group specified in the subcomponent. This default behavior can be changed by setting an override group. Membership in a group can even be removed by assigning an empty string to the override group.

Plan interaction

Information about how the object interacts with the plan, such as the intersection of the PlanComponent with walls.

Data

Getting data

Static JSON data that can be queried in RoomleScript with the functions getData, getDataOrNull or getDataWithDefault. While getData generates an error if the data is not found, getDataOrNull and getDataWithDefault never generate an error and always return a valid value. getDataOrNull returns null if the data is not found, while getDataWithDefault has an additional default argument that is returned if the data is not found. The default argument is only evaluated if the data is not found.

The getData* can return not only values, but also objects and arrays. The elements of the objects can be accessed with the . (member access) operator. Multidimensional arrays or lists of arrays are not supported because the RoomleScript does not support multidimensional arrays at all, not even for basic data types.

Example

{
    "data": {
        "simpleObject": {
            "stringValue": "string 1",
            "intValue": 1
        },
        "nestedObject": {
            "elementA": {
                "stringValue": "string 2",
                "intValue": 2
            },
            "elementB": {
                "stringValue": "string 3",
                "intValue": 3
            }
        },
        "objectArray": [
            {
                "stringValue": "string 4",
                "intValue": 4
            },
            {
                "stringValue": "string 5",
                "intValue": 5
            }
        ],
        "valueArray": [ 6, 7 ],
        "multiDimensionalArray": [ 
          [8, 9],
          [10, 11] 
        ],
         "multiDimensionalObjectArray": [ 
          [
              {
                  "stringValue": "string 12",
                  "intValue": 12
              },
              {
                  "stringValue": "string 13",
                  "intValue": 13
              }
          ],
          [
              {
                  "stringValue": "string 14",
                  "intValue": 14
              },
              {
                  "stringValue": "string 15",
                  "intValue": 15
              }
          ] 
        ]
    }
}
value = getData('simpleObject', 'stringValue');          // value == "string 1"
value = getData('nestedObject', 'elementA', 'intValue'); // value == 2
value = getData('objectArray', 0, 'intValue');           // value == 4
value = getData('valueArray', 1);                        // value == 6
obj = getData('simpleObject');
value1 = obj.stringValue;                                // value1 == "string 1"
value2 = obj.intValue;                                   // value2 == 1
obj = getData('nestedObject', 'elementB');
value1 = obj.stringValue;                                // value1 == "string 3"
value2 = obj.intValue;                                   // value2 == 3
obj = getData('nestedObject');
value1 = obj.elementA.stringValue;                       // value1 == "string 2"
value2 = obj.elementA.intValue;                          // value2 == 2
innerObject = obj.elementB;
value3 = innerObject.stringValue;                        // value3 == "string 3"
value4 = innerObject.intValue;                           // value4 == 3
obj = getData('objectArray', 0);
value1 = obj.stringValue;                                // value1 == "string 4"
value2 = obj.intValue;                                   // value2 == 4
array = getData('objectArray');                          // array is an array with two elements, each element is an object 
array = getData('valueArray');                           // array is the array [6, 7]
array = getData('multiDimensionalArray', 1);             // array is the array [10, 11]
array = getData('multiDimensionalArray');                // array is "null" because multidimensional arrays are not supported
array = getData('multiDimensionalObjectArray', 0);       // array is an array with two elements, each element is an object 
array = getData('multiDimensionalObjectArray');          // array is "null" because multidimensional arrays are not supported

getData* returns a copy of the data. The elements of objects can also be assigned. Note that this only changes the data in the variable, but of course not the data in the component.

obj = getData('simpleObject');
value1 = obj.stringValue;               // value1 == "string 1"
value2 = obj.intValue;                  // value2 == 1

obj.stringValue = getData('nestedObject', 'elementA');
obj.intValue = 10;
value3 = obj.stringValue.stringValue;   // value3 == "string 2"
value4 = obj.stringValue.intValue;      // value4 == 2
value5 = obj.intValue;                  // value5 == 10

obj2 = getData('simpleObject');
value6 = obj2.stringValue;              // value1 == "string 1"
value7 = obj2.intValue;                 // value2 == 1

In the Roomle script, the names of the variables can also be self, other, child, parent, sibling, connection, other_connection, parameter and object. Despite the fact that this is a bad style, it does not contradict the corresponding context names. However, if such a variable is an object, the context must be explicitly specified when accessing the object's members.

self = getData('simpleObject');         // bad style to name a variable "self" which is actually "self.self"
value1 = self.intValue;                 // value1 is "null", because there is no variable "intValue" in the context "self"
value2 = self.self.intValue;            // value2 == 1

Elements of arrays of objects can be accessed with the get function:

array = getData('objectArray');
obj = get(array, 1);                    // obj is the 2nd element of the array
value1 = obj.stringValue;               // value1 == "string 5"
value2 = obj.intValue;                  // value2 == 5

Elements of arrays of objects can be overwritten with the set function:

array = getData('objectArray');
obj = get(array, 1);                    // obj is the 2nd element of the array
set(array, 0, obj);                     // the 1st element of the array is now the same as the 2nd element

As for any other array, the length of an array of objects can be get with the length function:

array = getData('objectArray');
value = length(array);                  // value == 2

Evaluating data

In addition to the gerData* functions, there are evaluateData, evaluateDataOrNull or evaluateDataWithDefault. These functions work exactly like the corresponding getData* functions, however, if the data is a string, the string is treated as an expression and evaluated. The expressions in the data are not parsed when the component is loaded, but only at runtime when the particular element in "data" is evaluated for the first time. Note that to prevent recursion, it is not allowed to call a getData* or evaluateData* function again within a data expression.

Example

{
    "data": {
        "simpleObject": {
            "stringValue": "'string 1'",
            "intValue": 1,
            "formula": "variableA + 1"
        },
        "nestedObject": {
            "elementA": {
                "stringValue": "'string 2'",
                "intValue": 2,
                "formula": "variableB * 2"
            },
            "elementB": {
                "stringValue": "'string 3'",
                "intValue": 3,
                "formula": "round(variableC, 0)"
            }
        },
        "objectArray": [
            {
                "stringValue": "'string 4'",
                "intValue": 4,
                "formula": "variableD | string(variableE)"
            },
            {
                "stringValue": "'string 5'",
                "intValue": 5
            }
        ]
    }
}
variableA = 2;
value = evaluateData('simpleObject', 'formula');  // value == "3.00"
variableA = 10;
obj = evaluateData('simpleObject');  
value1 = obj.stringValue;                         // value1 == "string 1"
value2 = obj.intValue;                            // value2 == "1"
value3 = obj.formula;                             // value3 == "11.00"
variableB = 100;
variableC = 3.1;
obj = evaluateData('nestedObject');  
value1 = obj.elementA.stringValue;                // value1 == "string 2"
value2 = obj.elementA.intValue;                   // value2 == "2"
value3 = obj.elementA.formula;                    // value3 == "200.00"
value4 = obj.elementB.stringValue;                // value4 == "string 3"
value5 = obj.elementB.intValue;                   // value5 == "3"
value6 = obj.elementB.formula;                    // value6 == "3.00"
variableD = 'article';
variableE = 4i;
obj = evaluateData('objectArray', 0); 
value1 = obj.stringValue;                         // value1 == "string 4"
value2 = obj.intValue;                            // value2 == "4"
value3 = obj.formula;                             // value3 == "article4"

With the functions getSubComponentData, getSubComponentDataOrNull, getSubComponentDataWithDefault, evaluateSubComponentData, evaluateSubComponentDataOrNull and evaluateSubComponentDataWithDefault data from subcomponents can be accessed. The functions work exactly like the corresponding getData* and evaluateData* functions, but have an additional argument at the beginning, which is the internal ID of the subcomponent.

Example

{
    "id": "catalogId:data",
    "data": {
        "testData1": {
            "stringValue": "'string 1'",
            "intValue": 1,
            "formula": "variableA + 1"
        }
      }
}
{
    "id": "catalogId:main",
    "onUpdate": "

        value1 = getSubComponentData('globalData', 'testData1', 'stringValue1');   // value1 == "string 1"
        
        variableA = 3;
        dataObject = evaluateSubComponentData('globalData', 'testData1');
        value2 = dataObject.stringValue;                                           // value2 == "string 1"
        value3 = dataObject.integerValue;                                          // value3 == 1
        value4 = dataObject.formula;                                               // value4 == 4
    ",      
    "subComponents": [
        {
            "internalId": "globalData",
            "componentId": "catalogId:data"
        }
    ]
}

Finding data

In addition to getting or evaluating data, the data can also be filtered using the findData, findSubComponentData, findAndEvaluateData, and findAndEvaluateSubComponentData functions. findData" returns the filtered objects and values as they are. findAndEvaluateData treats expressions as strings and evaluates them before filtering. With the functions findSubComponentData, and findAndEvaluateSubComponentData data from subcomponents can be accessed.

The last arguments of this functions is the filter functions, which can be a script function ot a component function. The function must have exactly 2 arguments, the key and the value of the data object and The function must return a boolean value. If an array is searched, the key is a 'Decimal' number with the index of the element. If an object is searched for, the key is a "String" value with the key of the data object. All functions return an array with the matches. If no value matches the filter, an empty array is returned. If an object is searched for, the returned array contains objects with 2 properties. The name of the first property is "key" and contains the key of the matching object. The name of the second property is "value" and contains the value of the matching object.

function filter(key, value) {
      return value.intValue > 1;
}
valueA = findData('objectArray', filter);
valueB = findSubComponentData('subComponent', 'objectArray', filter);
valueC = findAndEvaluateData('objectArray', filter);
valueD = findAndEvaluateSubComponentData('subComponent', 'objectArray', filter);

If the functions are used with a component function, the first argument of the component function must be the key and the second argument of the function must be the value. The values are passed to the function in the order of the arguments, not in the order of the keys. So if the name of the first argument is "value" and the name of the second argument is "key", this is just bad naming, but it does not change the order of the arguments. Default values of the arguments are of no use:

{
    "id": "catalogId:data",
    "onUpdate": "
        valueA = findData('objectArray', filter);
        valueB = findSubComponentData('subComponent', 'objectArray', filter);
        valueC = findAndEvaluateData('objectArray', filter);
        valueD = findAndEvaluateSubComponentData('subComponent', 'objectArray', filter);
    ",
    "functions": [
        {
            "key": "filter",
            "arguments": [{ "key":