Second year at Archilogic

One could consider that an app fundamentally has 3 layers: a data layer, an application logic layer, and a UI layer. In the case of the Editor app I was working on at Archilogic, it would look something like:

Very high level overview of the Editor app
Very high level overview of the Editor app

While my first year was mostly dedicated to refreshing the whole UI, the second year was all about swapping out the data layer, and the tightly coupled application layer.

Preparing the way

To ease the transition from the old to the new data format, the core of the app needed to be refactored in a way that abstracted away the actual implementation details of the interactions with the data: how a floor model is loaded, how an element on the floor plan is selected/modified/copied/deleted, how the floor is then persisted via API, etc. The concept of an action was introduced. An action is defined by

  1. a condition under which it can run. Some actions are only possible in 2D, some are not possible when the app is offline, some are only allowed for some user role, etc.
  2. what it will run. Actions update the canvas, change the app state, and modify elements.
  3. an optional keyboard shortcut

Actions are triggered by users via form inputs, keyboard shortcuts, left-click context-menu, or UI buttons. For now, only editing in 2D is possible, but everything has been put in place to introduce 3D editing in the future, thanks to abstracting interactions with the canvas.

The new data format

For many years, a floor would be stored in a bit of a messy format called Scene Structure. The new data format is much more structured. Spaces are defined by edges, which are defined by vertices. You have guessed it, those vertices and edges make a graph. Hence, the name of the new data format: Space Graph.

A simple space—in other words, a room—delimited by 4 walls, with a door, a desk and a chair

A simple space
A simple space

is represented in Scene Structure as seen on the left, and in Space Graph on the right:

{
  "v": "2.1",
  "x": 0,
  "y": 0,
  "z": 0,
  "id": "cf336998-e7e4-4f11-ae42-20125ca94c26",
  "ry": 0,
  "type": "plan",
  "class": [],
  "floorArea": 32.5,
  "modelDisplayName": "New floor",
  "children": [
    {
      "x": 0,
      "y": 0,
      "z": 0,
      "id": "07491751-c03b-496e-adad-24b99ffa2185",
      "ry": 0,
      "type": "level",
      "children": [
        {
          "h": 0.2,
          "x": 0,
          "y": 0,
          "z": 0,
          "id": "43b8dceb-56c8-4170-ba4f-650f8f8de0a8",
          "ry": 0,
          "type": "polyfloor",
          "usage": "Private office",
          "polygon": [
            [
              5.85,
              0.15
            ],
            [
              0.15,
              0.15
            ],
            [
              0.15,
              5.85
            ],
            [
              5.85,
              5.85
            ]
          ],
          "hCeiling": 3,
          "materials": {
            "top": "basic-floor"
          },
          "hasCeiling": false,
          "polygonHoles": [],
          "children": []
        },
        {
          "h": 3,
          "l": 6,
          "w": 0.15,
          "x": 0.075,
          "y": 0,
          "z": 0,
          "id": "a2b416cc-b45c-4e4b-ab02-b6ea8b7a86a6",
          "ry": -90,
          "type": "wall",
          "materials": {
            "top": {
              "colorDiffuse": [
                0.12549019607843137,
                0.12549019607843137,
                0.12549019607843137
              ]
            },
            "back": "basic-wall",
            "front": "basic-wall"
          },
          "baseHeight": 0,
          "backHasBase": false,
          "controlLine": "center",
          "frontHasBase": false,
          "children": []
        },
        {
          "h": 3,
          "l": 5.7,
          "w": 0.15,
          "x": 0.15,
          "y": 0,
          "z": 5.925,
          "id": "b2f0c124-2c74-49a1-975b-774e9cf35b89",
          "ry": 0,
          "type": "wall",
          "materials": {
            "top": {
              "colorDiffuse": [
                0.12549019607843137,
                0.12549019607843137,
                0.12549019607843137
              ]
            },
            "back": "basic-wall",
            "front": "basic-wall"
          },
          "baseHeight": 0,
          "backHasBase": false,
          "controlLine": "center",
          "frontHasBase": false,
          "children": []
        },
        {
          "h": 3,
          "l": 5.85,
          "w": 0.15,
          "x": 5.925,
          "y": 0,
          "z": 6,
          "id": "2f5fef72-997c-4e23-a04c-96e0c94c5f80",
          "ry": -270,
          "type": "wall",
          "materials": {
            "top": {
              "colorDiffuse": [
                0.12549019607843137,
                0.12549019607843137,
                0.12549019607843137
              ]
            },
            "back": "basic-wall",
            "front": "basic-wall"
          },
          "baseHeight": 0,
          "backHasBase": false,
          "controlLine": "center",
          "frontHasBase": false,
          "children": []
        },
        {
          "h": 3,
          "l": 5.85,
          "w": 0.15,
          "x": 6,
          "y": 0,
          "z": 0.075,
          "id": "540593c3-db1c-4c92-8249-bf9275d1d3fe",
          "ry": -180,
          "type": "wall",
          "materials": {
            "top": {
              "colorDiffuse": [
                0.12549019607843137,
                0.12549019607843137,
                0.12549019607843137
              ]
            },
            "back": "basic-wall",
            "front": "basic-wall"
          },
          "baseHeight": 0,
          "backHasBase": false,
          "controlLine": "center",
          "frontHasBase": false,
          "children": [
            {
              "h": 2,
              "l": 1,
              "w": 0.05,
              "x": 2.5,
              "y": 0,
              "z": 0,
              "id": "19747ebe-52d3-43be-be5f-6ec5456118ec",
              "ry": 0,
              "side": "front",
              "type": "door",
              "class": [],
              "hinge": "right",
              "doorType": "singleSwing",
              "doorAngle": 90,
              "leafWidth": 0.03,
              "threshold": true,
              "handleType": "squareEdged",
              "leafOffset": 0.005,
              "frameLength": 0.05,
              "frameOffset": 0,
              "fixLeafRatio": 0.3,
              "thresholdHeight": 0.01,
              "children": []
            }
          ]
        },
        {
          "x": 3,
          "y": 0,
          "z": 4,
          "id": "8311dab0-6090-46a2-b0c5-2d93db39edfe",
          "ry": 0,
          "src": "!d18c0430-b896-44c2-8b5e-d38defe18a08",
          "type": "interior",
          "children": []
        },
        {
          "x": 3,
          "y": 0,
          "z": 4.5,
          "id": "aad211c0-00ae-49c0-b7a1-959944f4c26c",
          "ry": 180,
          "src": "!7ed123c3-b6c8-4b76-9b4e-69a1562c179b",
          "type": "interior",
          "children": []
        }
      ]
    }
  ]
}
{
  "schemaVersion": "0.16.7",
  "spatialStructure": {
    "id": "07491751-c03b-496e-adad-24b99ffa2185",
    "type": "spatialStructure:layout",
    "spatialGraph": {
      "vertices": [
        {
          "id": "cc5802d7-5fee-492c-a6f2-4b57a9bd9ad0",
          "type": "spatialGraph:vertex",
          "position": [
            0,
            0
          ]
        },
        {
          "id": "06be49ac-2ab4-44ab-9442-626b497096a6",
          "type": "spatialGraph:vertex",
          "position": [
            0,
            6
          ]
        },
        {
          "id": "fde5c965-0e30-4887-833b-9157de2f3ec8",
          "type": "spatialGraph:vertex",
          "position": [
            6,
            6
          ]
        },
        {
          "id": "449f2bde-0d68-4ea8-886a-2d3b151eaabe",
          "type": "spatialGraph:vertex",
          "position": [
            6,
            0
          ]
        }
      ],
      "edges": [
        {
          "id": "10226dd1-d701-4285-b364-51fedd8bebf1",
          "type": "spatialGraph:edge",
          "vertices": [
            "cc5802d7-5fee-492c-a6f2-4b57a9bd9ad0",
            "06be49ac-2ab4-44ab-9442-626b497096a6"
          ]
        },
        {
          "id": "4df310c9-01d8-4555-a9f4-e9017c02ed18",
          "type": "spatialGraph:edge",
          "vertices": [
            "06be49ac-2ab4-44ab-9442-626b497096a6",
            "fde5c965-0e30-4887-833b-9157de2f3ec8"
          ]
        },
        {
          "id": "7060299c-d755-434d-aaff-30c023bef636",
          "type": "spatialGraph:edge",
          "vertices": [
            "fde5c965-0e30-4887-833b-9157de2f3ec8",
            "449f2bde-0d68-4ea8-886a-2d3b151eaabe"
          ]
        },
        {
          "id": "e58a459d-8938-4f0d-bcd9-6ce3bf080919",
          "type": "spatialGraph:edge",
          "vertices": [
            "449f2bde-0d68-4ea8-886a-2d3b151eaabe",
            "cc5802d7-5fee-492c-a6f2-4b57a9bd9ad0"
          ]
        }
      ]
    },
    "spaces": [
      {
        "id": "43b8dceb-56c8-4170-ba4f-650f8f8de0a8",
        "type": "layout:space",
        "boundaries": [
          {
            "edges": [
              "7060299c-d755-434d-aaff-30c023bef636",
              "4df310c9-01d8-4555-a9f4-e9017c02ed18",
              "10226dd1-d701-4285-b364-51fedd8bebf1",
              "e58a459d-8938-4f0d-bcd9-6ce3bf080919"
            ]
          }
        ],
        "attributes": {
          "program": "none",
          "usage": "Private office"
        }
      }
    ],
    "elements": [
      {
        "id": "a2b416cc-b45c-4e4b-ab02-b6ea8b7a86a6",
        "type": "element:wall",
        "parameters": {
          "width": 0.15,
          "height": 3,
          "offset": -0.075,
          "elevation": 0,
          "materials": {
            "top": "#202020",
            "bottom": "#000000",
            "side1": "asm:basic-wall",
            "side2": "asm:basic-wall",
            "join1": "asm:basic-wall",
            "join2": "asm:basic-wall"
          }
        },
        "edge": "10226dd1-d701-4285-b364-51fedd8bebf1"
      },
      {
        "id": "b2f0c124-2c74-49a1-975b-774e9cf35b89",
        "type": "element:wall",
        "parameters": {
          "width": 0.15,
          "height": 3,
          "offset": -0.075,
          "elevation": 0,
          "materials": {
            "top": "#202020",
            "bottom": "#000000",
            "side1": "asm:basic-wall",
            "side2": "asm:basic-wall",
            "join1": "asm:basic-wall",
            "join2": "asm:basic-wall"
          }
        },
        "edge": "4df310c9-01d8-4555-a9f4-e9017c02ed18"
      },
      {
        "id": "2f5fef72-997c-4e23-a04c-96e0c94c5f80",
        "type": "element:wall",
        "parameters": {
          "width": 0.15,
          "height": 3,
          "offset": -0.075,
          "elevation": 0,
          "materials": {
            "top": "#202020",
            "bottom": "#000000",
            "side1": "asm:basic-wall",
            "side2": "asm:basic-wall",
            "join1": "asm:basic-wall",
            "join2": "asm:basic-wall"
          }
        },
        "edge": "7060299c-d755-434d-aaff-30c023bef636"
      },
      {
        "id": "540593c3-db1c-4c92-8249-bf9275d1d3fe",
        "type": "element:wall",
        "parameters": {
          "width": 0.15,
          "height": 3,
          "offset": -0.075,
          "elevation": 0,
          "materials": {
            "top": "#202020",
            "bottom": "#000000",
            "side1": "asm:basic-wall",
            "side2": "asm:basic-wall",
            "join1": "asm:basic-wall",
            "join2": "asm:basic-wall"
          }
        },
        "edge": "e58a459d-8938-4f0d-bcd9-6ce3bf080919",
        "elements": [
          {
            "id": "6a688745-449d-40f6-a8b0-8e326da106f5",
            "type": "element:opening",
            "parameters": {
              "position": [
                2.5,
                0
              ],
              "dimensions": [
                1,
                2
              ],
              "materials": {
                "top": "#FFFFFF",
                "bottom": "#FFFFFF",
                "sides": "#FFFFFF"
              }
            },
            "elements": [
              {
                "id": "19747ebe-52d3-43be-be5f-6ec5456118ec",
                "type": "element:door",
                "position": [
                  0,
                  0,
                  0
                ],
                "parameters": {
                  "length": 1,
                  "height": 2,
                  "frameThickness": 0.05,
                  "frameDepth": 0.15,
                  "doorType": "singleSwing",
                  "doorAngle": 90,
                  "hardware": true,
                  "hingeSide": "right",
                  "doorSide": "side2",
                  "materials": {
                    "frame": "asm:doorLeaf-flush-white",
                    "leaf": "asm:doorLeaf-flush-white"
                  }
                },
                "rotation": 0,
                "rotationAxis": [
                  0,
                  1,
                  0
                ]
              }
            ]
          }
        ]
      },
      {
        "id": "f114c64b-50fb-4341-b8a8-e137116a0679",
        "type": "element:floor",
        "boundaries": [
          {
            "edges": [
              "7060299c-d755-434d-aaff-30c023bef636",
              "4df310c9-01d8-4555-a9f4-e9017c02ed18",
              "10226dd1-d701-4285-b364-51fedd8bebf1",
              "e58a459d-8938-4f0d-bcd9-6ce3bf080919"
            ]
          }
        ],
        "parameters": {
          "height": 0.2,
          "elevation": 0,
          "materials": {
            "top": "asm:basic-floor",
            "bottom": "#808080",
            "sides": "asm:basic-wall"
          }
        }
      },
      {
        "id": "8311dab0-6090-46a2-b0c5-2d93db39edfe",
        "type": "element:asset",
        "position": [
          3,
          0,
          4
        ],
        "rotation": 0,
        "rotationAxis": [
          0,
          1,
          0
        ],
        "product": "d18c0430-b896-44c2-8b5e-d38defe18a08",
        "geometries": []
      },
      {
        "id": "aad211c0-00ae-49c0-b7a1-959944f4c26c",
        "type": "element:asset",
        "position": [
          3,
          0,
          4.5
        ],
        "rotation": 180,
        "rotationAxis": [
          0,
          1,
          0
        ],
        "product": "7ed123c3-b6c8-4b76-9b4e-69a1562c179b",
        "geometries": []
      }
    ],
    "annotations": [],
    "views": []
  },
  "sharedResources": {
    "products": [
      {
        "id": "d18c0430-b896-44c2-8b5e-d38defe18a08",
        "type": "product:static",
        "attributes": {
          "categories": [
            "tables"
          ],
          "subCategories": [
            "desk"
          ],
          "thumbnail": "https://microservices.archilogic.com/storage/get/60b0f3cd-60d9-43a4-ada2-508051bb2eda/535e624259ee6b0200000484/220527-1148_xwD24l/snapshot.png",
          "boundingBox": {
            "min": [
              -0.8,
              0,
              -0.4
            ],
            "max": [
              0.8,
              0.75,
              0.4
            ]
          },
          "updatedAt": "2022-09-23T09:16:50.232Z"
        },
        "geometries": [
          {
            "type": "reference:geometry",
            "geometry": {
              "type": "geometry:uri",
              "format": "data3d",
              "id": "productGeometry-d18c0430-b896",
              "uri": "/535e624259ee6b0200000484/220523-1625-5rnt5i/archilogic_2022-05-23_16-25-07_1Z50Tb.gz.data3d.buffer"
            }
          }
        ],
        "name": "Desk 160/80"
      },
      {
        "id": "7ed123c3-b6c8-4b76-9b4e-69a1562c179b",
        "type": "product:static",
        "attributes": {
          "categories": [
            "seating"
          ],
          "subCategories": [
            "taskChair"
          ],
          "thumbnail": "https://microservices.archilogic.com/storage/get/60b0f3cd-60d9-43a4-ada2-508051bb2eda/535e624259ee6b0200000484/220523-0237_d9124l/snapshot.png",
          "boundingBox": {
            "min": [
              -0.35,
              0,
              -0.31
            ],
            "max": [
              0.35,
              1.06,
              0.31
            ]
          },
          "seatCapacity": 1,
          "updatedAt": "2022-09-23T09:09:16.075Z"
        },
        "geometries": [
          {
            "type": "reference:geometry",
            "geometry": {
              "type": "geometry:uri",
              "format": "data3d",
              "id": "productGeometry-7ed123c3-b6c8",
              "uri": "/535e624259ee6b0200000484/220504-1057-cb8z7m/archilogic_2022-05-04_10-57-51_pgm4gn.gz.data3d.buffer"
            }
          }
        ],
        "name": "Task Chair"
      }
    ],
    "geometries": [],
    "materials": [],
    "relations": []
  }
}

