Advanced Docking Logic

This chapter requires the reader to have already an understanding of the Roomle platform and to have some experience with scripting, including topics described in the Basic Docking Logic chapter. Topics described in this chapter are ones of the most complex you can achieve in the Roomle platform.

Utilizing connection.isPreview Check

In the docking points context, a boolean getter connection.isPreview can be utilized. Usually the condition updates itself in every update loop. If the condition uses extensive computations, like searching through many arrays slowing down the configuration, you can utilize a pattern like the one following:

{
    "mask": "shelves",
    "position": "{ 100, 200, 300}",
    "condition": "
        if (connection.isPreview) {
            /* 
            adding a new add-on, preview phase
            compute whethet it fits
            */
        } else {
            /* already docked, keep it */
            condition = true;
        }
    "
}

You check in the preview phase whether the addon fits. You prevent the situation where you have an illogical configuration, because it won't delete the child as if you were using the condition in the common way. However, you can still add some computations whether it is still valid. Example: you have a docking range of shelves inside a wardrobe. They must be placed at least 5 docking positions from each other, which is something you would do in the conneciton.isPreview == true branch, because every docking point in the range cycles through an array in 10 indices and it would be expensive to compute the conditions in every update loop. But you can get to a situation where you change the wardrobe height from 2500 to 1600 mm, therefore you need to delete the shelves that go through the wardrobe ceiling. In this case, you can of course use the connection.isPreview == false branch as in any other script.

if (connection.isPreview) {
    /* 
    for (from position - 5 to position + 5) - check if there is a shelf docked
    Note: we will show this further in this chapter
    */
} else {
    _.positionZ = zFromVector(connection.position);
    condition = dockRangeHeight > _.positionZ;
}

Storing Data in the connection Context in assignmentScripts

In cases where you need to compute a value once in the first time and you are certain that you do not need to recompute it later, you can compute it only in onDock and retrieve its value in both onUpdate and onUnDock scripts. See following example:

"assignmentScripts": {
    "onDock": "
        _.i = round(xFromVector(connection.position) / stepX, 0);
        _.j = round(yFromVector(connection.position) / stepY, 0);
        connection._index = _.i * self.maxX + _.j;
        set(self.dockedWidths, connection._index, other.width);
    ",
    "onUpdate": "
        if (connection._index >= 0) {
            set(self.dockedWidths, connection._index, other.width);
        }
    ",
    "onUnDock": "
        if (connection._index >= 0) {
            set(self.dockedWidths, connection._index, 0);
        }
    "
}

Sibling Points

Up until now, you knew how to detect a neighbour if it has been docked via the docking points. This is a perfectly possible and recommended way to detect neighbours, as long as your product docks in a single line. If you can fork the abstract connecting line, you can end up in loops or parallel configurations, where you need to detect what is next to the current component and eventually transfer data. This is a common topic in shelf systems or also in docking ranges. To directly communicate with a neighbouring element, you can use the Sibling Points scripting feature, where you define a connection point in one or more components at a given position with a mask. If there are two siblings points in one place with matching masks, they connect together and standard assignments, as you already know them, ensure the ability to share data between the components in the configuration regardless of their position in the parent-child hierarchy.

{
    "id":"example:siblings",
    ...
    "siblings": [
        {
            "mask": "horizontalSibling",
            "position": "{ -width / 2, 0, 100}",
            "rotation": "{0, 0, 0}",
            "selfAssignments": {}
        },
        {
            "mask": "horizontalSibling",
            "position": "{width / 2, 0, 100}",
            "rotation": "{0, 0, 0}",
            "selfAssignments": {}
        }
    ]
}

Note: The siblings arrtibute of type List<ConnectionWithAssignment>. Docking points inherits ConnectionWithAssignments and adds a condition and rotation. Therefore, siblings points have neither condition nor rotation.

Note: You do not have a (direct) possibility to know what component is on the other side of the sibling point, neither can you get data from the other sibling point. Therefore, you need to rather pull the data from the other side than to push it and use the pulled data to compute what you need afterwards. Therefore we recommend using selfAssignments or assignmentScripts where you assign to self. values based on the other. values.

Example: Grid Shelf System

In this example we have to create a shelf system consisting of spaces dockable to left, right and top, with a similar logic to our demo USM configurator, where widths and heights synchronize across the columns and lines (like in an Excel table, where you can not have two cells with different heights in a single row). We will work step-by-step in implementing a similar logic.

