6 November 2014
Sliding Along 3D Walls

How do you move a group of objects synchronously around the inside of a closed space? It’s not as easy as it originally sounded to me.

One of the projects I did for a Meteor development company in Cincinnati was conceptualizing and developing a 3D editing and visualization system that let their client’s users design their closets.

For me, it was a beautiful thing to work with 3D on the web, especially with Meteor’s reactive interactivity. Visualization technology has come a long way since I first started playing with computer graphics 40 years ago, but like any good domain there are still interesting applied problems in it that crop up. It’s one of the reasons that I really love programming.

I’m going to simplify one of those interesting problems I encountered for the purposes of this post, but let’s say what it amounts to is sliding a set of pictures along the walls in a room. I’ll take a few liberties with the process, but I’ll stay true to the problem. I’m also going to take liberties with the code here, as it’s more like pidgin english only intended to get the idea across succinctly.

So with those caveats, I’ll jump in.

The 3D Coordinate Space

I learned way back to visualize 3-space as a left-handed coordinate system with the positive X axis to the right, positive Z axis going up, and positive Y axis coming out at you. This makes intuitive sense to me — Z is elevation above a flat page, like looking at a relief map spread out on a table, and if you orient the system so Y is up and X is to the right, things farther from you have bigger Z values.

three.js was chosen to do the rendering because it’s pretty awesome, but unfortunately for me it uses the right-handed coordinate system: the positive X axis is still to the right, but the positive Y axis is going up and the positive Z axis is coming out of the page. Basically, all this means is that when Z values are increasing, objects are getting closer to the observer. (This is how Z order on the web works — although on the web, Y is positive going down the page, and if you rotate Y to up, the web is actually using a left-handed coordinate system.)

This may seem like it’s pretty trivial, but it’s like having to forget how to ride a bike and then re-learn a different way, or like forcing yourself to tie your shoes starting with the laces left-over-right instead of right-over-left. It feels otherworldly and completely unnatural. Your mental muscle memory is severely compromised.

The 3D Room

What all my coordinate system whining really means is that constructing the room felt unnatural to me… but I got through it. Walls in the simplified room aligned with planes perpendicular to the X and Z axes. The 3D origin was chosen to be the geometric center of the room. Looking through the front wall of the room to the back, left is in the negative X direction, and right is positive X. The X extent spans the width of the room, and the Z extent its depth. The Y extent is the height of the room.

Given the placement of the origin (0,0,0), the room is divided into eight octants. But I really didn’t care any more about this except that I always started the visualization by looking at the origin as the object point from some other point as the viewpoint. It was convenient to think of the room as symmetrically surrounding the 3D origin.

The room itself was constructed in an interesting way. To give each wall a negative-left, positive-right feel, everything was built as if it were the back wall and rotate it around the Y axis into place. This is a tremendously human simplification: it removes all the special casing that would otherwise be needed if the walls were constructed in coordinate system orientation. When you’re looking at any wall inside-face on, left and right are in the appropriate directions.

Hanging Pictures on the Walls

The real problem isn’t about pictures, but they’ll do for this discussion. They are flat and fit on a wall.

The idea is that when you put a picture on a wall, you’re subject to a set of constraints. First they need to hang on a wall — not the floor or the ceiling — Y is always along the vertical axis of the picture. Second, they can hang on any wall, front, back, left or right in this case. And third, they must be flat on a wall — you can’t go too far into a corner without intersecting a wall — or above the ceiling or below the floor.

Now, for the sake of freedom of expression, I’ll relax the third rule slightly. If a picture intersects a corner, it will go through it — but only to it’s halfway point. Whichever wall the halfway point of the picture is on, that’s the wall the picture will lay flat against.

With all that background set up, I can now talk about the problem of moving pictures on the walls.

Sliding a Picture on the Walls

Stepping into interactions, I can select a picture by touching it, and I can move it around the room by dragging it along the wall. When I let go, the picture sticks on the wall where I dragged it.

If I drag it into a corner, it stays on the wall until the picture’s halfway point crosses the edge of the wall it’s on and then it rotates onto the other wall. I don’t ever lift the picture up and put it down — I slide it along the walls.

That may seem crazy… when you want to move a physical picture to another spot on in a physical room, you don’t drag it on the wall. But for my purposes, keeping it on the wall was useful, and pixels don’t leave marks and the pictures don’t require picture hangers.

Sliding a Group of Pictures on the Walls

Now things get harder — they aren’t so clear-cut when you’re dragging a group of pictures.

