Configurationformat

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 Docking Components, where the components can interfact with each other via Connections. Another way of combining components are SubComponents.

Connections

There are two ways how components might connect with each other: docking and siblings. Both types share the ability to set values on the component and the other side of the connection. Docking is a parent-child hierarchy, while siblings can traverse the whole configuration tree. A connection can exist between two or more, up to the maxConnections attribute, of the connection point.

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');
    "
}
coordinate system

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.

Restriction level value can also be retrieved with the GetEnvironmentProperty.

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.

Script Access Rights

As mentioned above, various scripts can have access to self, connection and other. But not all scripts have full access to everything everywhere, instead it is explicitly defined for every script where it has access to and which type of access it has.

List of script access locations

This list is just an information for the considered location of every part/component.

{"_",                   ScriptAccessLocation::Local},
{"",                    ScriptAccessLocation::Local},
{"self",                ScriptAccessLocation::Self},
{"parameter",           ScriptAccessLocation::Self},
{"object",              ScriptAccessLocation::Self},
{"connection",          ScriptAccessLocation::Connection},
{"other",               ScriptAccessLocation::Other},
{"other_connection",    ScriptAccessLocation::Other},
{"parent",              ScriptAccessLocation::Other},
{"child",               ScriptAccessLocation::Other},
{"sibling",             ScriptAccessLocation::Other}

List of script access rights

The following is a list of all scripts and script paths (also subpaths) and the corresponding access rights for it. The first member (string) defines the path in the script, so for example onUpdate. The second string defines if the path is part of another path, eg onUpdate in parentDockings. The next member is the ScriptType, this is just relevant code internally. Next is the ScriptAccessLocation and defines where the script has access to, so if it has access just to self, connection or other. These are in hierarchic order, so if a script has rights to access other, it also has rights for connection and self, (read) access to self is always granted. The last member defines if the script has only read or also write access rights.

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. The master components also defines if and with what amount the subcomponent is added to the partlist. If the subcomponent itself contains subcomponents, each element in the subcomponent is multiplied with the appearance of the subcomponent and added to the partlist. This numberInPartList may also be a decimal number.

Every subcomponent can be defined as a main component. When accessing the partlist, the elements are aggregated overall and on a mainComponent basis. This means that everything beneath a mainComponent is aggregated but not combined with elements outside of this mainComponent. If mainComponents contain mainComponents themself they are aggregated seperatly and doesn't show up in the higher level mainComponents partlist. The mainComponent is also used to define the part list groups. Top-level components are set as mainComponents by default, but can be declared NOT main components with a self reference in their list of subcomponents.

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.

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": "arg1isTheKey" }, { "key": "arg2isTheValue" }],
            "script": "
                return arg2isTheValue.intValue > 1;
            "
        }
    ],
    ...
}

If the functions are used to look up array data, all the values that match the filter are returned in an array. If no value matches the filter, an empty array is returned. Regardless of whether you search in objects or arrays, arrays with key value pairs are always returned. If you search in arrays, the "key" is the index of the element found. The indices of course start with 0. When searching in objects, the "key" is the name of the property found. "value" is the element found.

{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value > 1 && value < 4;
        }
        valueA = findData('valueArray', filter);
    ",
    "data": {
        "valueArray": [0, 1, 2, 3, 4, 5]
    }
}

The content of valueA is an array with 2 object:.

[
  { "key": 2, "value": 2 },
  { "key": 3, "value": 3 }
]
{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value.intValue > 1;
        }
        valueB = findData('objectArray', filter);
    ",
    "data": {
        "objectArray": [
            { "intValue": 0 },
            { "intValue": 1 },
            { "intValue": 2 },
            { "intValue": 3 }
        ]
    }
}

The content of valueB is an array with 2 objects:

[
  { "key": 2, "value": { "intValue": 2 } }, 
  { "key": 3, "value": { "intValue": 3 } } 
]
{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value.intValue > 1;
        }
        valueC = findData('object', filter);
    ",
    "data": {
        "object": {
            "key1": { "intValue": 0 },
            "key2": { "intValue": 1 },
            "key3": { "intValue": 2 },
            "key4": { "intValue": 3 }
        }
    }
}

The content of valueC is an empty array with 2 objects with the properties "key" and "value":

[
    { 
        "key": "key3",
        "value": { "intValue": 2}
    },
    { 
        "key": "key4",
        "value": { "intValue": 3 }
    }
]

If no value matches the filter, an empty array is returned.

{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value.intValue > 1;
        }
        valueD = findData('objectArray', filter);
    ",
    "data": {
        "objectArray": [
            { "intValue": 0 },
            { "intValue": 1 }
        ]
    }
}

The content of valueD is an empty array [].

The data is evaluated prior to filtering:

{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value.formula > 1;
        }
        inputValue = 1;
        valueE = findAndEvaluateData('objectArray', filter);
    ",
    "data": {
        "objectArray": [
            { "formula": "inputValue + 0" },
            { "formula": "inputValue + 1" },
            { "formula": "inputValue + 2" },
            { "formula": "inputValue + 3" }
        ]
    }
}

The content of valueE is an array with 3 objects:

[ 
  { "key": 1, { "formula": 2.00 } }, 
  { "key": 2, { "formula": 3.00 } }, 
  { "key": 3, { "formula": 4.00 } } 
]

With findDataKey and findSubComponentDataKey it is also possible to search only for indices and keys of objects and values. The functions work exactly like the corresponding findData and findSubComponentData functions, but return only the keys or indices of the matching objects and values instead of key-value pairs.

{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value > 1 && value < 4;
        }
        valueA = findDataKey('valueArray', filter);
    ",
    "data": {
        "valueArray": [0, 1, 2, 3, 4, 5]
    }
}

The content of valueA is an array with 2 indices: [2, 3].

{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value.intValue > 1;
        }
        valueB = findDataKey('objectArray', filter);
    ",
    "data": {
        "objectArray": [
            { "intValue": 0 },
            { "intValue": 1 },
            { "intValue": 2 },
            { "intValue": 3 }
        ]
    }
}

The content of valueB is an array with 2 indices: [2, 3].

{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value.intValue > 1;
        }
        valueC = findDataKey('object', filter);
    ",
    "data": {
        "object": {
            "key1": { "intValue": 0 },
            "key2": { "intValue": 1 },
            "key3": { "intValue": 2 },
            "key4": { "intValue": 3 }
        }
    }
}

The content of valueC is an empty array with 2 keys ['key3', 'key4'].

If no value matches the filter, an empty array is returned.

{
    "id": "catalogId:data",
    "onUpdate": "
        function filter(key, value) {
            return value.intValue > 1;
        }
        valueD = findDataKey('objectArray', filter);
    ",
    "data": {
        "objectArray": [
            { "intValue": 0 },
            { "intValue": 1 }
        ]
    }
}

The content of valueD is an empty array [].

Plugin Data

It is possible to add plugin-data to the component-definition. Only data of well known plugins is considered. The details on the json structure can be found below. More details about the concepts and usage can be found in Plugin Documentation

Sample file

{
  "id": "sampleCatalog:component1",
  
  "labels": {
    "de": "deutsches Label",
    "en": "english Label", ...
  },
  "label": "parameter",
  "parameters": [
    {
      "key": "Length",
      "enabled": "true",
      "visible": "true",
      "visibleInPartList": "true",
      "unitType": "length",
      "labels": {
        "de":"Laenge",
        "en":"Length"
      },
      "label": "parameter",
      "type": "Decimal",
      "defaultValue": 1400,
      "validValues": [
        1400,
        1600, ...
      ]
    }, ...
  ],
  "animations": [
    {
      "key": "openClose",
      "labels": {
        "de":"Open / Close",
      },
      "enabled": "true",
      "visible": "true",
      "global": "true",
      "visibleAsGlobal": "true",
      "visibleInPlanner": "true",
      "sort": 1,
      "level": 2,
      "actions": [
        {
          "key": "close",
          "label": "Close",
          "type": "origin"
        },
        {
          "key": "open",
          "label": "Open",
          "type": "matrix"
        }
      ]
    }, ...
  ],
  "pricing": [
    {
      "region": "RML_DEFAULT",
      "currency": "EUR",
      "price": "<price calculation skript>"
    }
  ],
  "possibleChildren": [  { "componentId":"sampleCatalog:component2" } ... ],
  "parentDockings": {
    "points": [ {
        "position": "{0,0,0}",
        "mask": "DockingMask1",
        "rotation": "{0,0,0}",
        "assignments": {}
      },...
    ],
    "lines": [{
        "position": "{(-Breite/2),0,-42.5}",
        "mask": "DockingMask2",
        "assignments": {},
        "rotation": "{0,-(0),-90}",
        "positionTo": "{(Breite/2),0,-42.5}"
      },...
    ],
    "ranges": [{
        "position": "{0,-(-34.5),1100}",
        "mask": "DockingMask1",
        "assignments": {},
        "rotation": "{0,0,0}",
        "stepEnd": "{0,34.5,2200}",
        "stepX": "0",
        "stepY": "0",
        "stepZ": "488"
      },...
    ],
    "lineRanges": [{
        "position": "{(-Breite/2),0,-42.5}",
        "mask": "DockingMask2",
        "assignments": {},
        "rotation": "{0,0,-90}",
        "positionTo": "{(Breite/2),0,-42.5}",
        "stepEnd": "{0,34.5,2200}",
        "stepX": "0",
        "stepY": "0",
        "stepZ": "488"
      },...
    ]},
    "childDockings": {
        "points": [...]
    },
    "addOnSpots": [{
        "position":"{-100,0,0}",
        "visible":"hasLeftNeighbour == false",
        "mask":"leftSpot"
      },...
    ],
    "subComponents": [ {
          "internalId":"leftSide",
          "componentId":"sampleCatalog:sideComponent",
          "assignments": {
            "Length":"Length"
          },
          "numberInPartList":"3",
          "active":"true",
          "supersedings":[
            { "type":"parameter","key":"color" }
          ]
        },{
            "internalId":"rightSide",
            "componentId":"sampleCatalog:sideComponent",
            "assignments": {
            "Length":"Length*2"
          },
        "numberInPartList":"if(Length > 10) {number= 2; } else { number = 1; }"
       },...
    ],
    "packageSizes": [5, 3],
    "packaging": [{
          "size": 1,
          "condition": "someOtherParameter == 1"
        },{
            "size": 7,
            "condition": "someOtherParameter == 1"
          }
    ],
    "articleNr":"article1234",
    "geometry": "<geometry skript>",
    "geometryHD": "<geometry HD skript>",
    "previewGeometry": "<previewGeometry skript>",
    "environmentGeometry": "<environmentGeometry skript>",
    "boundingGeometry": "<boundingGeometry skript>",
    "planInteration": {
        "intersectWithWalls": "true",
        "fixedSnapLevels": "true",
        "snapLevels": [
            {
                "level": "200",
                "default": "true",
                "enabled": "true"
            }
        ]
    },
    "data": {...}
}
tag name
type
description

id

string

the global unique id of the component. It is combined from the unique id of the catalog and an unique id of the component within the catalog

onUpdate

Script

a Script being executed on every changes. Setting of parametervalues within this script persist.

valid

Script

a condition to evaluate if the component is considered valid.

label

Script

A script being executed when the label is requested. Once preset, the label map ("labels") is ignored.

labels

map

a map containing key-value pairs for the label of this component. The keys are the isocodes of the language. The values are the labels to be used.

parameters

array

list of the parameters of this component.

parameterGroups

array

list of the possible parameterGroups of this component.

animations

array

list of the animations of this component.

possibleChildren

array

list of the possibleChild objects defining possible children for this component. Each child must have either itemId or componentId. Items must be configurable. Can have a script defining if it is the default. If multiple possible children evaluate default to true, its undefined which one is taken as the default child.

parentDockings

map

contains the definitions for all possible dockings where other components can be docked to this component as children. There can be points, lines, ranges (of points) and lineRanges.

childDockings

map

contains the definitions of all possible dockingPoints where this component can be docked to another component as a child. This contains only points.

addOnSpots

array

contains the definitions of all possible addOnSpots.

subComponents

array

list of possible subcomponents of this component. They can be referenced from the geometryScript and the previewGeometry by the internalId. If the component itself should appear in the partlist additionally to the subcomponents, a backreference is possible but deprecated, numberInPartList and sortInPartList in the component definition itself should be used instead