We start from the logic implementation, using abstract geometry in order not to overwhelm the script from the beginning. We start with visualizing the blank spaces and preparing the docking points.

Unfold to see the component definition
{
    "id": "usm:frame",
    "parameters": [
        {
            "key": "width",
            "labels": {
                "de": "Breite",
                "en": "Width"
            },
            "type": "Decimal",
            "unitType": "length",
            "defaultValue": 750,
            "validValues": [
                350,
                395,
                500,
                750
            ],
            "visible": "true"
        },
        {
            "key": "depth",
            "sort": 10,
            "global": true,
            "labels": {
                "de": "Tiefe",
                "en": "Depth"
            },
            "type": "Decimal",
            "unitType": "length",
            "defaultValue": 350,
            "validValues": [
                350,
                500
            ],
            "visible": false
        },
        {
            "sort": 10,
            "key": "height",
            "labels": {
                "de": "Höhe",
                "en": "Height"
            },
            "type": "Decimal",
            "unitType": "length",
            "defaultValue": 350,
            "validValues": [
                100,
                175,
                250,
                350,
                395
            ],
            "visible": "true"
        }
    ],
    "onUpdate": "
        if (ifnull(inited, false) == false) {
            inited = true;
            isRoot = true;
        }
    ",
    "geometry": "
        if (isRoot) {
            coordSystemAxesLength = 200;
            coordSystemAxesThickness = 10;
            BeginObjGroup();
                AddPlainCube(Vector3f{coordSystemAxesThickness, coordSystemAxesThickness, coordSystemAxesLength}); SetObjSurface('demoCatalogId:test_crazy_gree');
                AddPlainCube(Vector3f{coordSystemAxesThickness, coordSystemAxesLength, coordSystemAxesThickness}); SetObjSurface('demoCatalogId:cyan');
                AddPlainCube(Vector3f{coordSystemAxesLength, coordSystemAxesThickness, coordSystemAxesThickness}); SetObjSurface('demoCatalogId:red');
            EndObjGroup();
        }
        AddCube(Vector3f{width, depth, height});
         MoveMatrixBy(Vector3f{ -width / 2, 0, 0});
         SetObjSurface('isdt:black_transparent');
    ",
    "parentDockings": {
        "points": [
            {
                "mask": "gridLeft",
                "position": "{ -width / 2, 0, 0}",
                "rotation": "{0, 0, 0}",
                "condition": "true"
            },
            {
                "mask": "gridRight",
                "position": "{width / 2, 0, 0}",
                "rotation": "{0, 0, 0}",
                "condition": "true"
            },
            {
                "mask": "gridTop",
                "position": "{0, 0, height}",
                "rotation": "{0, 0, 0}",
                "condition": "true"
            }
        ]
    },
    "childDockings": {
        "points": [
            {
                "mask": "gridLeft",
                "position": "{width / 2, 0, 0}",
                "rotation": "{0, 0, 0}",
                "condition": "true",
                "selfAssignments": {
                    "onDock": {
                        "isRoot": false
                    },
                    "onUnDock": {
                        "isRoot": true
                    }
                }
            },
            {
                "mask": "gridRight",
                "position": "{ -width / 2, 0, 0}",
                "rotation": "{0, 0, 0}",
                "condition": "true",
                "selfAssignments": {
                    "onDock": {
                        "isRoot": false
                    },
                    "onUnDock": {
                        "isRoot": true
                    }
                }
            },
            {
                "mask": "gridTop",
                "position": "{0, 0, 0}",
                "rotation": "{0, 0, 0}",
                "condition": "true",
                "selfAssignments": {
                    "onDock": {
                        "isRoot": false
                    },
                    "onUnDock": {
                        "isRoot": true
                    }
                }
            }
        ]
    },
    "possibleChildren": [
        {
            "componentId": "usm:frame"
        }
    ]
}

We already show a coordinate system axes in the root component - see figure above. You can understand from the code, that only the top-level parent has the isRoot variable set to true. Any other component has it set to false. In order to be less error-prone, we add visualization of the sibling points. We will use spheres with a diameter of 50 units, and we will also detect using sibling points, if there is a neighbour in the respective direction. Therefore, we add some values to the onUpdate - inited block:

hasLeftNeighbour = false;
hasRightNeighbour = false;
hasTopNeighbour = false;
hasBottomNeighbour = false;

Which we visualize in the geometry and colour them in red if they are not connected, green when they are connected.

AddSphere(Vector3f{50, 50, 50});
 MoveMatrixBy(Vector3f{ -width / 2 + 25, 0, 50});