I first extended the selection model to easily toggle pictures in and out of a selection. This let me select one or more pictures by touching each of them. If I want to grab a whole set of pictures at once — touch, touch, touch and I’m set to go.

When starting to drag one of the selected pictures… what should happen? It makes sense to move the whole set together as a group. Drag up and they all move up. Drag down, all down. Left, right — left, right. But what should happen when I drag them to a corner?

Wrong Turn #1

Working too fast is a great way to find out what you don’t know. And it can lead to interesting results that are just plain incorrect.

I blasted through the code, first separating the picture being dragged directly from the rest of the selected pictures. Then I just had the selected pictures track the relative movement of the dragging picture.

diff3 = draggedNewPosition3 - draggedOldPosition3
rot3 = cornerRotationFromPosition(draggedNewPosition)
for selected in selection
  selected.position3 += diff3
  selected.rotation3 = rot3

That worked like a charm until I hit a corner. All of a sudden, every picture in the selection, wherever it was in the middle of moving, rotated in place and unless it’s horizontal midpoint coincided with the picture being dragged, began to move away from the wall. The dragged picture made the appropriate movements and rotations, but the rest of the selection spun and moved in the same direction as the dragged object — not staying on the walls as I wanted it to.

Wrong Turn #2

Clearly, the selected pictures shouldn’t be spinning around their centers, they should be spinning around a common center, that being the center of the picture being dragged. When it spins, they should all spin around the common point, the whole of the selection rotating onto the other wall.

Grouping the selection temporarily in a new 3D object,

diff3 = draggedNewPosition3 - draggedOldPosition3
rot3 = cornerRotationFromPosition(draggedNewPosition)
selectedGroup.position3 += diff3
selectedGroup.rotation3 = rot3

Beautiful! The whole group turned at once and what was flat against one wall was now flat along the other! Sort of… Some objects were far off the wall on one side of the dragged picture or the other. And if the selection included picture from multiple walls, everything was messed up.

Rethinking

What was breaking down in both of the wrong turns was the third of the three constraints. The midpoints of the pictures were spinning before they individually hit the corner, unknowingly first and then knowingly. I had found two ways to make it fail spectacularly.

Instead of what I’d tried, the selected pictures needed to act like they were being dragged individually, as if there were n drags going on simultaneously.

What Does Synchronous Movement Really Mean?

So I’d finally got to the root of the issue. If there are a set of pictures all moving synchronously along the walls of the room, each interacting with corners correctly, then I needed a different way to talk about movement than in pure 3D.

What’s actually happening when you think about it, is that the movement is occurring in 2D on the inside of a box. It’s 3D in the sense that you’re looking at 3D renditions of pictures in a 3D environment you can walk through, but the movement of the pictures as defined is all about moving on the surface of the walls.

So, what does 2D movement mean on the walls of the 3D box? Well, that’s straightforward. You can move a picture up, down, left and right. Up and down is easy. That’s just the Y coordinate.

diffY = draggedNewPosition3.y - draggedOldPosition3.y
diffVertical = diffY

So it’s easy to know from the dragged movement how far the pictures moved vertically. But horizontally? That’s a little different.

Right and Left Movement

Unfortunately, it isn’t as easy to figure left-right movement as up-down since left-right changes it’s coordinate system depending on which wall you’re on. In the simple rectangular box situation, each wall is different — the back and front walls depend on X, the left and right walls depend on Z, and the coordinates are pairwise-reversed for each.

This is not a pretty situation. How the heck do you even start to figure out the horizontal distance between two points on different walls?

The simple answer: you measure it! Let’s go back to first principles of arithmetic. What is 8-3? It’s 5 of course, everyone knows that. But stop and think: what does 8-3 really mean?

A number n is the magnitude in a consistent frame. Measurement only makes sense if there’s an established frame of reference in which magnitudes can be compared. So

8 - 3 = 5

is just the shorthand that everyone learned a long time ago. Really, it’s

(8-0) + (0-3) = 5

Numbers are actually the distance to the origin of the number line.

This perspective is just what I needed to figure out the horizontal movement. I had to pick a point inside the box and make it the left-right origin, measure the distances along the walls to both the new and old points, and subtract them to get the horizontal difference.

distNew = horizontalDistanceFromReference(draggedNewPosition3)
distOld = horizontalDistanceFromReference(draggedOldPosition3)
diffHorizontal = distNew-distOld

To make things easy, I aligned horizontal measurement with the left and right directions the back wall: left was negative and right was positive.

