manipulator

The maths a translate/rotate/scale widget needs, as pure functions. No scene, no engine, no pointer — so the part most likely to be subtly wrong is the part that can be tested without a browser. manipulator-view draws it and b3d-manipulator is the element you actually use.

Each grip answers one question: given where the pointer is aiming now, what value should this handle have?

grip reading
translate the point on the axis closest to the pointer's ray
planar where the ray crosses the plane the grip lies in, as a point
rotate the angle around the axis where the ray crosses its plane
scale the same projection as translate, read as a ratio
uniform the ray's distance from the widget centre, read as a ratio

A grip, not a mode

The widget shows every enabled affordance AT ONCE and lets the grip you grab say what the drag means — the shape Cheetah 3D's universal manipulator made its reputation on. A mode switch asks the author to declare their intent twice: once to the toolbar, and again to the handle. Here Grip is the whole vocabulary, and it is what a pick returns.

Snapping quantises the VALUE, not the movement

A drag that snaps by stepping the delta accumulates error: sixty frames of round(delta) is not round(sixty deltas), and a piece walks off the grid over a long drag. Quantising the resulting absolute value instead means a snapped object is always exactly on the grid, however it got there.

The one exception is ROTATION, which snaps its DELTA — a 15° step about the ring you grabbed is what the setting promises, and it has to survive a start rotation that is not itself on the grid.

The drag measures against a FROZEN frame

beginDrag captures the object's axes and holds them for the whole gesture. Reading them live is a feedback loop for rotation: measure the angle in the object's current frame, apply it, and the frame has now turned by what you just applied — so the next sample measures against a basis your own output moved. That reads, from the outside, as "a tiny movement spins the thing hundreds of degrees".

Where this came from

Ported from tosijs-3d-ensemble, where it was built and shaken out against real authoring — including on a phone, which is where most of the sizing decisions in manipulator-view were won. The maths is unchanged; the shape is not. Ensemble's version speaks in [x, y, z] tuples and writes into an ensemble document, so it is re-expressed here in this library's {x, y, z} and returns a transform rather than editing anything.