You can clearly see how much better the data is organized, for example with position and properties attributes. With Scene Structure, spaces needed to be calculated based on all the walls’ position, length and thickness. Now with Space Graph, a space is simply defined by edges. The data can also be checked for validity because a vertex has to belong to an edge, edges must intersect at vertices, etc.

The new Space Graph format is also self-contained, as it for example contains the product data for the assets (desk and chair). For Scene Structure, this data was externalized in the 2D/3D renderer.

The old and the new data format being so fundamentally different, abstracting them as much as possible was an essential first step, as described in the previous section. But it also meant that pretty much everything non-UI-related has been rewritten. Another difficulty in the transition to the new format was that a lot was designed and implemented by the Space Graph SDK team members along the way. I would often be blocked for a couple of days, then be submerged by a slew of improvements I needed to integrate into the Editor app.

Other improvements

I was a bit reluctant at first to introduce even more changes in parallel to swapping out the data format, but it turned out that the concepts the team wanted to push were totally worth it.

One of those improvements was enforcing that all changes to the Space Graph data would go through undo-redo operations. Those are an integral part of the Space Graph SDK, so the client apps do not need to know about all the side effects of, for example, deleting a vertex or an edge. They are called undo-redo because you can then replay the user’s actions very easily, even in a fun way:

Undo-redo operations: notice that operations are batched to accurately represent a single user interaction
Undo-redo operations: notice that operations are batched to accurately represent a single user interaction