articleNr

string

the script to calculate the articleNr that should be displayed in the partlist. If the script contains only a string, this string is used as the articleNr. Otherwise the content of the variable "articleNr" is used.

packageSizes

array

list of numbers of packages which are allowed for this component

packaging

array

list of sizes with conditions for adding certain sizes if needed

dimensionings

array

list of all dimensionings that might be used.

siblings

array

contains the definitions of all possible siblingPoints.

geometry

string

the geometry script for this component. For details see the RoomleScript chapter.

geometryHD

string

the geometry script for higher quality of this component. For details see the RoomleScript chapter.

previewGeometry

string

the geometry script for this component in previewmode. For details see the RoomleScript chapter.

environmentGeometry

string

the geometry script for the environment of this component

boundingGeometry

string

the geometry script for the bounds of this component

planInteraction

object

definition about how the object interacts with the plan

data

object

contains the subobject with plugin-specific data

plugin-data

object

data that can be queried with the function getData, getDataOrNull or getDataWithDefault

Measuring of Components

By default the Configurator takes the real bounding box of the geometry as the measurements. If necessary it be can specified a custom definition for the shown measurements with the setBoxForMeasurement command in the onUpdate script. When the command is called, the display of the measurements behaves as if the component would consist only of a simple Cube with the size and offset as given in the command.

setBoxForMeasurement(size: Vector3f, offset: Vector3f);   

Origin of Components

By default the origin of a component is the center of the bounding box. If necessary the it be can be specified a custom definition for the origin with the setOrigin command in the onUpdate script. When the command is called, the origin of the component is set to the given position.

setOrigin(origin: Vector3f);

This can be useful to fix a specific point of the component in place when the size of the component changes.

closed
opened

closed door

closed door

Example - Set Origin

{
    "id": "roomle_script_documentation:set_origin_test",
    "parameters": [
        {
            "key": "opened", "label": "'Open Door'", "type": "Boolean", "defaultValue": false,
            "valueObjects": [ { "value": false, "label": "'Closed'" }, { "value": true, "label": "'Opened'" } ]
        }
    ],
    "onUpdate": "
        setOrigin({0, 50, 0});
        ",
    "geometry": "
        BeginObjGroup('door frame');
        AddCube({50, 100, 1950});
        Copy();
        MoveMatrixBy({950, 0, 0});
        AddCube({1000, 100, 50});
        MoveMatrixBy({0, 0, 1950});
        EndObjGroup();
        SetObjSurface('roomle_script_documentation:white');
        AddCube({900, 50, 1950});
        if (opened) {
            RotateMatrixBy({0, 0, 1},{0, 0, 0}, 75);
        }
        MoveMatrixBy({50, 100, 0});
        SetObjSurface('roomle_script_documentation:white');
        "
}

Docking Components

Components can be docked to each other via dockpoints. Each Component can have any amount of children, but at most one parent which leads to a parent-child treehierachy. It is possible to transfer data from one component to another. Either directly via the Dockingconnection or via Siblingconnections which can be created between any two components. Data is transfered via assignments.

Labels

Components, Parameters, Parameter Groups, Value objects and Dimensioning objects can have a label. A Label is a user friendly name used to represent the object in the user interface. Labels can be defined with either a language map or a script. A label map is a collection of language and label pairs. If the label for a requested language does not exist in a label map, the English label is used. If the English label is requested but does not exist, the key or value of the object is used instead of the label. In a label script, a label will be generated completely dynamically. Once a label script is preset, the label map is ignored. However, the label from the labels map is the input label of the label script.

Label map exsample:

"labels": {
    "de": "Laenge",
    "en": "Length"
}

Label script examples:

"label" : "isRound ? 'Diameter' : 'Length'"
"label" : "
    _.aspect = string(width/height, 2);
    if (width > height) {
        label = 'landscape: ' |  _.aspect;
    } else {
        label =  'portrait: ' |  _.aspect;
    }
"  
"label" : "
    _.aspect = string(width/height, 2);
    if (width > height) {
        return 'landscape: ' |  _.aspect;
    } else {
        return 'portrait: ' |  _.aspect;
    }
"  

Value object label scripts can be combined with the automatically unit formatted values. In the example below, the values label of the "size" parameter is "ø 100 cm" if isRound and "100 cm" otherwise.

{
    "id": "test:diameter",
    "parameters": [
        {
            "key": "size",
            "type": "Decimal",
            "defaultValue": 1000,
            "unitType": "length",
            "valueObjects": [
                {
                    "value": 1000,
                    "label": "isRound ? ‘ø ‘ | label : label",
                    "condition": "true"
                }
            ]
        }
    ]
}

Docking

Each component can define dockingPoints and dockingLines where other components can be docked to (called ParentDockings) and dockingPoints (called ChildDockings) through which this component can be docked to the ParentDockings of other components. Components which are docked to another component A are called the children of A. Through the docking connection the parent can set parameters of the child, e.g. the width of a shelf can be set by the parent frame. The child can also set parameters of the parent. There are three types of assignments: onDock, onUpdate and onUndock.

When a new component is docked the assignments are always first executed on the parent side. Meaning

  • assignmentOnDock in parentDocking

  • assignmentOnDock in childDocking

  • assignmentOnUpdate in parentDocking

  • assignmentOnUpdate in childDocking

Assignments in the onDock Block doesn't trigger recursive onUpdate actions themself.

On configuration changes, only the update assignments from the changed component are executed.

On Undock the order is reversed:

  • assignmentOnUnDock in childDocking

  • assignmentOnUnDock in parentDocking

One ParentDockPoint can only be used by one ChildDockPoint, on the other hand one ParentDockLine can be used by multiple ChildDockPoints at once. The maximum possible Children on a DockLine can be set via the maxChildren property.

Dynamically change docking

If an existing dock-connection and the parent-child relationship becomes invalid (due to a changed condition or changed masks), the child is undocked from their parent. In principle and dogmatically, an undocked child (a child without a parent) and all of its children (the entire child tree) are deleted immediately. However, in order to prevent unnecessary deletion, an alternative docking between the child and the parent is sought and, if found, established (docked) before the children are deleted. This feature enables completely dynamic changes of docking points. When the docking is changed, the child's position is recalculated and automatically changed depending on the new position and rotation, whereby the distance from the new to the old position is not restricted. If there is more than 1 possible new docking position, the position that causes the smallest translation of the child component is selected.

Delete PlanComponent and try to keep the children

If a PlanComponent is deleted and the children of the deleted PlanCmponent should be kept, it must be attempted to dock the children to the parent. A child can only be kept if it can be docked to the parent. Therefore, a matching docking point with a mask that matches the parent's mask must be found. Which child has priority when docking to the parent depends on the "persistent" and "priority" properties of the "parentDockings" to which the children are currently docked. This means that the PlanComponet to be deleted decides via the "persistent" and "priority" properties of the "parentDockings" which children are retained. If "persistent" is set to "false", a child element cannot be docked at all to the parent of its parent. The default value for "persistent" is "true". All chides that may be docked to the parent of their parent are sorted by the attribute "priority" and are attempted to dock in this order. If the attribute "Priority" is not set, this means the lowest priority.

In the following example, component "A" has a child "B". "B" itself has three children "C1", "C2" and "C3". For the parent docking of "B" to "C1", the attribute "persistent" is set to "false". The parent docking of "B" to "C2" has set the attribute "persistent" to "true" and "priority" 1. The parent docking of "B" to "C3" has set the attribute "persistent" to "true" and "priority" 1.

docking delete child

PlanComponent "B" is deleted. The root PlanComponent can only take over one of the 3 children because it has only one docking point. The PlanComponent "C1" is not "persistent", so it is not docked to the root PlanComponent. "C1" is automatically deleted. "C2" has the higher priority than "C3", so that "C2" is docked to the root PlanComponent and "C3" is automatically deleted.

docking delete child result

Collision detection of docked components

Sometimes a newly added component would collide with an existing one. In some cases this is the expected behavior but in other scenarios the collision should not be allowed. It is possible to define a collisionCondition script for DockingPoints, DockingLines, DockingPointRanges and DockingLineRanges. The collisionCondition script defines if a collision with another component is allowed for the corresponding docking. If the collisionCondition script evaluates to false all docking previews of colliding components get discarded, so it is not possible to add a component there. A list (array) of all colliding components can be retrieved inside of the collisionCondition script via collidingComponentIDs. It is an array which contains the IDs of all colliding components, ordered by their overlapping volume from largest to smallest. There are also some additional functions available inside the collisionCondition script:

  • getComponentProperty(key:string, componentId?: int)

  • getBoxOrigin(componentId?: int)

  • getBoxSize(*componentId?: int)

  • getBoxForMeasurementOrigin(componentId?: int)

  • getBoxForMeasurementSize(componentId?: int)

These functions have been extended and optionally accept an additional parameter to retrieve the corresponding data from a specific component via its ID. The IDs inside the collidingComponentIDs variable can be used in this case.

The calculation of the collision detection is based on axis aligned bounding box calculations. So for determination if something is colliding or not the BoundingBox or BoxForMeasurement (if available) get used. This is a very fast and performant calculation but has the drawback that sometimes components can get detected as colliding, even if they aren't, just because their bounding boxes are colliding but not the 3D objects themselves. This is especially the case if 3D objects get rotated.

If the object has a BoundingGeometry defined this geometry gets used and a more complex mesh intersection calculation is taking place which checks if the 3D objects are intersecting each other. This is an exact calculation that checks exactly the 3D meshes against each other, and so is a bit slower but far more precise. If only one of the objects has a BoundingGeometry defined the BoundingBox of the other object gets used for calculation. Be aware that if the component to be docked has a BoundingGeometry defined, the more complex mesh intersection calculation will always take place at docking preview generation.

In the case of DockingLines and DockingLineRanges, the generated lines are clipped based on the projection of the bounding box of the colliding geometry onto the line. This means that the line is segmented and shortened depending on the collisions detected. If none of the remaining line segments is large enough to dock the component in its full size, the line is completely discarded.

Locking of parent dockings

With the property childDeletionLocked it is possible to prevent deletion of child components on a specific parent docking. If this property is set to true it will not be possible to delete the child component docked at this docking. But if the parent component itself gets deleted, the docked child will also get deleted, even if the childDeletionLocked property is set to true. childDeletionLocked supports simple expression/condition scripts.

Siblings

Additionaly to assignments through parent/child connections, components can transfer Data via Siblingpoints. Siblingpoints connect to any other siblingpoint with the same mask and same position somewhere within the object. Therefor its possible to transfer data (parametervalues) directly between two childcomponents on completly seperated branches in the parent-child-hierachy. Like Dokcings, Siblingpoints have onDock, onUpdate and onUndock assignments. Regarding the executionorder during the docking process the logic follows the same rules as with Dockingpoints: first the assignments from the currently existing Component, then the assignment from the newly docked one. First both onDock, then onUpdate. On undock the order is reversed, same as with the DockingPoints.

Docking assigments are always executed prior to sibling assignments.

Self Assignments

Normal assignments (either on docking or siblings) sets the value of a parameter on the other side of the connection. With self assignments one can set values on parameters of the component of the docking/sibling.

Self assignments are always executed after the "normal" assignment and never trigger recursive actions.

onUpdate of Connections

Each connection definition (dockingpoint, dockingline, siblingpoint) may have their own onUpdate. Those are executed after the onUpdate of the component and the calculation of the position but before any other script of the connection is evaluated (condition, assignments). This onUpdate may write values to the connections-context which is then available in the other scripts on this connection. This is usefull when the connection may decide what data from the whole component to use within this context.

Silent Assignments

Normal "On Update"-Assignments lead to a disabling of the parameter which is set from the assignment. This is done because normally a User shouldnt be able to change a Parameter whose value will be override with the next refresh. In some case (e.g. if the change of the value triggers an update of the connected Component which leads to changes in the parameters so that the changed value stays the same after the next refresh) this automatic disabling should be silenced. In this case one can use assignmentsOnUpdateSilent. Those assignments are handled exactly like onUpdate Assignments, but without disabling the parameter. Silent assignments are always applied before the normal assignments (if both exist).

AssignmentScripts

