UI controls in a 3D scene often need to follow an object: a delete button, dimension label, or contextual action placed next to the selected model.
A simple implementation is to take the object's world position and add a fixed 3D offset. The problem is that the projected length of that offset changes with the camera angle.
In one prototype, a delete button was anchored at the cube center plus a vertical world-space offset. During real-device testing, I noticed that in a top-down view the button moved into the center of the cube and covered the center handle of the Gizmo.
At first, this looked like a special top-view bug. Claude Code's diagnosis was different: the offset itself was degenerating in screen space. When the camera looks almost directly along the vertical axis, that 3D offset projects to nearly zero pixels, so the button collapses back toward the center.
That also means the problem is continuous rather than view-specific. As the camera approaches a vertical angle, the button gradually moves inward; the top-down view is only the extreme case.
So the fix could not be "use another position in top view." The positioning rule itself needed to change: instead of relying on a world-space direction, calculate the control from the geometry that is actually visible on screen.
The final rule uses screen-space coordinates and starts from the lowest visible element rather than from the model center.
Step 1 anchorY = max(A, B, C) + 35px
A = bottom of the cube's projected bounding box
B = lowest visible Gizmo arrow endpoint + 16px
C = lowest dimension-label anchor + 16px
Step 2 button center Y = clamp(anchorY, 28px, viewport height - 28px)
Step 3 button center X = clamp(projected bounding-box center X, 48px, viewport width - 48px)Two choices matter here.
First, the calculation starts from whatever is currently lowest on screen. Depending on camera angle and object rotation, that may be the cube, a Gizmo handle, or a dimension label. Anchoring to only one of them will fail in some orientations.
Second, the rule uses the projected bounding box rather than a specific cube edge. As the cube rotates, the lowest edge can switch from one side to another. A bounding box changes more continuously and avoids visible jumps in the button position.
clamp also handles a case that initially looked like a separate problem: what happens when the model becomes so large that its lower edge extends beyond the viewport?
If the calculated Y position falls below the viewport, the clamp simply brings the button back inside the screen-safe area. The control stays visible instead of following the model off-screen.
The same rule therefore covers normal model sizes, objects near the viewport edge, and oversized models that extend beyond the screen. No separate "large model" branch is needed.
After the first rule was defined, development suggested a simpler implementation: because the Gizmo appears at a relatively stable screen size, why not place the delete button at a fixed pixel distance below the center and avoid calculating the projected bounding box altogether?
The idea was reasonable and cheaper to implement. But when tested with the camera pulled back, the cube became smaller while the fixed offset stayed the same, leaving the button visibly detached from the model.
The hidden assumption was that all relevant objects behave the same way in screen space. They do not: the Gizmo behaves more like a screen-space UI element with a relatively stable visual size, while the cube is a real 3D object and becomes larger or smaller on screen as the camera distance changes. A fixed pixel offset works for the first case, but not for the second.
The value also has to account for a worst-case orientation, such as an axis arrow pointing downward. In most other orientations, that same fixed value creates too much empty space. At the opposite extreme, when the model becomes very large, the button can end up over the model surface instead of outside it.
The final rule therefore stayed unchanged: position the control from the current projected geometry, not from a pixel value that only works within a limited camera range.
1. The formula positions the element center. The button uses CSS translate(-50%, -50%), so the calculated coordinate refers to its center rather than its top edge. If the button is 20px tall and the desired visible gap is 25px, the center needs to be placed at 25px + button height / 2 = 35px. If the button size changes, this value needs to change with it.
2. Viewport margins also clamp the center point. The 28px and 48px values describe the button center's minimum distance from the viewport edge. The visible whitespace is smaller by half of the button's width or height.
The initial diagnosis and formula came primarily from Claude Code. My role was to verify the observed behavior, test the revised implementation, and decide whether later simplifications still held up in real use.
The most useful step came when the fixed-offset simplification was proposed. Instead of accepting or rejecting it by intuition, I tested it across camera distance and model scale.
Simplifications like this often look convincing because they work in the common case. What matters is whether the assumption behind them still holds when the camera, object size, and view angle change.