Another massive improvement was changing how walls are drawn. It is very important to make sure the graph behind Space Graph is as simple as possible, i.e. create the fewest possible vertices. We added all kinds of snapping behaviors for that:

Wall snapping: a wall is essentially an edge with some geometry (thickness). We want all edges to connect at vertices. Therefore, the cursor is nudged towards the wall’s edge as often as possible.
Wall snapping: a wall is essentially an edge with some geometry (thickness). We want all edges to connect at vertices. Therefore, the cursor is nudged towards the wall’s edge as often as possible.

Coding fun

The wall snapping was only one of many user interactions that were very fun to code. I had not done much graphics programming since my studies 20 years ago, and it was the thing I enjoyed most at the time.

A feature that had been requested for years was “box multi-selection”: the user draws a rectangle (box), and everything inside that rectangle gets selected. Every drawing app has this. In a couple of days, I came up with an implementation involving object-aligned and axis-aligned bounding boxes, and a 2D spatial indexing library called RBush:

selectCandidates() {
  if (!this.selectionRectangle) {
    return
  }
  this.selectedNodeIds = new Set<string>()
  const selectionRectangleMinX = this.selectionRectangle[0][0]
  const selectionRectangleMinY = this.selectionRectangle[0][1]
  const selectionRectangleMaxX = this.selectionRectangle[2][0]
  const selectionRectangleMaxY = this.selectionRectangle[2][1]
  // find intersections of the selection rectangle with axis-aligned bounding
  // boxes of layout nodes
  const candidatesAfterFirstPass: SelectionCandidate[] = this.spatialIndex.search({
    minX: selectionRectangleMinX,
    minY: selectionRectangleMinY,
    maxX: selectionRectangleMaxX,
    maxY: selectionRectangleMaxY
  })
  // first pass: non-rotated layout nodes are final matches
  //
  // Non-rotated layout nodes have identical axis-aligned and object-aligned
  // bounding boxes, so we know for sure that they have to be selected
  const candidatesForSecondPass: SelectionCandidate[] = []
  for (const selectionCandidate of candidatesAfterFirstPass) {
    if (selectionCandidate.isAxisAligned) {
      this.selectedNodeIds.add(selectionCandidate.nodeId)
    } else {
      candidatesForSecondPass.push(selectionCandidate)
    }
  }
  // second pass: layout nodes with their axis-aligned bounding boxes entirely
  //              inside the selection rectangle are final matches
  const candidatesForThirdPass: Partial<SelectionCandidate>[] = []
  for (const selectionCandidate of candidatesForSecondPass) {
    const rectangleContainsCandidate =
      selectionRectangleMinX <= selectionCandidate.minX &&
      selectionCandidate.maxX <= selectionRectangleMaxX &&
      selectionRectangleMinY <= selectionCandidate.minY &&
      selectionCandidate.maxY <= selectionRectangleMaxY
    if (rectangleContainsCandidate) {
      this.selectedNodeIds.add(selectionCandidate.nodeId)
    } else {
      candidatesForThirdPass.push({
        objectAlignedBoundingBox: selectionCandidate.objectAlignedBoundingBox,
        nodeId: selectionCandidate.nodeId
      })
    }
  }
  // third pass: layout nodes with an intersection between the edges of their
  //             object-aligned bounding box and the edges of the selection
  //             rectangle are final matches.
  //
  // This effectively means that there is an intersection between the selection
  // rectangle and the layout node’s object-aligned bounding box
  for (const selectionCandidate of candidatesForThirdPass) {
    if (this.rectangleAndCandidateIntersect(selectionCandidate.objectAlignedBoundingBox || [])) {
      this.selectedNodeIds.add(selectionCandidate.nodeId)
    }
  }
  if (this.selectedNodeIds.size === 0) {
    this.emit('unselectAll')
  } else {
    this.emit('select', this.selectedNodeIds)
  }
}