For more complex solutions its possible to define assignmentScripts. Again seperated into onDock, onUnDock and onUpdate, those scripts are executed after the "normal" assignments are done. Within the assignmentScript one has access to the values of both sides of the connection (parent and child in docking, both siblings for siblingpoints). Those are accessilbe via context definition. F.e. the paramter "width" of the other side of the connection is accessible via "other.width" while "self.width" is the own parameter width. In parentDockings the "other." context is also accessible via "child.", in childDockings its "parent." and for Siblings its "sibling."

Dock Ranges

Dockranges provide the ability to create a range of dockPoints without specifing each of them individually. A range is defined by a startingPoint, the stepSize and the endPoint. The step might be one 3D step which is used as a direct increment until the endPoint is reached, or as stepX, stepY and stepZ which creates a raster of points. The condition and assignments are defined for the range, but executed for each generated point seperatly. Within those scripts you can access the position of the point and index within the range via "connection.position" and "connection.index".

AddOn Spots

For a visual hint to the user, its possible to define addOnSpots. An AddOnSpot is a position in the 3D space relative to the current Component where a Plus-Sign is shown. If the User clicks on the sign, the AddOn-View opens.

AddOnSpots have a position and a condition. The mask is defined for future use and has nothing to do with docking masks.

Packaging Size

This feature allows defining a package size for its component. This size will be the amount of all appearances of this component. Description by example:

{
   "id": "comp",
   "parameters": [
      {
           "key": "someParameter",
           "type": "Decimal",
           "visible":true,
           "defaultValue": 1,
           "validValues": [1, 2, 3, 4, 5, 6, 7, 9, 10]
        }, 
      {
           "key": "someOtherParameter",
           "type": "Decimal",
           "visible":true,
           "defaultValue": 0
       }
   ],
   "geometry": "
        ...
   ",
   "parentDockings": {
      ...
   },
   "packageSizes": [5, 3],
   "packaging": [{
       "size": 1,
       "condition": "someOtherParameter == 1"
   }],
   "articleNr": "
     articleNr= 'nr'|someParameter|'x'|packageSize
   ",
   "pricing": [{
       "region": "default",
       "currency": "EUR",
       "price": "
          price= 100*packageSize;
        "
   }]
}  

The possible "packageSizes" are set to [5,3], this means for example if the component is docked 11 times the number of packages with size 5 will be 2 and the number of packages with size 3 will be 1. The number of components will always be first divided into the biggest possible package size and then the next smaller one and next smaller one until the smallest one is reached.

The parameter packaging adds also the size 1 to the "packageSize" if the condition of "someOtherParameter == 1" is true.

The articleNr and pricing provide the packageSize getter in order to be able to assign a specific article number or price per package size.

Dimensioning

The configurator automatically shows the dimensions of the bounding box, or of the boxForMeasurement if defined. Additional dimensioning levels can be defined as objects in the dimensionings array. Every dimensioning has a type, from, to, level and visibility field. If a dimensioning has a label ("label" or "labels") the the calculated value and the unit of the dimensioning is replaced by the label. If the "visbility" script evaluates to false, the dimensioning is not shown.

Dimensioning objects can be applied not only to a single component, but also across components. The “context” attribute must be set for this. The value of the context attribute can be “component”, “object” or “both” (“component” and “object”) and the default value is "component". “object” dimensions that are of the same "type" and the same "level" and that are connected to each other or overlap are combined across components.

Dimensioning definition:

property
default value
description

axis (or type)

"z"

defines where this dimensioning applies (x, y or z)

| from | | defines the begining of the dimensioning | | to | | defines the end of the dimensioning | | level | 0 | the layer of the dimensioning. 0 is the outermost layer | | context | "component" | can be "component", "object" or "both" | | label | | a script to calculate the label of the dimensioning | | labels | | a map containing key-value pairs for the label of this dimensioning. | | visible | true | can be any condition defining if this dimensioning should be shown | | parameterKey | | a key to reference a specific parameter in the dimensioning | | parameterFunction | | a function to calculate the parameter value for the dimensioning |

Example:

{
  "id": "catalogId:componentId",
  ...
  "dimensionings": [
    {
      "type": "z",
      "from": "0",
      "to": "height",
      "level": 0,
      "context": "both"
    },
    {
      "type": "x",
      "from": "0",
      "to": "width",
      "level": 0,
      "context": "component"
    }
  ]
}

Instead of using a static dimensioning definition object with a “visibility” property, a dimensioning can also be created using the AddAbsoluteDimensioning function in the “onUpdate” script. This function creates a dimenisoning object exactly as if it were explicitly specified in the component definition JSON. The default value for the context argument is “component”.

AddAbsoluteDimensioning(
    type: string,
    from: float,
    to: float,
    level: int,
    context?: 'component' | 'object' | 'both',
    label: string,
    parameterKey?: string,
    parameterFunction?: string)

Example:

dimensionings generated with AddAbsoluteDimensioning

Example - AddAbsoluteDimensioning

{
    "id": "roomle_script_documentation:add-absolute-dimensioning",
    "onUpdate": "
        AddAbsoluteDimensioning('x', -1000, 1000, 0);
        AddAbsoluteDimensioning('x', -1000, -500, 1);
        AddAbsoluteDimensioning('x', -500, 0, 1);
        AddAbsoluteDimensioning('x', 0, 500, 1);
        AddAbsoluteDimensioning('x', 500, 1000, 1);
        AddAbsoluteDimensioning('x', -1000, -600, 2);
        AddAbsoluteDimensioning('x', -600, -400, 2);
        AddAbsoluteDimensioning('x', -400, -100, 2);
        AddAbsoluteDimensioning('x', -100, 100, 2);
        AddAbsoluteDimensioning('x', 100, 400, 2);
        AddAbsoluteDimensioning('x', 400, 600, 2);
        AddAbsoluteDimensioning('x', 600, 1000, 2);
        AddAbsoluteDimensioning('z', -500, 500, 0);
        AddAbsoluteDimensioning('z', -500, 0, 1);
        AddAbsoluteDimensioning('z', 0, 500, 1);
        AddAbsoluteDimensioning('z', -500, -100, 2);
        AddAbsoluteDimensioning('z', -100, 100, 2);
        AddAbsoluteDimensioning('z', 100, 500, 2);
    ",
    "geometry": "
        BeginObjGroup('palte');
        AddPlainCube(Vector3f{2000, 1000, 100});
        MoveMatrixBy({-1000, -500, 0});
        SetObjSurface('pfleiderer:r20391_nw');
        AddCylinder(100, 100, 200, 16, bevelWidth = 0);
        MoveMatrixBy({-500, 0, -50});
        SetObjSurface('pfleiderer:u17005_vv');
        MinusOperator();
        AddPlainCube(Vector3f{200, 200, 200});
        MoveMatrixBy({-100, -100, -50});
        SetObjSurface('pfleiderer:u17005_vv');
        MinusOperator();
        AddCylinder(100, 100, 200, 16, bevelWidth = 0);
        MoveMatrixBy({500, 0, -50});
        SetObjSurface('pfleiderer:u17005_vv');
        MinusOperator();
        EndObjGroup();
        RotateMatrixBy({1,0,0},{0,0,0},90);
    "
}

Dimensioning axis

In some cases, the default dimensioning axes ‘x’, ‘y’ and ‘z’ are not suitable for the component. In this case, it is possible to define custom axes for the dimensioning. This is done by defining an object in the "dimensioningAxes" array in the component definition or with the AddAbsoluteDimensioningAxis function in the "onUpdate" script. The “key” is the name of the axis and can be used in a dimensioning object to identify the axis in the same way as a one of default axis. The axis is defined by a "key", an “origin”, a "direction" and a “labelDirection”. The origin is the starting reference point for each dimensioning of this axis. The “direction” defines the direction of the dimensioning. The “direction” and a “labelDirection” together define the plane in which the dimesniongs are drawn.

Dimensioning axis definition:

property
default value
description

key

defines the name mof the axis

orign

{0, 0, 0}

defines the orignm of the dimensioning

diretion

{0, 0, 1}

defines the direction dimensioning

labelDirection

{-1, 0, 0}

defines the upward direction of the label and levels

Example:

{
  "id": "catalogId:componentId",
  ...
  "dimensioningAxes": [
    {
      "key": "leftZ",
      "orign": "{200, 100, 0}",
      "diretion": "{0, 0, 1}",
      "labelDirection": "{0.707, 0.707, 0}"
    }
  ]
}

AddAbsoluteDimensioningAxis function:

AddAbsoluteDimensioningAxis(key: string, origin: Vector3f, direction: Vector3f, labelDirection: Vector3f);

Trapezoid example:

trapezoid dimensioning

Example - Trapezoid dimensioning

{
    "id": "roomle_script_documentation:trapezoid-dimensioning",
    "parameters": [
        {
            "key": "miterAngle",
            "type": "Decimal",
            "defaultValue": 30,
            "enabled": false,
            "visible": false
        }
    ],
    "onUpdate": "
        _.l = 500 * tan(miterAngle * M_PI / 180);
        _.sideLength = sqrt(pow(_.l, 2) + pow(500, 2));
        _.dx = _.l / _.sideLength;
        _.dy = 500 / _.sideLength;
        AddAbsoluteDimensioning('x', -_.l, 500 + _.l, 1);
        AddAbsoluteDimensioning('z', 0, 400, 1);
        AddAbsoluteDimensioningAxis('leftSide', { -_.l, 0, 0}, {_.dx, _.dy, 0}, {-_.dy, _.dx, 0});
        AddAbsoluteDimensioning('leftSide', 0, _.sideLength, 1);
        AddAbsoluteDimensioningAxis('rightSide', {500 + _.l, 0, 0}, {-_.dx, _.dy, 0}, {_.dy, _.dx, 0});
        AddAbsoluteDimensioning('rightSide', 0, _.sideLength, 1);
        AddAbsoluteDimensioningAxis('front', {0, 500, 0}, {1, 0, 0}, {0, 1, 0});
        AddAbsoluteDimensioning('front', 0, 500, 1);
        ",
    "geometry": "
        _.l = 500 * tan(miterAngle * M_PI / 180);
        AddPrism(400, Vector2f[{ -_.l, 0}, {500 + _.l, 0}, {500, 500}, {0, 500}]);
         SetObjSurface(kronospan:0134_bs);
    "
}

Curved shelf example:

curved shelf dimensioning

Example - Curved shelf dimensioning

{
    "id": "roomle_script_documentation:curved-shelf-dimensioning",
    "onUpdate": "
        _.dx = sin(M_PI / 6);
        _.dy = cos(M_PI / 6);
        AddAbsoluteDimensioningAxis('leftZ', {_.dx * 1100, _.dy * 1100, 0}, {0, 0, 1}, {_.dx, _.dy, 0});
        AddAbsoluteDimensioning('leftZ', 0, 850, 1);
        AddAbsoluteDimensioning('leftZ', 50, 400, 2);
        AddAbsoluteDimensioning('leftZ', 450, 800, 2);
        ",
    "geometry": "
        function curvedShape(from, to, height) {
            _.p = [{to * sin(-M_PI / 6), to * cos(-M_PI / 6)}];
            for (_.i = 1; _.i <= 8; _.i++) {
                _.a = -M_PI / 6 + (M_PI / 3) * _.i / 8;
                pushBack(_.p, {to * sin(_.a), to * cos(_.a)});
            }
            for (_.i = 0; _.i <= 8; _.i++) {
                _.a = M_PI / 6 - (M_PI / 3) * _.i / 8;
                pushBack(_.p, {from * sin(_.a), from * cos(_.a)});
            }
            AddPrism(height, _.p, edgeStyle = 'edge');
        }
        BeginObjGroup('curvedShelf');
        curvedShape(600, 1100, 50);
        Copy();
        MoveMatrixBy({0, 0, 400});
        Copy();
        MoveMatrixBy({0, 0, 400});
        AddPlainCube({50, 500, 850});
        MoveMatrixBy({-50, 600, 0});
        RotateMatrixBy({0,0,1}, {0,0,0}, 30);
        AddPlainCube({50, 500, 850});
        MoveMatrixBy({0, 600, 0});
        RotateMatrixBy({0,0,1}, {0,0,0}, -30);
        EndObjGroup();
        SetObjSurface(egger:h3359_st32);
    "
}

Geometry dimensioning