Measuring Distance Around the Box

For no other better reason, I picked the corner between the front and the left face as the origin. Any arbitrary point on the perimeter will do, so the front-left corner was as good as any.

horizontalDistance =
  if position3.x == -width/2                 # left wall
    depth/2-position3.z
  else if position3.z == -depth/2            # back wall
    depth + position3.x+width/2
  else if position3.x == width/2             # right wall
    depth+width + position3.z+depth/2
  else if position3.z == depth/2             # front wall
    depth+width+depth + width/2-position3.x

Now, I’m not going to go into the detail of what to do for the walls in a non-rectangular room, where walls are angled. Suffice to say it means using a little trigonometry. It’s not hard, but it takes away from my discussion a little bit. I also didn’t bother to iterate through an in-order wall array &mdash again just extra complication. The takeaway is I walk the walls in-order and generate the position.

Moving to a new Wall Position

Since I know how far the horizontal movement is, now I have to apply it to all the pictures in the selection. This turns out to just be a simple reversal of the distance calculation. For each picture,

newHorizontalDistance = oldHorizontalDistance+horizontalDifference
if newHorizontalDistance < depth
  position3.x = -width/2
  position3.z = depth/2 - newHorizontalDistance
else if newHorizontalDistance < depth+width
  position3.z = -depth/2
  position3.x = (newHorizontalDistance-depth) - width/2
else if newHorizontalDistance < depth+width+depth
  position3.x = width/2
  position3.z = (newHorizontalDistance-depth-width) - depth/2
else if newHorizontalDistance < depth+width+depth+width
  position3.z = depth/2
  position2.x = width/2 - (newHorizontalDistance-depth-width-depth)

It’s not hard, but I needed to be careful of the directions of the axes related to the different walls.

And Getting the Appropriate Wall Rotation

Remember how I built the walls? I used the back wall as a reference and rotated them into place. I then used a calculation just like the one for horizontal distances to get the picture rotations:

rotation.Y =
  if position3.x == -width/2              # left wall
    leftWall.rotation.Y
  else if position3.z == -depth/2         # back wall
    backWall.rotation.Y
  else if position3.x == width/2          # right wall
    rightWall.rotation.Y
  else if position3.z == depth/2          # front wall
    frontWall.rotation.Y

Three Lefts Make a Right

One more thing, just to make sure it’s clear. When you travel the whole way around the box, you end up at the starting place. This logically implies travel in either direction, left or right, to reach the same horizontal point on the box. If you’re interested,

perimeter = 2*(width+depth)
if abs(horizontalDistance) > perimeter/2
  horizontalDistance = -sign(horizontalDistance)*(perimeter-abs(horizontalDistance))

This sets the horizontal distance to either left (negative) or right (positive), whichever has the smallest magnitude. It doesn’t matter, but it’s nice to know the transformation.

This also implies that if the amount of the difference is bigger than the perimeter or less than zero, it can be normalized to be between zero and the perimeter — just to keep everything behaving nicely.

Putting it All Together

I now had all the pieces needed to drag a set of selected pictures along the walls of a room to a new position.

diffVertical = draggedNewPosition3.y - draggedOldPosition3.y
diffHorizontal =
  horizontalDistanceFromReference(draggedNewPosition3) -
  horizontalDistanceFromReference(draggedOldPosition3)
for selected in selection
  selected.position3.y = selected.position3.y+diffVertical
  distOld = horizontalDistanceFromReference(selected.position3)
  distNew = distOld+diffHorizontal
  if distNew < depth
    selected.position3.x = -width/2
    selected.position3.z = depth/2 - distNew
    selected.rotation.y = leftWall.rotation.y
  else if distNew < depth+width
    selected.position3.z = -depth/2
    selected.position3.x = (distNew-depth) - width/2
    selected.rotation.y = backWall.rotation.y
  else if distNew < depth+width+depth
    selected.position3.x = width/2
    selected.position3.z = (distNew-depth-width) - depth/2
    selected.rotation.y = rightWall.rotation.y
  else if distNew < depth+width+depth+width
    selected.position3.z = depth/2
    selected.position3.x = width/2 - (distNew-depth-width-depth)
    selected.rotation.y = frontWall.rotation.y

That’s it. Now, as the selection of pictures are dragged, they moved synchronously, and turn the corners as their midpoints reach them.

Takeaways

Take a Look

You can try the finished closet design system at Organized Living to see some practical 3D in action.