The code is surprisingly simple, but it took me several iterations to get to that point. In debug mode, the algorithm is kinda visible:

Box multi-selection powered by spatial indexing
Box multi-selection powered by spatial indexing

Once users could easily multi-select elements, they obviously wanted to move and rotate them. I one-upped it and added the possibility to move the rotation center. For assets (desks, chairs, …) it makes sense. Less so for walls, but it is fun to use:

Moving and rotating a multi-selection
Moving and rotating a multi-selection

Another often requested feature was ways to align assets. The minimal version to do it is bounding box snapping:

Asset to asset bounding box snapping
Asset to asset bounding box snapping

A last example of 2D-programming is the wall connection helper. Instead of having the user painstakingly try to position 2 walls so they would nicely align and connect, do the heavy lifting in the app:

Let walls connect if their edges are aligned, or if their geometries would intersect
Let walls connect if their edges are aligned, or if their geometries would intersect

More typical web app programming was involved the last two interactions I want to show here: window/door frame positioning, and wall location line positioning:

Window/door frame positioning
Window/door frame positioning
Cycling through wall location line positions and adjust wall thickness via keyboard shortcuts or form inputs
Cycling through wall location line positions and adjust wall thickness via keyboard shortcuts or form inputs

Visual fun

In frontend development, there is the coding fun, and then there is the satisfaction of creating something visually fun. I really liked the data visualization aspect of my time at Archilogic.