if (hasLeftNeighbour) {
    SetObjSurface('isdt:green');
} else {
    SetObjSurface('isdt:red');
}

AddSphere(Vector3f{50, 50, 50});
 MoveMatrixBy(Vector3f{width / 2 - 25, 0, 50});
if (hasRightNeighbour) {
    SetObjSurface('isdt:green');
} else {
    SetObjSurface('isdt:red');
}

AddSphere(Vector3f{50, 50, 50});
 MoveMatrixBy(Vector3f{0, 0, height - 25});
if (hasTopNeighbour) {
    SetObjSurface('isdt:green');
} else {
    SetObjSurface('isdt:red');
}

AddSphere(Vector3f{50, 50, 50});
 MoveMatrixBy(Vector3f{0, 0, 25});
if (hasBottomNeighbour) {
    SetObjSurface('isdt:green');
} else {
    SetObjSurface('isdt:red');
}

Now the most important part: The sibling points themselves:

{
    "mask": "horizontalSibling",
    "position": "{ -width / 2, 0, 50}",
    "selfAssignments": {
        "onDock": {
            "hasLeftNeighbour": true
        },
        "onUnDock": {
            "hasLeftNeighbour": false
        }
    }
},
{
    "mask": "horizontalSibling",
    "position": "{width / 2, 0, 50}",
    "selfAssignments": {
        "onDock": {
            "hasRightNeighbour": true
        },
        "onUnDock": {
            "hasRightNeighbour": false
        }
    }
},
{
    "mask": "verticalSibling",
    "position": "{0, 0, height}",
    "selfAssignments": {
        "onDock": {
            "hasTopNeighbour": true
        },
        "onUnDock": {
            "hasTopNeighbour": false
        }
    }
},
{
    "mask": "verticalSibling",
    "position": "{0, 0, 0}",
    "selfAssignments": {
        "onDock": {
            "hasBottomNeighbour": true
        },
        "onUnDock": {
            "hasBottomNeighbour": false
        }
    }
}

If you have two of those components docked, they will always match with the sibling points. Notice that the horizontal sibling points are not in the corners, but at the height of 50. This way, you can be sure that you won't connect with a component diagonally (there would have to be sibling points in the upper corner as well, but it is more understandable). In the current state, there are sibling points where their connections are visualized using the debug spheres:

Because we now have all the neccessary data in the component regarding what can fit where, we can now add parent-side conditions. The simplest docking pattern in such shelf systems is a "pitchfork-like" hierarchy - only the components that are on the bottom can dock to the left and right, while all can dock in the vertical up direction. Therefore, the conditions will be:

(!hasBottomNeighbour) && ((!connection.isPreview) || (!hasLeftNeighbour)) for the left docking point (!hasBottomNeighbour) && ((!connection.isPreview) || (!hasRightNeighbour)) for the right docking point

!hasBottomNeighbour Implicates this is the bottom element -> therefore it even should have the left and right docking points. (!connection.isPreview) || (!hasLeftNeighbour) Allows docking as long as something is docked on the left side. However, after docking, this will immediately delete the docked child. Therefore this check needs to be there only in preview.

A more simple-to-understand version of above:

if (connection.isPreview) {
    if (hasBottomNeighbour) {
        condition = false;
    } else {
        condition = hasLeftNeighbour /* or hasRightNeighbour */
    }
} else {
    condition = true;
}

Option 2: Implement this without connection.isPreview using hasLeftChild and hasRightChild variables, as described in the Basic Docking Logic.

The sibling points will be used, among other, to lock the widths and heights in the rows and columns. Therefore, assignmentsOnUpdateSilent will be used. "height": "height" in the horizontal siblings, "width": "width" in the vertical siblings. Why have we just picked assignmentsOnUpdateSilent instead of assignmentsOnUpdate? As written Basic Docking Logic, we need to keep the assigned parameters enabled.

Now the time comes to the geometry. The shelf system consists of pipes and connecting heads that connected together form frames. Into these frames, walls, doors, floors, trays and other parts can be mounted. The heads have 5 holes, allowing to screw the pipes together. Because the heads can be shared among up to 4 components, we have to define a rule which of the components will draw the head. Also, the frames on the bottom have legs. Check out subComponent definitions from the USM shelf system.

Unfold subComponent definitions

{ "internalId": "HEAD", "componentId": "usm:head", "numberInPartList": "1" }, { "internalId": "PIPE_HORIZONTAL", "componentId": "usm:pipe", "assignments": { "length": "width" }, "numberInPartList": "1" }, { "internalId": "PIPE_VERTICAL", "componentId": "usm:pipe", "assignments": { "length": "height" }, "numberInPartList": "1" }, { "internalId": "PIPE_FORWARD", "componentId": "usm:pipe", "assignments": { "length": "depth" }, "numberInPartList": "1" }, { "internalId": "FOOT", "componentId": "usm:levelingfoot", "numberInPartList": "1" }

The task now is to define which of the neighbours draws which heads, which pipes etc. Before you read further, please try to yourself define a ruleset, which will ensure that all parts will be there once and only once. Hint: we've already got more than enough data in the hasLeftNeighour, hasRightNeighbour, hasBottomNeighbour and hasTopNeighbour parameters.

In our example we follow with building the ruleset in a way that we first build the component that is most to the left on the bottom. If add another component to the right, it will be missing its left parts. If we build upwards, the bottom will be missing. If we are filling a top-right corner, where there is the left and the right neighbour, the top and right parts will be present. Therefore we can say that we always have the two top pipes, two right pipes and the two top-right heads. Left top heads, and left pipes are there when !hasLeftNeighbour. Bottom heads, legs and pipes are there if !hasBottomNeighbour. The bottom left parts are there whenever there is no bottom or left neighbour -> (!hasLeftNeighbour) && (!hasRightNeighbour)

We prepare logical variables for this in onUpdate in order to make the code more legible. These computations are really simple and might seem trivial. However, an uninitiated person will read that the geometry and part list counts depend on whether the component HAS the parts and the component has the parts as long as there are no neighbours, making the code provide the answer to the question: "Why does it have the parts?"

hasBottomParts = !hasBottomNeighbour;
hasLeftParts = !hasLeftNeighbour;
hasBottomLeftParts = hasBottomParts && hasLeftParts;

Therefore we can build the geometry:

if (hasLeftParts) {
    BeginObjGroup();
        SubComponent('PIPE_VERTICAL');
        SubComponent('HEAD');
         MoveMatrixBy(Vector3f{0, 0, height});
    EndObjGroup();
     MoveMatrixBy(Vector3f{ -width / 2, 0, 0});
    Copy();
     MoveMatrixBy(Vector3f{0, depth, 0});
    SubComponent('PIPE_FORWARD');
     RotateMatrixBy(Vector3f{1, 0, 0}, Vector3f{0, 0, 0}, -90);
     MoveMatrixBy(Vector3f{ -width / 2, 0, height});
}
if (hasBottomParts) {
    BeginObjGroup();
        SubComponent('HEAD');
        SubComponent('PIPE_HORIZONTAL');
         RotateMatrixBy(Vector3f{0, 1, 0}, Vector3f{0, 0, 0}, -90);
        SubComponent('FOOT');
    EndObjGroup();
     MoveMatrixBy(Vector3f{width / 2, 0, 0});
    Copy();
     MoveMatrixBy(Vector3f{0, depth, 0});
    SubComponent('PIPE_FORWARD');
     RotateMatrixBy(Vector3f{1, 0, 0}, Vector3f{0, 0, 0}, -90);
     MoveMatrixBy(Vector3f{width / 2, 0, 0});
}
if (hasBottomLeftParts) {
    SubComponent('HEAD');
     MoveMatrixBy(Vector3f{ -width / 2, 0, 0});
    Copy();
     MoveMatrixBy(Vector3f{0, depth, 0});
     SubComponent('FOOT');
     MoveMatrixBy(Vector3f{-width / 2, 0, 0});
     Copy();
     MoveMatrixBy(Vector3f{0, depth, 0});
    SubComponent('PIPE_FORWARD');
     RotateMatrixBy(Vector3f{1, 0, 0}, Vector3f{0, 0, 0}, -90);
     MoveMatrixBy(Vector3f{ -width / 2, 0, 0});
}
BeginObjGroup();
    SubComponent('HEAD');
     MoveMatrixBy(Vector3f{width / 2, 0, height});
    SubComponent('PIPE_HORIZONTAL');
     RotateMatrixBy(Vector3f{0, 1, 0}, Vector3f{0, 0, 0}, 90);
     MoveMatrixBy(Vector3f{ -width / 2, 0, height});
    SubComponent('PIPE_VERTICAL');
     MoveMatrixBy(Vector3f{width / 2, 0, 0});
EndObjGroup();
Copy();
 MoveMatrixBy(Vector3f{0, depth, 0});
SubComponent('PIPE_FORWARD');