In addition to the absolute dimensioning of components, it is also possible to define dimensions for the geometry of a component in the geometry script. This is done using the AddDimensioning and AddDimensioningAxis functions. The ‘AddDimensioning’ function creates a dimensioning object that is displayed in the 3D view, while the AddDimensioningAxis function defines a separate axis for the dimensioning. An axis defined with AddDimensioningAxis is affected by the matrix transformation functions in the same way as the geometry. Unlike AddAbsoluteDimensioning, the AddDimensioning function cannot use the standard axes 'x', 'y' and 'z', but only the user-defined axes defined with AddDimensioningAxis. The functions ‘AddDimensioning’ and ‘AddDimensioningAxis’ can also be used in the geometry of subcomponents and thus generate dimensions that do not relate directly to the component itself, but to the geometry of the subcomponents.

AddDimensioning(
    type: string,
    from: float,
    to: float,
    level: int,
    context?: 'component' | 'object' | 'both',
    label: string,
    parameterKey?: string,
    parameterFunction?: string)
AddDimensioningAxis(key: string, origin: Vector3f, direction: Vector3f, labelDirection: Vector3f);

Exmaple

geometry dimensioning

Example - Geometry dimensioning

{
    "id": "roomle_script_documentation:bathroom_dimensioning",
    "geometry": "
        BeginObjGroup('carcass');
        AddCube({1000, 500, 20}, edgeStyle = 'chamfer', bevelWidth = 4, material = 'egger:f093_st7');
        MoveMatrixBy({-500, 0, 800});
        AddCube({996, 488, 10}, edgeStyle = 'edge', material = 'egger:f800_st9');
        MoveMatrixBy({-498, 0, 600});
        AddCube({10, 488, 190}, edgeStyle = 'edge', material = 'egger:f800_st9');
        MoveMatrixBy({-498, 0, 610});
        Copy();
        MoveMatrixBy({986, 0, 0});
        AddCube({996, 10, 200}, edgeStyle = 'edge', material = 'egger:f800_st9');
        MoveMatrixBy({-498, 488, 600});
        EndObjGroup();
        AddDimensioningAxis('x', {-500, 500, 600}, {1, 0, 0}, {0, 0, -1});
        AddDimensioningAxis('y', {-500, 500, 600}, {0, -1, 0}, {0, 0, -1});
        AddDimensioningAxis('z', {-500, 500, 600}, {0, 0, 1}, {-1, 0, 0});
        AddDimensioning('x', 0, 1000, 1, 'component');
        AddDimensioning('y', 0, 500, 1, 'component');
        AddDimensioning('z', 0, 220, 1, 'component');
        SubComponent('mirror');
        MoveMatrixBy({0, 20, 1200});
        SubComponent('basin');
        MoveMatrixBy({0, 250, 820});
        SubComponent('water-faucet');
        MoveMatrixBy({0, 250, 820});
        ",
    "environmentGeometry": "
        AddCube({2000, 120, 2400});
        SetObjSurface('roomle_script_documentation:white');
        MoveMatrixBy({-1000, -120, 0});
        ",
    "subComponents": [
        {
            "internalId": "mirror",
            "componentId": "roomle_script_documentation:bathroom_mirror"
        },
        {
            "internalId": "basin",
            "componentId": "roomle_script_documentation:bathroom_basin"
        },
        {
            "internalId": "water-faucet",
            "componentId": "roomle_script_documentation:bathroom_water_faucet"
        }
    ]
}
{
    "id": "roomle_script_documentation:bathroom_mirror",
    "geometry": "
        BeginObjGroup('mirror');
        AddExternalMesh('roomle_script_documentation:bathroom_mirror_Object_0_001',Vector3f{780000,13997.7,779999.9},Vector3f{-390000,13127.5,-0.1});
        SetObjSurface('roomle_script_documentation:bathroom_mirror_material_1');
        AddExternalMesh('roomle_script_documentation:bathroom_mirror_Object_1_001',Vector3f{540000,7515.3,539077.1},Vector3f{-270000,13359.8,120456.9});
        SetObjSurface('roomle_script_documentation:bathroom_mirror_material_2');
        AddExternalMesh('roomle_script_documentation:bathroom_mirror_Object_2_001',Vector3f{549956,48238.4,549956},Vector3f{-274978,-27113.3,115021.9});
        SetObjSurface('roomle_script_documentation:bathroom_mirror_material');
        EndObjGroup();
        ScaleMatrixBy({0.001, 0.001, 0.001});
        AddDimensioningAxis('x', {-375, 54, 750}, {1, 0, 0}, {0, 0, 1});
        AddDimensioningAxis('z', {-375, 54, 0}, {0, 0, 1}, {-1, 0, 0});
        AddDimensioning('x', 0, 750, 1, 'component');
        AddDimensioning('z', 0, 750, 1, 'component');
        "
}
{
    "id": "roomle_script_documentation:bathroom_basin",
    "geometry": "
        BeginObjGroup('basin');
        AddExternalMesh('roomle_script_documentation:bathroom_basin_Sink_01_lambert2_0_001',Vector3f{877.2,996.8,257.8},Vector3f{-438.6,-498.4,0});
        SetObjSurface('roomle_script_documentation:bathroom_basin_lambert2');
        EndObjGroup();
        ScaleMatrixBy({0.5, 0.5, 0.5});
        RotateMatrixBy({0, 0, 1}, {0, 0, 0}, 90);
        AddDimensioningAxis('x', {-996.8/4, 877.2/4, 257.8/2}, {1, 0, 0}, {0, 0, 1});
        AddDimensioningAxis('y', {-996.8/4, 877.2/4, 257.8/2}, {0, -1, 0}, {0, 0, 1});
        AddDimensioningAxis('z', {-996.8/4, 877.2/4, 0}, {0, 0, 1}, {-1, 0, 0});
        AddDimensioning('x', 0, 996.8/2, 1, 'component');
        AddDimensioning('y', 0, 877.2/2, 1, 'component');
        AddDimensioning('z', 0, 257.8/2, 1, 'component');
        "
}
{
    "id": "roomle_script_documentation:bathroom_water_faucet",
    "geometry": "
        BeginObjGroup('basin');
        AddExternalMesh('roomle_script_documentation:bathroom_basin_Sink_01_lambert3_0_001',Vector3f{319.6,87.3,293.2},Vector3f{-384,-43.7,242.5});
        SetObjSurface('roomle_script_documentation:bathroom_basin_lambert3');
        EndObjGroup();
        ScaleMatrixBy({0.5, 0.5, 0.5});
        RotateMatrixBy({0, 0, 1}, {0, 0, 0}, 90);
        AddDimensioningAxis('x', {-43.7/2, -30, 242.5/2+293.2/2}, {1, 0, 0}, {0, 0, 1});
        AddDimensioningAxis('y', {-43.7/2, -30, 242.5/2+293.2/2}, {0, -1, 0}, {0, 0, 1});
        AddDimensioningAxis('z', {-43.7/2, -30, 242.5/2}, {0, 0, 1}, {-1, 0, 0});
        AddDimensioning('x', 0, 87.3/2, 1, 'component');
        AddDimensioning('y', 0, 319.6/2, 1, 'component');
        AddDimensioning('z', 0, 293.2/2, 1, 'component');
        "
}

Interactive dimensioning

A dimensioning can also be made interactive. This allows the user to modify the dimensions directly in the 3D view.

interactive dimensioning

Example - Interactive dimensioning

To make a dimension interactive, it must be linked to a parameter. This can be done by setting the "parameterKey" property in the dimensioning definition or by using the "parameterKey" argument in the AddAbsoluteDimensioning or AddDimensioning function. When a value is entered in the edit field, this value is assigned to the parameter.

For example, the following dimensioning is linked to the “width” parameter. So if the value 500 mm is entered in the input field, this value is assigned to the “width” parameter (width = 500).

AddDimensioning('x', 0, width, 1, 'component', '', 'width');

This is not always sufficient, as not every dimension is directly represented by a parameter. Therefore, it is possible to modify the value with a function before it is assigned. This can be done by setting the parameterFunction property in the dimensioning definition or by using the parameterFunction argument in the AddAbsoluteDimensioning or AddDimensioning function. The function must be a valid script that returns a value and has exactly one argument.

For example the following dimensioning is linked to the “width” parameter and the "addThickness" function. So if the value 500 mm is entered in the input field, this value is modifed by the "addThickness" function and the result is assigned to the “width” parameter (width = addThickness(500)).

"functions": [
    {
        "key": "addThickness",
        "arguments": [
            { "key": "input" }
        ],
        "script": "return input + 60;"
    }
],
AddDimensioning('innerX', 0, width-60, 1, 'component', '', 'width', 'addThickness');

Plan interaction

In order for configurations to interact with objects in the plan such as walls, it must be possible to define the part of the geometries that can be influenced. This information must be taken into account when placing a configuration in a plan.

Intersect with wallssee

see Interaction between Configurator and Planner - Intersect with wallsl

Snap levels

see Interaction between Configurator and Planner - Snap Level

Snap vectors

see Interaction between Configurator and Planner - Snap Vector

Wall thickness

see Interaction between Configurator and Planner - Wall Thickness

Configuration format

The Configuration describes an actual component in a plan with the set parameters and the docked children.

{
  "componentId": "<id der Root Komponente>",
  "parameters": {
        "paramKey1": "paramValue1", 
        "paramKey2": "paramValue2", ? 
  },
  "children": [{
         "componentId": "",
         "parameters": {
                "paramKey1": "paramValue1", 
                "paramKey2": "paramValue2", ... 
        },
        
// Docking point sample
        "dockParent": "[ {x,y,z} ]",  
        "dockPosition": "{x,y,z}",
        "dockChild": "{x,y,z}",
      
// Docking line sample
        "dockParent": "[ {x,y,z}, {x,y,z} ]", 
        "dockPosition": "{x,y,z}",
        "dockChild": "{x,y,z}",

        "children": [ ... ]
    }]
}
Tag name
Type
Description

componentId

string

the id of outermost component.

parameters

map

the values to be set for the parameters of this component. Its a Map of key-value pairs where the key is the key of the parameter and the value is the value to be set.

children

array

contains recursivly the definition for all components which are docked to this component.

children/dockChild

string

defines the dockingPoint (one of the points defined in ChildDockings) used to dock this component to its parent. It is defined throu the position of the dockingPoint.

children/dockParent

string

defines the docking of the parent (from the possible dockings defined in ParentDockings of the parent) used to dock this component to. For a DockingPoint the Array contains one point (position of the point), for a DockingLine it contains two points (the start and end point of the line). The positions are within the coordinate system of the parent.

children/dockPosition

string

defines the actual position of the dockingPoint within the coordinate system of the parent. This is used to define the actual position on a dockingLine.

A note to the docking logic: The positions should be stated with 2 digits floating point precision. When matching the points with given docking points from the component definition, they are rounded to 1 digit.

Requirements

There are a few requirements to consider when defining the configuration definition for Components. Since Roomle provides realtime visualization on different plattforms, the models must not become to complex. In general the same rules apply as for static items. When talking about 3D visualization the final number of triangles is the main factor. The less triangles the better the performance. On the other hand less triangles may mean less quality. Therefore one should thrive for the perfect tradeoff between quality and performance.

We define recommendations for preferred average values and upper boundaries that must not be exceeded.

Items

Since Components can be combined to items (via docking logic) of a big variety in size and complexity are possible. Nevertheless the average trianglecount should be between 2000 and 3000. Any Item must not exceed 10000 triangles.

For these values only the rendered triangles are counted, meaning the geometry parts send to the 3D engine for a given parameter combination. Meshes and Primitives which are in the geometryscript but not send to the Engine because of conditions in the script are not counted. Because of the nature of primitives, they should always be preferred to meshes wherever possible.

Geometryfunction
trianglecount desktop
trianglecount mobile

AddCube

43

12

AddSphere

Depending on the maximum size per dimension: < 10 mm: up to 80 < 100 mm: up to 290 < 1000 mm: up to 1100 < 3000 mm: up to 2100

same

AddCylinder

8*CircleSegments - 4

4*CircleSegments - 4

AddPrism

8*NumberOfVertices - 4

4*NumberOfVertices - 4

AddRectangle

2

2

AddMesh

as defined in the call if no indices provided: nrVertices/3