Workflows plugin: the floor plan conversion team uses this plugin to enforce a given style, e.g. wall heights, wall materials, types of furniture, etc.
Workflows plugin: the floor plan conversion team uses this plugin to enforce a given style, e.g. wall heights, wall materials, types of furniture, etc.
Quality Control plugin: the original floor plan image is displayed, walls in semi-transparent green. In the current step, the Quality Control agent has to assess (pass/fail) whether all walls are correctly modeled, and can add a comment.
Quality Control plugin: the original floor plan image is displayed, walls in semi-transparent green. In the current step, the Quality Control agent has to assess (pass/fail) whether all walls are correctly modeled, and can add a comment.
Quality Control plugin: it illustrates the power of theming in the 2D canvas.
Quality Control plugin: it illustrates the power of theming in the 2D canvas.
BOMA plugin: I was not involved in the development of this plugin, but it is included here to showcase the power of the Space Graph data model, and because it is very colorful.
BOMA plugin: I was not involved in the development of this plugin, but it is included here to showcase the power of the Space Graph data model, and because it is very colorful.
Graph plugin: I was also not involved in the development of this plugin, but it illustrates the graph aspect of the Space Graph data model. Adjacency of spaces becomes very clear. Indoor routing could for example use this information.
Graph plugin: I was also not involved in the development of this plugin, but it illustrates the graph aspect of the Space Graph data model. Adjacency of spaces becomes very clear. Indoor routing could for example use this information.

Unit tests, and the future

If you have worked with me, you know that I value having a good test coverage. I introduced coverage measurement in the Editor app 2 months after I joined. It was… bad. I never got to reach my 80% coverage goal, but tests got a lot better:

when I joined after 2.5 years
branches 8% 53%
functions 7% 62%
lines 10% 67%
statements 10% 66%

I semi-succeeded to make my fellow colleagues adopt the more descriptive test descriptions, making tests act as specifications. So I tried to lead by example. Here are some of the nicer ones.

Some unit tests for the Quality Control plugin:

QualityControl > renders all progress bars at 0%, 1 opened group and sub-group with 1 active check and 1 unanswered check, 1 closed group and sub-group with 1 unanswered checks, 1 closed group and sub-group with 2 unanswered checks
QualityControl > when skipping to check 2.1.1 and making it pass > renders the overall progress bar at 20%, the group 2 and sub-group 2.1 progress bars at 100%, the other progress bars at 0%, 1 closed group and sub-group with 2 unanswered checks, 1 opened group and sub-group with 1 answered check, 1 opened group and sub-group with 1 active check and 1 unanswered check
QualityControl > when skipping to check 2.1.1 and making it pass > then going back to check 1.1.1 and making it pass > renders the overall progress bar at 40%, the group 1 and sub-group 1.1 progress bars at 50%, the group 2 and sub-group 2.1 progress bars at 100%, the group 3 and sub-group 3.1 progress bars at 0%, 1 opened group and sub-group with 1 answered check and 1 active check, 1 opened group and sub-group with 1 answered check, 1 closed group and sub-group with 2 unanswered checks
QualityControl > when skipping to check 2.1.1 and making it pass > then going back to check 1.1.1 and making it pass > then the check 1.1.2 fails > renders the overall progress bar at 60%, the group 1, sub-group 1.1, the group 2 and sub-group 2.1 progress bars at 100%, the group 3 and sub-group 3.1 progress bars at 0%, 1 closed group and sub-group with 2 answered checks, 1 opened group and sub-group with 1 active and answered check, 1 opened group and sub-group with 2 unanswered checks
QualityControl > when skipping to check 2.1.1 and making it pass > then going back to check 1.1.1 and making it pass > then the check 1.1.2 fails > then the check 3.1.1 fails > renders the overall progress bar at 80%, the group 1, sub-group 1.1, the group 2 and sub-group 2.1 progress bars at 100%, the group 3 and sub-group 3.1 progress bars at 50%, 1 closed group and sub-group with 2 answered checks, 1 closed group and sub-group with 1 answered check, 1 opened group and subgroup with 1 answered check and 1 active check
QualityControl > when skipping to check 2.1.1 and making it pass > then going back to check 1.1.1 and making it pass > then the check 1.1.2 fails > then the check 3.1.1 fails > then the comment on check 1.1.2 is modified, then the user goes back to group 3 and sub-group 3.1 and check 3.1.2 passes, and finally the report is shown > renders a report