as defined in the call if no indices provided: nrVertices/3

Configuration Definition

Beside the restrictions on triangles for realtime rendering performance, restriction on filesize for the configuration format apply. This is needed to ensure parsing and execution speed during interaction. Configuration definition should have less than 10k characters on average and must not exceed 500k characters. For these values all characters in the json-file are counted. To fullfill those boundaries one should try to use primitives wherever possible.

Textures

Since imagefiles are always heavy on the memory usage, please keep the usage of textures as little as possible.

Regarding imagefiles for the textures, the same rules as for textures of static items applies:

Imagefiles must be JPEG or PNG with a maximum size of 2048x2048 pixels. The recommended size is below 512x512 pixels. For better performance consider keeping the imagesize a power of two (e.g. 32,64,128...), not necessarily square and use the mm dimensions for scaling.

JSON Objects

This chapter defines all possible JSON Objects used within a Component definition or Configuration. A "?" denotes an optional field.

Script-types

For some properties it is possible to pass a Script, Expression or Condition instead of a simple constant value. The different script types are:

  • Script: Complex scripts

    • e.g.: "value1 = 1; value2 = 2; if (value1 < value2) {return true;}"

  • Expression: Simple expression that evaluates to a specific value

    • e.g.: "value1 - value2"

  • Condition: Similar to expression but always evaluates to true or false

    • e.g.: "value1 >= value2"

Currency: "EUR"|"GBP"|"USD"|...
Region: "default"|"at"|"uk"|"us"...
Language: "en"|"de"|...
Type: "Integer"|"Decimal"|"String"|"Boolean"|"Material"
UnitType: "length"|"area"|"count"|"angle"
Value: long|float|String|true|false
Script<ResultType>: String
Range : {
  "valueFrom": Expression<Decimal>,
  "valueTo": Expression<Decimal>,
  "step": Expression<Decimal>,
  "unitRelativeStep": Boolean?(false),
}
ParameterGroup : {
  "key": String,
  "label": Script<String>?,
  "labels": { (Language(_Country)?: String)+ },
  "collapsed": Condition<Boolean>?(false),
  "sort": Script<Long>?(0)
}
Parameter : {
  "key": String,
  "type": Type,
  "sort": long,
  "unitType": UnitType,
  "value": Value,
  "label": Script<String>?,
  "labels": { (Language(_Country)?: String)+ },
  "defaultValue": Value?,
  "enabled": Condition<Boolean>?(true),
  "visible": Condition<Boolean>?(true),
  "visibleAsGlobal": Condition<Boolean>?(true),
  "global": Boolean?(false),
  "visibleInPlanner": Boolean?(false),
  "volatile": Boolean?(false),
  "highlighted": Boolean?(false),
  "visibleInPartList": Condition<Boolean>?,
  "validGroups": [ String* ]?,
  "validRange": Range?,
  "validValues": [ Value* ]?,
  "conditionalGroups":[ ValueObject* ]?,
  "valueObjects":[ ValueObject* ]?,
  "onValueChange":Script?,
  "group":Expression<String>?,
  "level": Integer,
  "levelCondition": Script<Boolean>?
}
AnimationAction : {
  "key": String,
  "label": Script<String>?,
  "labels": { (Language(_Country)?: String)+ },
  "type": 'origin'/'matrix'
}
Animation : {
  "key": String,
  "label": Script<String>?,
  "labels": { (Language(_Country)?: String)+ },
  "group": Expression<String>?,
  "sort": long,
  "enabled": Condition<Boolean>?(true),
  "visible": Condition<Boolean>?(true),
  "visibleAsGlobal": Condition<Boolean>?(false),
  "visibleInPlanner": Boolean?(false),
  "global": Boolean?(false),
  "level": long,
  "actions": [ AnimationAction* ]
}
ValueObject  {
  "value": Value,
  "label": Script<String>?,
  "labels": { (Language(_Country)?: String)+ }?,
  "condition": Script<Boolean>?,
  "thumbnail":String?
}
ConnectionWithAssignment  {
 "position": Expression<Vector3f>,
 "maxConnections": Expression<long>?(1),
  "mask": Expression<String>,
  "onUpdate": Script?,
  "assignmentsOnDock": { String:Script< String>,...},
  "assignmentsOnUpdate": { String:Script< String>,...},
  "assignmentsOnUpdateSilent": { String:Script< String>,...},
  "assignmentsOnUnDock": { String:Script< String>,...},
  "selfAssignments": {
    "onDock": { String:Script< String>,...},
    "onUpdate": { String:Script< String>,...},
    "onUnDock": { String:Script< String>,...}
  },
  "assignmentScripts": {
    "onDock": Script,
    "onUpdate": Script,
    "onUnDock": Script
  },
  "persistent": Condition<Boolean>,
  "priority": Expression<Integer>,
  "childDeletionLocked": Condition<Boolean>(false),
}
DockingPoint : ConnectionWithAssignment {
  "rotation": Expression<Vector3f>,
  "condition": Script<Boolean>?(true),
  "collisionCondition": Script<Boolean>?(true),
  "rotationaxis": Expression<Vector3f>,
  "rotationangle": Expression<float>,
}
DockingRange  {
  "stepX": Expression<float>,
  "stepY": Expression<float>,
  "stepZ": Expression<float>,
  "stepEnd": Expression<Vector3f>
}
DockingPointRange : DockingPoint, DockingRange  {
}
DockingLine : DockingPoint {
  "positionTo": Expression<Vector3f>,
  "maxChildren":Expression<long>?(Inf)
}
DockingLineRange : DockingLine, DockingRange {
}
ParentDockings : {
  "points":[ DockingPoint* ],
  "lines": [ DockingLine* ],
  "ranges":[ DockingPointRange* ],
  "lineRanges": [ DockingLineRange* ]
}
ChildDockings : {
  "points":[ DockingPoint* ]
}
SiblingPoint : ConnectionWithAssignment {
}
AddOnSpot {
  "position": Expression<Vector3f>,
  "mask": String,
  "visible": Condition<Boolean>,
  "condition": Script<Boolean>?(true)
}
PriceList : {
  "region": Region,
  "currency": Currency,
  "retailPriceDependsOnCustomerPrice": Boolean?(false),
  "price": Script<float>,
  "retailPrice": Script<float>
}
Superseding : {
  "type": String,
  "key": String,
  "overrides": SupersedingOverride?
}
SupersedingOverride : {
  "key": String,
  "group": String?
}
SubComponent : {
  "internalId": String,
  "componentId": String,
  "assignments": { String:Script< String>,...},
  "numberInPartList":Script<integer>,
  "active":Script<Boolean>?(true),
  "isMain":Script<Boolean>?(false),
  "supersedings":[ Superseding* ],
  "sort": Script<Long>?(-1)
}
PossibleChild : {
  "itemId": String?,
  "componentId": String?,
  "condition": Script<Boolean>?(true),
  "default": Script<Boolean>?(false),
  "group": Expression<String>,
  "visible": Condition<Boolean>?,
  "visibleAsGlobal": Condition<Boolean>?,
  "level": Integer
}
Component : {
  "id": String,
  "valid": Script<Boolean>?(true),
  "label": Script<String>?,
  "labels": { (Language(_Country)?: String)+ },
  "sortOnLoad": Integer,
  "parameters": [ Parameter* ],
  "animations": [ Animation* ],
  "parameterGroups": [ ParameterGroup* ]?,
  "possibleChildren": [PossibleChild*],
  "parentDockings": ParentDockings?,
  "childDockings": ChildDockings?,
  "siblings":[ SiblingPoint* ]?,
  "addOnSpots":[ AddOnSpot* ]?,
  "geometry": Script,
  "geometryHD": Script?,
  "previewGeometry": Script,
  "environmentGeometry": Script?,
  "boundingGeometry": Script?,
  "packageSizes": Array<Integer>,
  "packaging": [Packaging+],
  "dimensioning": [ Dimensioning*]?,
  "pricing": [ PriceList+ ],
  "articleNr":Script<String>,
  "numberInPartList":Script<integer>?(1),
  "sortInPartList": Script<Long>?(-1),
  "subComponents":[SubComponent*],
  "onUpdate":Script?,
  "planInteraction": PlanInteraction,
  "plugin-data": PluginData?
}
ParameterValues  {
  String: String,... 
}
Configuration : {
  "componentId": String,
  "parameters": ParameterValues,
  "dockParent": Expression<Vector3fArray>,
  "dockPosition": Expression<Vector3f>,
  "dockChild":Expression<Vector3f>,
  "children": [Configuration*]?
}
Packaging : {
  "size": Integer,
  "condition": Script<Boolean>
}
Dimensioning : {
  "type": String,
  "from": Script<Float>,
  "to": Script<Float>,
  "level": Integer,
  "context": String,
  "label": Script<String>?,
  "labels": { (Language(_Country)?: String)+ },
  "visible": Script<Boolean>
}
PlanInteraction: {
  "intersectWithWalls": Condition<Boolean>,
  "fixedSnapLevels": Condition<Boolean>,
  "snapLevels": [
      {
          "level": Expression<Decimal>,
          "default": Condition<Boolean>,
          "enabled": Condition<Boolean>
      }
  ],
  "snapVectors": [
      {
          "positionFrom": Expression(Vector3f),
          "positionTo": ExpressionVector3f),
          "type": String,
          "enabled": Condition(Boolean)
      }
  ]
}

Configuration

Loading Configurations

By default, the order in which the components of a configuration are loaded and docked is undefined and arbitrary This may lead to undesirable behavior in relation to the code in the component definition scripts. To avoid this, the sortOnLoad property can be used to define the order in which the components are loaded. The sortOnLoad property is a constant integer value that defines the order in which the components are docked. Once the components of the configuration have the sortOnLoad property defined the components are sorted by this property before they are docked. Components without the sortOnLoad property are docked after the components with the sortOnLoad property. If the none of the components have the sortOnLoad property defined, the order in which the components are docked is arbitrary.

Geometry Features

There exist a set of functions for generating geometry objects for this component. Each function has optional Parameters to modify the generation of the uv-Coordinates for the object. This is relevant for the placement of textures.

A uvScale of 2 means that the uv-Coordinates are scaled by 2 which results in the texture being shown with half the size. Same goes for the uv-Offset and Rotation: They modifiy the position and orientation of the uv-Coordinates, which in return affects the placement of the texture in the "other" direction.

For the AddMesh function one can provide explicit uvCoord and normals. The number of uvCoords must match the number of indices provided. The number of normals must match the number of vertices provided.

Vertices, uvCoords and uvOffset ist always in mm.

SubComponent injects the corrensponding geometry of the given Subcomponent. For geometry, geometryHD, previewGeometry, environmentGeometry or boundingGeometry scripts the corresponding geometry, geometryHD, previewGeometry, environmentGeometry or boundingGeometry of the sub component is injected.

Keyword arguments

It is possible to pass parameters for geometry functions via keyword arguments. So it is not needed to input all parameters with default values just to set the last one, instead it is possible to pass the parameter name and value to set it. So for example if one wants to just set the bevel value, instead of writing AddCube(Vector3f{1000, 1000, 1000}, Vector2f{0, 0}, 0, Vector2f{0, 0}, 100); it is possible to just write AddCube(Vector3f{1000, 1000, 1000}, bevelWidth=100); instead. The paramter names can be looked up in the list below.

This is currently only possible for geometry functions of simple geometries, not meshes or external meshes. Keyword arguments are supported for the following functions:

  • AddCube

  • AddPlainCube

  • AddCylinder

  • AddSphere

  • AddPrism

  • AddRectangle

For custom functions, refer to Custom Functions - Keyword Arguments.

Geometry faces indices

Basic geometry funtions support passing of multiple individual properties via arrays for some parameters (like materials or uvTransforms). The index of the property inside the array specifies the face of the geometry the property gets applied to. These indices are defined in the core and differ based on the geometry function.

To get to know which indices correspond to which face, please refer to the the functions reference:

Material parameter

All basic geometry functions support passing a list (array) of material IDs directly in the constructor, either via array of strings or keyword arguments. If different materials get passed, the geometry gets created with the provided different materials. The order of the IDs inside the array is defined as mentioned above in the section Geometry faces indices.

If a material for a side gets provided via keyword arguments but no base material ("material") was given an error message will be logged and the base material gets set to the default value (empty string which results in plain white in the renderer). Some geometries, like prism or cylinder, have no left, right, front and back, but only a side/mantle that goes all around. In this case the material for the side has to be set via the 'materialFront' property or corresponding index in the array, all other provided side materials will be ignored.

If setObjSurface(...) function gets called after component creation, all previously defined materials get overridden.

Cube

Or materials to pass whole array as keyword argument.

So if a for example a cube gets created with the following code:

materials = ['isdt:red', 'isdt:green', 'isdt:blue', 'isdt:black', 'isdt:cyan', 'isdt:magenta', 'isdt:yellow', 'isdt:white'];
AddCube(Vector3f{1000, 1000, 1000}, Vector2f{1, 1}, 0, Vector2f{0, 0}, 50, materials);

The result is a cube with a green bevel, a blue top, black bottom, cyan front, magenta back, yellow left side and white right side. The first member in the array is always the base material, so if not all sides are explicitly set to a material these sides get the base material assigned.

So if for example if a cube gets created with the following code:

materials = ['isdt:red', 'isdt:green', 'isdt:blue'];
AddCube(Vector3f{1000, 1000, 1000}, Vector2f{1, 1}, 0, Vector2f{0, 0}, 50, materials);

The result will be a red cube with a green bevel and a blue top.

It is also possible to set the materials via keyword arguments, so the code could look like this:

AddCube(Vector3f{1000, 1000, 1000}, bevelWidth = 50, material = 'isdt:black', materialBevel = 'isdt:red');

Or pass an array as keyword argument:

mat = ['isdt:black', 'isdt:red'];
AddCube(Vector3f{1000, 1000, 1000}, bevelWidth = 50, materials = mat);

These would result in a black cube with a red bevel.

Prism

Geometry Face
Index
Keyword

BASE

0

material

BEVEL

1

materialBevel

TOP

2

materialTop

BOTTOM

3

materialBottom

SIDE

4

materialSide

SIDE #N

materialSide0, materialSide1, materialSide2, ...

Prisim with individual materials for each side:

{
    "id": "roomle_script_documentation:prism_side_face_material",
    "geometry": "
        AddPrism(
            500,
            [{0, 500}, {476, 155}, {294, -405}, {-294, -405}, {-476, 155}],
            bevelWidth = 20,
            material = 'roomle_script_documentation:black',
            materialBevel = 'roomle_script_documentation:white',
            materialSide0 = 'roomle_script_documentation:red',
            materialSide1 = 'roomle_script_documentation:green',
            materialSide2 = 'roomle_script_documentation:blue',
            materialSide3 = 'roomle_script_documentation:yellow',
            materialSide4 = 'roomle_script_documentation:magenta'
        );
    "
}

Example - prism with individual material on either side

configurable object position in plan

Prisim with individual uv transformation for each side:

{
    "id": "roomle_script_documentation:prism_side_face_uv",
    "geometry": "
        materials = ['egger:f021_st75', 'egger:f021_st75', 'egger:f021_st75', 'egger:f021_st75', 'egger:f021_st75', 'egger:h193_st12', 'egger:h194_st12', 'egger:h197_st10', 'egger:h2033_st10', 'egger:h2033_st10'];
        AddPrism(
            500,
            [{0, 500}, {476, 155}, {294, -405}, {-294, -405}, {-476, 155}],
            [{1,1}, {1,1}, {1,1}, {1,1}, {1,1}, {1,1}, {1,1}, {1,1}, {1,1}],
            [0, 0, 0, 0, 0, 0, 36, 72, 108, 144],
            [{0,0}, {0,0}, {0,0}, {0,0}, {0,0}, {0,0}, {0,0}, {0,0}, {0,0}, {0,0}],
            20,
            materials
        );
    "
}

Example - prism with individual uv transformation on either side

configurable object position in plan

UvTransform parameters

Similar to the multi-material arrays for geometry construtor functions it is possible to pass multiple different UV-transforms for all basic geometries. That means that all three different UV-transforms can be passed via a single value, to apply the same UV-transform to all faces, or as arrays of values, to apply different UV-transforms to different faces. So for example a uvScale can be passed as single value like Vector2f{2, 2} to apply a 2-times scaling to all faces of the geometry, or as multiple value array [{2, 2}, {3, 3}, {4, 4}] to apply different UV-scalings to different faces of the geometry. The order of the transforms inside the array is defined the same way as it is for the multi-material-arrays and can be seen in the section Geometry faces indices.

If SetUvTransform(...) function gets called after component creation, the transform gets added to the existing one.

EdgeStyle

With the edgeStyle keyword argument it is possible to set the edge style of the geometry. The available values are edge, chamfer, fillet or defeult.

value
description

edge

Creates a sharp edge, no beveling.

chamfer

Creates a chamfered edge, where the edge gets cut off at an angle of 45°.

fillet

Creates a rounded edge, where the edge gets rounded with a radius of the bevel width.

default

Default edges for backward compatibility. Creates a chamfer edge with fillet normal vectors.

edge style

Cube

{
    "id": "roomle_script_documentation:cube_edge_style",
    "geometry": "
        AddCube(
            {1000, 1000, 1000},
            bevelWidth = 250,
            edgeStyle = 'fillet',
            material = 'kronospan:k411_oe',
            materialBevel = 'kronospan:k353_rt'
        );
    "
}
cube with edge style

Example - cube with edge style

Prism

{
    "id": "roomle_script_documentation:prism_edge_style",
    "geometry": "
        _.p = [{1000, 0}];
        for (_.i = 1; _.i < 6; _.i++) {
            _.a = 2 * M_PI * _.i / 6;
            pushBack(_.p, {1000 * cos(_.a), 1000 * sin(_.a)});
        }
        AddPrism(
            1000,
            _.p,
            bevelWidth = 300,
            edgeStyle = 'chamfer',
            material = 'egger:h1344_st32',
            materialBevel = 'egger:f206_pm'
        );
    "
}
prism with edge style

Example - cube with edge style

Cylinder

{
    "id": "roomle_script_documentation:cylinder_edge_style",
    "geometry": "
        AddCylinder(
            500,
            500,
            1000,
            32,
            bevelWidth = 250,
            edgeStyle = 'fillet',
            material = 'pfleiderer:r30023_vv',
            materialBevel = 'pfleiderer:s63028_xmsm'
        );
    "
}
cylinder with edge style

Example - cube with edge style

Cone, truncated Cone

cone geometry
{
    "id": "catalog_id:component",
    "geometry": "
        AddCylinder(
            600,
            160,
            1000,
            32,
            bevelWidth = 200,
            edgeStyle = 'fillet',
            material = 'egger:h3359_st32',
            materialBevel = 'egger:f093_st7'
        );
    "
}
cone with edge style

Example - cube with edge style

Individual edge Style and width

It is possible to define the edge style and width for each side of the geometry individually. This can be done with the keyword arguments edgeStyleTop, edgeStyleBottom, edgeStyleSide and edgeWidthTop, edgeWidthBottom, edgeWidthSide. The allowed values for the edge styles are the same as for the edgeStyle keyword argument, so edge, chamfer, fillet or default. However, unlike with edgeStyle the value for edgeStyleTop, edgeStyleBottom, edgeStyleSide cannot only be a single value but also an array of values, where the index of the value inside the array defines the edge of the geometry it gets applied to. If the value is not an array but a single value, it applies to all edges in the area. In addtion a spcific edge cann be addressed by using the keywords edgeStyleTopyN, edgeStyleBottomN, edgeStyleSideN, edgeWidthTopN, edgeWidthBottomN, or edgeWidthSideN, wher N is the index of the edge. This allows the edge style and width to be defined in a structured manner. For example, the general edge style and the general edge width can be defined with edgeStyle and bevelWidth and additionally a different style and/or width for the top can be defined with edgeStyleTop or edgeWidthTop and on top a specific edge can be defined with edgeStyleTop0, edgeStyleTop1, etc. or edgeWidthTop0, edgeWidthTop1, etc. This allows for a very flexible definition of the edge style and width for each side of the geometry.

Cube

argument
type
description

bevelWidth

number

general edge width

edgeWidthTop

number | number[4]

width for all top edges or array of individual widths for the top edges in the order [left, front, right, back]

edgeWidthTop0, edgeWidthTop1, edgeWidthTop2, edgeWidthTop3

number

with for the individual top edges, where 0 is left, 1 is front, 2 is right and 3 is back

edgeWidthBottom

number | number[4]

width for all bottom edges or array of individual widths for the bottom edges in the order [left, front, right, back]

edgeWidthBottom0, edgeWidthBottom1, edgeWidthBottom2, edgeWidthBottom3

number

width for the individual bottom edges, where 0 is left, 1 is front, 2 is right and 3 is back

edgeWidthSide

number | number[4]

width for all side edges or array of individual widths for for the side edges in the order [left-front, right-front, right-back, left-back]

edgeWidthSide0, edgeWidthSide1, edgeWidthSide2, edgeWidthSide3

number

width for the individual side edges, where 0 is left-front, 1 is right-front, 2 is right-back and 3 is left-back

edgeStyle

string

general edge style

edgeStyleTop

string | string[4]

style for all top edges or array of individual styles for the top edges in the order [left, front, right, back]

edgeStyleTop0, edgeStyleTop1, edgeStyleTop2, edgeStyleTop3

string

style for the individual top edges, where 0 is left, 1 is front, 2 is right and 3 is back

edgeStyleBottom

string | string[4]

style for all bottom edges or array of individual styles for the bottom edges in the order [left, front, right, back]

edgeStyleBottom0, edgeStyleBottom1, edgeStyleBottom2, edgeStyleBottom3

string

style for the individual bottom edges, where 0 is left, 1 is front, 2 is right and 3 is back

edgeStyleSide

string | string[4]

style for all side edges or array of individual styles for the side edges in the order [left-front, right-front, right-back, left-back]

edgeStyleSide0, edgeStyleSide1, edgeStyleSide2, edgeStyleSide3

string

style for the individual side edges, where 0 is left-front, 1 is right-front, 2 is right-back and 3 is left-back

Example - cube with individual edge style

{
    "id": "catalog_id:component",
    "geometry": "
        AddCube(
            {2000, 1500, 1000},
            edgeWidthTop = 200,
            edgeWidthBottom = 0,
            edgeWidthSide = 300,
            edgeStyleTop = 'chamfer',
            edgeStyleBottom = 'edge',
            edgeStyleSide = 'fillet',
            material = 'pfleiderer:s63009_cm',
            materialBevel = 'pfleiderer:f76037_em'
        );
    "
}
cube with individual edge style

Prism

argument
type
description

bevelWidth

number

general edge width

edgeWidthTop

number | number[N]

width for all top edges or array of individual widths for the top edges, where “N” is the number of sides of the prism

edgeWidthTop0, edgeWidthTop1, ...

number

width for the individual top edges, where 0 is left, 1 is front, 2 is right and 3 is back

edgeWidthBottom

number | number[N]

width for all bottom edges or array of individual widths for the bottom edges, where “N” is the number of sides of the prism

edgeWidthBottom0, edgeWidthBottom1, ...

number

width for the individual bottom edges, where 0 is left, 1 is front, 2 is right and 3 is back

edgeStyle

string

general edge style

edgeStyleTop

string | string[N]

style for all top edges or array of individual styles for the top edges in the order [left, front, right, back]

edgeStyleTop0, edgeStyleTop1, ...

string

style for the individual top edges, where “N” is the number of sides of the prism

edgeStyleBottom

string | string[N]

style for all bottom edges or array of individual styles for the bottom edges in the order [left, front, right, back]

edgeStyleBottom0, edgeStyleBottom1, ...

string

style for the individual bottom edges, where “N” is the number of sides of the prism

Example - prism with individual edge style

{
    "id": "catalog_id:component",
    "geometry": "
        AddPrism(
            120,
            [{0, 0}, {0, 800}, {1150, 800}, {1200, 850}, {1200, 1150}, {1250, 1200}, {2000, 1200}, {2000, 0}],
            edgeWidthTop = 50,
            edgeStyle = 'edge',
            edgeStyleTop = ['edge', 'chamfer', 'chamfer', 'chamfer', 'chamfer', 'chamfer', 'edge', 'edge'],
            edgeWidthBottom = 0,
            edgeStyleBottom = 'edge',
            material = 'egger:u311_st9',
            materialTop = 'egger:f030_st75',
            materialBevel = 'egger:h3408_st38',
            materialSide1 = 'egger:h3408_st38',
            materialSide2 = 'egger:h3408_st38',
            materialSide3 = 'egger:h3408_st38',
            materialSide4 = 'egger:h3408_st38',
            materialSide5 = 'egger:h3408_st38'
        );
    "
}
prism with individual edge style

Cylinder

argument
type
description

bevelWidth

number

general edge width

edgeWidthTop

number

width for the top edge

edgeWidthBottom

number

width for the bottom edge

edgeStyle

string

general edge style

edgeStyleTop

string

style for the top edge

edgeStyleBottom

string

style for the bottom edge

Example - cylinder with individual edge style

{
    "id": "catalog_id:component",
    "geometry": "
        AddCylinder(
            600,
            600,
            1000,
            32,
            edgeWidthTop = 100,
            edgeWidthBottom = 200,
            edgeStyleTop = 'chamfer',
            edgeStyleBottom = 'fillet',
            material = 'kronospan:4298_ue',
            materialBevel = 'kronospan:k082_pw'
        );
    "
}
cylinder with individual edge style

Prism with cut-outs

It is possible to create a prism with cut-outs (holes) in the geometry. To do this, an array of polygons is passed to the AddPrism function. Polygons are defined as arrays of points, where each point is a Vector2f. The polygon defines the outline and the holes of the prism. The function AddPrism creates a prism with the specified outline and holes, subtracting the holes from the prism geometry. Unlike the CSG MINUS operator, the cut-outs are not created by a 3D operation, but by defining the holes in the outline of the prism.

Examples:

{
    "id": "roomle_script_documentation:prism-with-hole",
    "geometry": "
        _.outline = [{-300, -600}, {300, -600}];
        _.n = 64;
        for (_.i = 0; _.i < _.n / 2; _.i++) {
            _.a = M_PI * _.i / (_.n / 2);
            pushBack(_.outline, {300 * cos(_.a), 300 * sin(_.a)});
        }
        _.hole = [{180, 0}];
        for (_.i = 1; _.i < _.n; _.i++) {
            _.a = 2 * M_PI * _.i / _.n;
            pushBack(_.hole, {180 * cos(_.a), 180 * sin(_.a)});
        }
        AddPrism(
            100,
            [_.outline, _.hole],
            bevelWidth = 50,
            edgeStyle = 'chamfer'
        );
        SetObjSurface('pfleiderer:f76001_sd');
        RotateMatrixBy({1,0,0},{0,0,0},90);
    "
}
prism with hole

Example - Prism with hole

{
    "id": "catalog_id:component",
    "geometry": "
        _.outline = [{0, 0}, {0, 800}, {1150, 800}, {1200, 850}, {1200, 1150}, {1250, 1200}, {2000, 1200}, {2000, 0}];
        _.hole1 = [{200, 200}, {200, 600}, {600, 600}, {600, 200}];
        _.hole2 = [{700, 200}, {700, 600}, {1100, 600}, {1100, 200}];
        _.polygons = [_.outline, _.hole1, _.hole2];
        AddPrism(
            120,
            _.polygons,
            edgeWidthTop = 50,
            edgeStyle = 'edge',
            edgeStyleTop = ['edge', 'chamfer', 'chamfer', 'chamfer', 'chamfer', 'chamfer', 'edge', 'edge'],
            edgeWidthBottom = 0,
            edgeStyleBottom = 'edge',
            material = 'egger:u311_st9',
            materialTop = 'egger:f030_st75',
            materialBevel = 'egger:h3408_st38',
            materialSide1 = 'egger:h3408_st38',
            materialSide2 = 'egger:h3408_st38',
            materialSide3 = 'egger:h3408_st38',
            materialSide4 = 'egger:h3408_st38',
            materialSide5 = 'egger:h3408_st38'
        );
    "
}
wortk top with cut-outs

Example - Worktop with cutouts

Material Attributes

It is possible to override a representation property of the material for a specific geometry. This avoids the need to create multiple materials in the database when only one property (e.g. colour) is different. For example, a basic "plastic" material can be added to the DB, but the actual colour of the object is set in Roomle Script. This can be done with the SetObjSurfaceAttribute function, where a key/value pair is specified that overrides a property of the current object material. The colour of the material can be specified with a hexadecimal colour representation ('#ff8000', 'rgb(255,128,0)', 'rgb(50%,0%,100%)', 'hsl(0,100%,50%)'). e.g.:

SetObjSurface(mateialId);
SetObjSurfaceAttribute('color', '#ff0000');
SetObjSurfaceAttribute('alpha', 0.5);

Animations Matrix

Component definitions are asset that are used to create configurations and display them in the 3D scene, but the component definition does not trigger any actions. Component definitions only define which actions can be triggered and what happens when they are triggered. The actions are triggered by the user in the 3D scene, e.g. by clicking on a button, or are automatically triggered by the system (WebSdk) when the user interacts with the model. Thus, the geometry script defines what the final state of an animation is when it is triggered. The final state of the animation is defined by an animation matrix, which is combined with the object's matrix when the animation is triggered.

e.g.:

{
    "id": "catalog_id:test_component",
    "animations": [
        {
            "key": "openClose",
            "label": "'Open / Close'",
            "actions": [
                { "key": "close", "label": "'Close'", "type": "origin" },
                { "key": "open", "label": "'Open'", "type": "matrix" }
            ]
        }
    ],
    "geometry": "
        AddPlainCube({1000, 1000, 100});
        SetObjSurface('roomle_script_test:yellow');
        AddPlainCube(Vector3f{800, 800, 100});
        SetObjSurface('roomle_script_test:red');
        MoveMatrixBy({100, 100, 100});
        AnimationMatrixMoveBy('openClose:open', Vector3f{0, 500, 0});
    "
}

It is also not possible to animate parts of a Boolean operation, but only the entire object that results from a Boolean operation.

e.g.:

You can do something like this:

{
    "id": "catalog_id:test_component",
    "animations": [
        {
            "key": "openClose",
            "label": "'Open / Close'",
            "actions": [
                { "key": "close", "label": "Close", "type": "origin" },
                { "key": "open", "label": "Open", "type": "matrix" }
            ]
        }
    ],
    "geometry": "
        AddPlainCube(Vector3f{500, 500, 500});
        SetObjSurface('roomle_script_test:texture_4x4');
        AddSphere(Vector3f{1000, 1000, 1000});
        MoveMatrixBy(Vector3f{500, 500, 500});
        SetObjSurface('roomle_script_test:red');

        MinusOperator();
        AnimationMatrixMoveBy('openClose:open', Vector3f{0, 0, 500});
    "
}

But not something like this:

{
    "id": "catalog_id:test_component",
    "animations": [
        {
            "key": "openClose",
            "label": "'Open / Close'",
            "actions": [
                { "key": "close", "label": "Close", "type": "origin" },
                { "key": "open", "label": "Open", "type": "matrix" }
            ]
        }
    ],
    "geometry": "
        AddPlainCube(Vector3f{500, 500, 500});
        SetObjSurface('roomle_script_test:texture_4x4');
        AddSphere(Vector3f{1000, 1000, 1000});
        MoveMatrixBy(Vector3f{500, 500, 500});
        SetObjSurface('roomle_script_test:red');

        AnimationMatrixMoveBy('openClose:open', Vector3f{0, 0, 500});
        MinusOperator();
    "
}

At first glance this looks pretty similar, but there is one important difference in the last two lines of the geometry script. The first example applies the CSG operator (MINUS) first and then the AnimationMatrix second. This means that Object is constructed first and then the animation gets applied to the whole object. The second example adds the AnimationMatrix first and then the CSG operation, so the AnimationMatrix would be part of the CSG operation and this is not allowed and throws an error. The same is true for clip- and stretch-operations.

AnimationMatrixMoveBy, AnimationMatrixRotateBy, AnimationMatrixScaleBy

It is important that the functions generate exactly one animation matrix per "key" and geometry when they are chained together. If you apply animation matrices to a group with AnimationMatrixMoveBy, AnimationMatrixRotateBy, AnimationMatrixScaleBy, it is not the group that is animated, but the animation matrix of each individual object in the group is manipulated. The final animation matrix is the combination of the original matrix of the geometry and the additional animation matrix. With this approach it is currently not yet possible to create complex animations, but only very simple animations such as changing the position or rotating around an axis.

AnimatedMoveBy, AnimatedRotateBy, AnimatedScaleBy

Unlike AnimationMatrixMoveBy, AnimationMatrixRotateBy and AnimationMatrixScaleBy the operations AnimatedMoveBy, AnimatedRotateBy, AnimatedScaleBy consider the current transformation matrix of the object or group. This means that the animation is applied to the current transformation matrix of the object or group, which is then used to calculate the final position of the object in the scene. This allows more complex animations, but it also means that the order of operations matters. If you apply an AnimatedMoveBy, AnimatedRotateBy, AnimatedScaleBy operation after a MoveMatrisB, RotateMatrixBy, ScaleMatrixBy operation, the movement will be relative to the rotated position of the object.

The difference between the two groups of operations is best explained with examples:

In the following example the individual sprouts are animated with AnimationMatrixRotateBy, but the complete cage is animated with AnimatedRotateBy:

Example - animation demo cage

cloese
opend cage
opend sprouts

closed

opend cage

opend sprouts

{
    "id": "roomle_script_documentation:animation-demo-cage",
    "geometry": "
        _.p = [{-50, 50}, {-50, -50}, {50, -50}, {50, 50}];
        for (_.i = 1i; _.i <= 32i; _.i++) {
            _.a = M_PI * _.i / 32i;
            pushBack(_.p, {600 - 575 * cos(_.a), 475 * sin(_.a)});
        }
        for (_.i = 0i; _.i < 32i; _.i++) {
            _.a = M_PI * _.i / 32i;
            pushBack(_.p, {600 + 625 * cos(_.a), 525 * sin(_.a)});
        }
        BeginObjGroup();
        AddPrism(50, _.p);
        MoveMatrixBy({0, 143, -25});
        SetObjSurface('egger:h3734_st9');
        RotateMatrixBy({1, 0, 0}, {0, 0, 0}, 90);
        for (_.i = 1i; _.i <= 6i; _.i++) {
            Copy();
            RotateMatrixBy({1, 0, 0}, {0, 0, 0}, -30);
        }
        EndObjGroup();
        AnimationMatrixRotateBy('openClose:open-a', {0, 0, 1}, {0, 0, 0}, 90);
        AnimatedRotateBy('openClose:open-b', {0, 0, 1}, {0, 0, 0}, 90);
        AddSphere({1000, 1000, 1000});
        MoveMatrixBy({600, 0, 0});
        SetObjSurface('egger:f121_st87');
        ",
    "animations": [
        {
            "key": "openClose",
            "label": "'Open / Close'",
            "actions": [
                { "key": "close", "label": "'Close'", "type": "origin" },
                { "key": "open-a", "label": "'Open sprouts'", "type": "matrix" },
                { "key": "open-b", "label": "'Open cage'", "type": "matrix" }
            ]
        }
    ]
}

It is also possible to apply several consecutive animations to a geometry or mesh. In the following example, the blinds can be opened and closed, but the complete wings including the blinds can also be opened and closed. The blindes are animated with AnimationMatrixRotateBy, but the wings are animated with AnimatedRotateBy:

Example - animation demo window

cloese
opend wings
opend blinds

closed

opend wings

opend sprouts

{
    "id": "roomle_script_documentation:animation-demo-window",
    "parameters": [
        { "key": "width", "type": "Decimal", "defaultValue": 600, "visible": true, "enabled": false },
        { "key": "height", "type": "Decimal", "defaultValue": 800, "visible": true, "enabled": false }
    ],
    "onUpdate": "
        _.depth = 60;
        _.wallThickness = getObjectProperty('wallThickness', 120);
        setBoxForMeasurement({width, _.wallThickness, height},{0, (_.depth/2-_.wallThickness), 0});
    ",
    "geometry": "
        function frameGeometry(groupName, wParam, hParam, dParam, fPrame) {
            BeginObjGroup(groupName);
            AddPrism(dParam, [{0, 0}, {0, wParam}, {fPrame, wParam - fPrame}, {fPrame, fPrame}], bevelWidth = 0);
            RotateMatrixBy({0,0,1},{0,0,0},-90);
            AddPrism(dParam, [{0, 0}, {0, wParam}, {-fPrame, wParam - fPrame}, {-fPrame, fPrame}], bevelWidth = 0);
            RotateMatrixBy({0,0,1},{0,0,0},-90);
            MoveMatrixBy({0, -hParam, 0});
            AddPrism(dParam, [{0, 0}, {0, -hParam}, {fPrame, -hParam + fPrame}, {fPrame, -fPrame}], bevelWidth = 0);
            AddPrism(dParam, [{0, 0}, {0, -hParam}, {-fPrame, -hParam + fPrame}, {-fPrame, -fPrame}], bevelWidth = 0);
            MoveMatrixBy({wParam, 0, 0});
            EndObjGroup();
            RotateMatrixBy({1,0,0},{0,0,0},-90);
            RotateUvMatrixBy(90);
        }
        function wingGeometry(groupNameW, wParamW, hParamW, dParamW, fPrameW) {
            BeginObjGroup('wing');
            frameGeometry(groupNameW, wParamW, hParamW, dParamW, fPrameW);
            SetObjSurface('kronospan:8156_vh');
            _.h = hParamW - 2 * fPrameW - 20;
            _.n = round(_.h / 50, 0);
            _.sh = _.h / _.n;
            AddCube({6, _.sh + 20, wParamW - 2 * fPrameW});
            RotateUvMatrixBy(90);
            SetObjSurface('kronospan:8156_vh');
            AnimationMatrixRotateBy('openCloseBlinds:open', {0, 0, 1}, {0, 0, 0}, 70);
            MoveMatrixBy({-3, -(_.sh + 20) / 2, 0});
            RotateMatrixBy({0, 0, 1}, {0, 0, 0}, -70);
            RotateMatrixBy({0, 1, 0}, {0, 0, 0}, 90);
            MoveMatrixBy({fPrameW, dParamW / 2, fPrameW + _.sh / 2 + 10});
            for (_.i = 1; _.i < _.n; _.i++) {
                Copy(true);
                MoveMatrixBy({0, 0, _.sh});
            }
            EndObjGroup();
        }
        _.depth = 60;
        _.frameSize = 80;
        _.hingeAxisLeft = {_.frameSize-26, _.depth/2+6, 0};
        _.hingeAxisRight = {width - (_.frameSize-26), _.depth/2+6, 0};
        
        frameGeometry('frame', width, height, _.depth, _.frameSize);
        SetObjSurface('kronospan:k608_bs');
        MoveMatrixBy({0, -_.depth/2, 0});

        BeginObjGroup('hinges');
        AddCylinder(6, 6, 50, 16, edgeStyle='chamfer', bevelWidth=2);
        MoveMatrixBy({_.frameSize-26, _.depth/2+6, (height - 100) / 4});
        Copy();
        MoveMatrixBy({0, 0, (height - 100) / 2});
        Copy();
        MoveMatrixBy({width - (_.frameSize-26) * 2, 0, 0});
        Copy();
        MoveMatrixBy({0, 0, -(height - 100) / 2});
        EndObjGroup();
        SetObjSurface('kronospan:0190_af');

        BeginObjGroup('wings');
        wingGeometry('leftWing', width/2-60, height-120, 20, 30);
        MoveMatrixBy({60 , _.depth/2, _.depth});
        AnimatedRotateBy('openCloseWings:open', {0, 0, 1}, _.hingeAxisLeft, 120);
        wingGeometry('rightWing', width/2-60, height-120, 20, 30);
        MoveMatrixBy({width/2 , _.depth/2, 60});
        AnimatedRotateBy('openCloseWings:open', {0, 0, 1}, _.hingeAxisRight, -120);
        EndObjGroup();
    ",
     "animations": [
        {
            "key": "openCloseWings",
            "label": "'Open / Close Wings'",
            "level": 2,
            "actions": [
                { "key": "close", "label": "'Close'", "type": "origin" },
                { "key": "open", "label": "'Open'", "type": "matrix" }
            ]
        },
        {
            "key": "openCloseBlinds",
            "label": "'Open / Close Blinds'",
            "level": 1,
            "actions": [
                { "key": "close", "label": "'Close'", "type": "origin" },
                { "key": "open", "label": "'Open'", "type": "matrix" }
            ]
        }
    ]
}

Light sources

Addtionally to the geometries, it is also possible to add light sources to the component. The light sources can be added with the AddLightSource function. The light sources can be of type 'spot', 'point' or 'area'. The parameters for the light source are defined in the function call.

AddLightSource(
    type: 'spot' | 'point' | 'area',
    position: Vector3f, 
    direction?: Vector3f, 
    angle?: number, 
    color?: string, 
    intensity?: number,
    distance?: number,
    penumbra?: number, 
    decay?: number, 
    width?: number, 
    height?: number);
attribute
light type
description

type

'spot', 'point', 'area'

Defines the type of light source.

position

'spot', 'point', 'area'

The position of the light source in the 3D space.

direction

'spot', 'area'

The direction in which the light is emitted.

angle

'spot'

The angle of the spotlight in degrees.

color

'spot', 'point', 'area'

The color of the light source, specified as a string (e.g., '#ffffff' for white).

intensity

'spot', 'point', 'area'

The intensity of the light source, a number representing the brightness.

distance

'spot', 'point'

The distance at which the light source is effective.

penumbra

'spot'

The softness of the edges of the spotlight, a number between 0 and 1.

decay

'spot', 'point'

The decay rate of the light, a number representing how quickly the light intensity decreases with distance.

width

'area'

The width of the area light source.

height

'area'

The height of the area light source.

Point light examples:

AddLightSource('point', {0, 0, 100}, intensity = 10, color = '#ffff80');

Spot light example:

AddLightSource('spot', {0, 0, 1000}, {0, 0, -1}, angle = 45, intensity = 5, penumbra = 0.5, color = '#ff8080');

Area light examples:

AddCube({width, depth, 100}, material = 'pfleiderer:r20391_nw');
MoveMatrixBy({-width/2, 0, 0});
AddLightSource('area', {0, depth/2, -0.1}, {0, 0, -1}, intensity = 5, width = width, height = depth);

Example - lights in the plan

lights in the plan

Constructive Solid Geometry

Also called CSG Operators, they provide a possibility to perform different cut, union etc. operations on the meshes in RoomleScript. The CSG (Constructive Solid Geometry) operators can unite, intersect or subtract individual geometries or even groups of geometries. e.g.:

 /* MeshA */
AddPlainCube(Vector3f{500, 500, 500});

/* MeshB */
AddSphere(Vector3f{1000, 1000, 1000});
MoveMatrixBy(Vector3f{500, 500, 500});

/* Operator mesh A minus mesh B */
MinusOperator(); /* ... AndOperator, OrOperator */

OrOperator

AndOperator

MinusOperator

OR operator

AND operator

MINUS operator

combines two geometries, respectively determines the united volume

intersects two geometries, respectively determines the volume that two geometries have in common

subtracts the second geometry from the first one

The operators also work with geometries made of different materials. The final shape consists of the materials of the two objects from which it was created. So, for example, if a cube with material A is subtracted from a cube with material B, the resulting shape will have materials A and B. This also applies to multiple material geometries, groups, nested groups and combinations of all these where the objects in groups are ORed as long as no other operation is explicitly specified.

AddPlainCube(Vector3f{1000, 1000, 1000});
SetObjSurface('roomle\_script\_test:texture\_4x4'); 
AddPlainCube(Vector3f{1000, 1000, 1000});
MoveMatrixBy(Vector3f{250, 250, 250});
SetObjSurface('roomle\_script\_test:red');
MinusOperator();       
MINUS operator with differnt materials
m1 = ['roomle_script_test:blue', 'roomle_script_test:red'];
AddCube(Vector3f{1000, 1000, 1000}, bevelWidth = 50, materials = m1);
AddCube(Vector3f{1000, 1000, 1000}, bevelWidth = 50, materials = m1);
m2 = ['roomle_script_test:magenta', 'roomle_script_test:yellow'];
AddCube(Vector3f{1000, 1000, 1000}, bevelWidth = 50, materials = m2);
MoveMatrixBy(Vector3f{250, 250, 250});
MinusOperator(); 
MINUS operator with mulit material geometry
BeginObjGroup();
AddPlainCube(Vector3f{1000, 1000, 800});
SetObjSurface('roomle\_script\_test:blue');  
AddPlainCube(Vector3f{800, 800, 1000});
SetObjSurface('roomle\_script\_test:cyan');
MoveMatrixBy(Vector3f{200, 200, 800});
EndObjGroup();
BeginObjGroup();
AddCylinder(500, 500, 1400, 32);
SetObjSurface('roomle\_script\_test:yellow');
MoveMatrixBy(Vector3f{1000, 1000, 200});
AddSphere(Vector3f{1000, 1000, 1000});
SetObjSurface('roomle\_script\_test:red');
MoveMatrixBy(Vector3f{700, 700, 500});
EndObjGroup();
MinusOperator();
MINUS operator with groups

If a material is set after the CSG operation with SetObjSurface, the material of the whole resulting geometry will be overwritten and the resulting geometry will have only this material. If the material specification for a part of the geometry is missing, the first material involved in the operation is automatically used.

The operators work on the basis of the winding order of the triangles of the meshes. It may therefore be necessary to reverse the order of the triangles of the mesh. This can be done with the ReverseFaces function.

ClipOperator

The clip operator has 2 mandatory parameters and 4 optional parameters

ClipOperator(position: Vector3f, direction: Vector3f, *material: String, *uvScale: Vector2f, *uvRotation: float, *uvOffset:Vector2f);

With the clip operator geometry can be clipped with a plane. The plane is defined by a position and a direction. The operator clips exactly one geometry or group. It clips the geometry or group which was drawn prior to the clip operation. The clip operator can also create a cap geometry. The cap geometry is only generated if a material is specified.

AddPlainCube(Vector3f{1000, 1000, 1000});
SetObjSurface('roomle_script_test:texture_4x4');
ClipOperator(Vector3f{800, 0, 800}, Vector3f{-1, 0, -1}, 'roomle_script_test:red');

The clip operator works with the logic that you get what you see. Let's assume you are looking from a position in the scene in a certain direction, then everything you see is kept and everything you don't see is clipped.

clip operator

The ClipOperator is optimized for clipping a geometry with one plane and is therefore much more performant than the MinusOperator, which is a general boolean operation. For example, if you want to clip a geometry by a cube (A AND B, where B is the cube), then the cube consists of 6 faces, where each face has 2 triangles, resulting in a total of 12 triangles that must be considered for the clipping. The same can be achieved with 6 clipping planes, with each plane requiring only one operation, with the advantage that each clipped side cap can be given its own material. Of course, a clipping plane cannot be used to cut holes, for this purpose the MinusOperator is the best choice.

StretchOperator

The stretch operator has 3 mandatory parameters and 4 optional parameters.

StretchOperator(position: Vector3f, direction: Vector3f, distance: float, *material: String, *uvScale: Vector2f, *uvRotation: float, *uvOffset:Vector2f);

The stretch operator is an extension of the clip operator. Unlike the clip operator, however, it does not cut off a piece of the geometry, but divides the geometry in a plane, moves the parts apart and fills the gap with an extrude. The plane is defined by a position and a direction. The material and the UV transformation of the extruder can be specified separately as an option.

AddPlainCube(Vector3f{1000, 1000, 1000});
SetObjSurface('roomle_script_test:texture_4x4');
StretchOperator(Vector3f{800, 0, 800}, Vector3f{1, 0, 1}, 200, 'roomle_script_test:red');
clip operator

Docking request functions

A docking request can only be used in the "onUpdate" script of a component.

requestDockItem(item: string, parentPostion: Vector3f, childPostion?: Vector3f)

The docking request is only valid if a docking point or a docking line is defined at the parent position in the component. The subordinate position is optional and is only used to select a subordinate position if multiple child points are defined.

Construction elements special features

For construction elements in the Planner (Roomle Room Planner), like doors and windows, there are some special features available. These features only affect component definitions that are used as construction elements. On "generic" components these features have no effect.

Door arches

see Interaction between Configurator and Planner - Door arches

Last updated