Some unit tests for the 2D wall connection helper:

ConnectEdgesAction > when there are 2 walls that would intersect if they were longer > connects the walls
ConnectEdgesAction > when there are 2 walls that are on the same line and that would intersect if they were longer > connects the walls
ConnectEdgesAction > when there are 2 walls that are on the same line and that would intersect if they were longer (independent of edge directions) > connects the walls
ConnectEdgesAction > when there are 2 "vertical" walls (all vertices have same x value) that are on the same line and that would intersect if they were longer > connects the walls
ConnectEdgesAction > when there are 2 walls that are on the same line, and that would intersect if they were longer, but there is another wall in-between > does nothing
ConnectEdgesAction > when there are 2 parallel walls with geometries that would not overlap even if the walls were longer > does nothing
ConnectEdgesAction > when there are 2 parallel walls with geometries that overlap > connects the walls via a connector edge
ConnectEdgesAction > when there are 2 parallel walls with geometries that would overlap if the walls were longer > connects the walls via a connector edge

Some unit tests for a UI component in the Editor app:

Toolbar
  when nobody is logged in
    ✓ renders nothing
  when somebody is logged in
    ✓ renders a menu button
    ✓ does not render an input for the name
    ✓ renders 2 slots
    ✓ renders a user menu button
    then a floor is loaded
      ✓ renders a skeleton loader
      then the floor name is available
        ✓ renders the floor name
        then the name is changed and the saving succeeds
          ✓ calls the API
          ✓ displays a success message
        then the name is changed but the saving fails
          ✓ displays the error
  when somebody with insufficient rights is logged in
    ✓ does not render the 2 slots

Sometimes ASCII drawing would help make tests more readable. For example, in this test for the aforementioned multi-selection rotation, there are 3 selected points a, b and c (d is not selected) that are rotated 90º around the point o, resulting in points (a), (b) and ©:

/**
 *     ┆
 * ·   ┼   ·   c   ·   ·   ·   ·   ·
 *     ┆
 * ·   ┼   ·   ·   ·   ·   ·   ·   ·
 *     ┆
 * ·   a   · (a)b  ·   ·   ·   ·   ·
 *     ┆
 * ·   ┼   o   ·   ·   ·   ·   ·   ·
 *     ┆
 * ┼┄┄┄┼┄┄┄┼┄┄(b)┄┄┼┄┄(c)┄┄┼┄┄┄d┄┄┄┼
 *     ┆
 */

Or this drawing for the bounding box calculation tests:

/**
 *      ┆
 *  ·   ┼   ·   x   ·   ·
 *      ┆     ⟋   ⟍
 *  ·   ┼   x   ·   ⟍   ·
 *      ┆     ⟍       ⟍
 *  ┼┄┄┄┼┄┄┄┼┄┄┄⟍┄┄┄┼┄┄┄x
 *      ┆         ⟍   ⟋
 *  ·   ┼   ·   ·   x   ·
 *      ┆
 */

I have not gotten the chance to improve the overall architecture of the app. I feel like the UI could be further uncoupled from the app’s core logic. State management could be improved as well.

Kudos

I would like to end this by giving some kudos to the people I worked closely with: