Turns drags, wheels and double-clicks into OrbitCamera3D moves, under one rule: the left button is never the camera's. More...
| Import Statement: | import Clayground.Canvas3D |
The rig knows how to move; this knows what a gesture means. Split in two because the half that decides "right drag turns, middle drag pans" is the half a scene wants to configure - while the arithmetic that turns pixels into degrees and metres is the same everywhere and was being re-derived, with different constants, in every scene that had a camera.
There were, twice. First a mode per activity - one for turning the view, one for measuring with it - which fails the moment a third instrument is imagined: five modes for one camera. Then two modes, build and use, which held up better and was still wrong, and for a reason worth writing down: a mode existed only because the camera wanted the left button. LMB-drag panned, so a scene that needed LMB had to be able to take it back, and the thing that took it back was the mode. Every symptom followed from that - two keys negotiating over one pointer, a switch on a circuit board that could not be flipped because the camera swallowed the press, a measurement destroyed by looking around.
An RTS has none of these problems, and not because it is cleverer: it never puts panning on the left button. So neither does this.
Non-visual, and deliberately not a MouseArea. A scene that also picks or drags objects already owns the pointer; it keeps its own MouseArea and asks wants() what this press means before deciding it is its own:
OrbitInput3D { id: nav; rig: rig; view: view3d }
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
cursorShape: nav.cursorShape
onPressed: (m) => {
if (nav.begin(m.x, m.y, m.button, m.modifiers) !== "") return
myTool.press(m.x, m.y) // the left button is yours
}
onPositionChanged: (m) => {
if (nav.move(m.x, m.y)) return
if (!pressed) nav.hoverAt(m.x, m.y)
myTool.moveTo(m.x, m.y)
}
onReleased: { nav.end(); myTool.release() }
onWheel: (w) => nav.wheel(w.angleDelta.y, w.x, w.y)
onDoubleClicked: (m) => nav.recenterAt(m.x, m.y)
}
begin returns the drag it took ("orbit", "pan" or "" when it declined), so the scene's own gesture is simply the "" case - and with the default buttons an LMB press is always that case.
pickAt() answers "a place and a thing" for any pixel, and hovering keeps the same answer for wherever the cursor last was. Neither is a decision: this layer no longer has an opinion about what a click means, because the scene does. A tool asks when it wants to know.
See also OrbitCamera3D.
active : bool |
A drag is in progress.
anchor : var |
The world point this orbit is turning about, or null.
Taken at press and held for the whole drag - including the coast out of a flick, so a thrown orbit keeps spinning about the same thing.
anchorOrbit : bool |
clickSlop : real |
Pixels a press may travel and still count as a click.
One button applies it here - the right one, which both turns the view and, unmoved, cancelled. Only the distance travelled tells the two apart. Scenes apply the same rule to their own gestures with the same default, so a click means the same thing everywhere.
cursorShape : int |
Closed hand while a drag runs, plain arrow otherwise.
Deliberately almost nothing. With no modes there is no camera state left for a cursor to announce, and what a press will do is now the scene's business - it knows what is in the hand, and it owns the cursor while it matters. Bind a MouseArea to this and override it where the scene has something to say.
flick : bool |
Let a fast drag coast for a moment after the button comes up.
The rig's smoothMs already smooths each step; this is the other half of the feel - throwing the scene and watching it settle. It decays to a stop within about a third of a second and never moves further than the gesture was already moving.
flickDecay : real |
Velocity kept per frame while coasting.
flickThreshold : real |
Pixels per frame a drag needs to coast at all.
gesture : string |
The drag in progress: "orbit", "pan" or "".
The only state this layer keeps about the pointer, and it lasts exactly as long as a button is down (plus the coast out of a flick).
groundY : real |
Height of the plane groundAt() hits.
hovering : var |
invertPitch : bool |
Flip the vertical orbit direction.
The default is grab-the-scene: dragging down tips the scene down, which raises the camera. Two of the three labs had worked that out independently; the third had it the other way, which is exactly the kind of drift a shared layer is for.
orbitButtons : int |
Buttons that turn the rig.
panButtons : int |
Buttons that drag the world.
The middle button alone, and that default is the whole design: adding Qt.LeftButton here is how a scene deliberately spends the left button on the view. A scene with nothing to select (a scene that only looks at something) should - left-drag panning is the most natural gesture there is, and it costs that scene nothing. A scene with a tool must not, and this layer will not do it behind its back.
panModifiers : int |
Modifiers that make any button pan.
Empty by default: the left button is the scene's, so a modifier-plus-drag would be exactly the kind of leftover this layer exists to abolish. Set it (to Qt.ShiftModifier, say) in a scene whose domain leaves that modifier free and that wants the escape hatch anyway.
panSpeed : real |
Multiplier on the grab-the-ground pan.
pickObjects : bool |
Ask the view what object a pick landed on, as well as where.
On by default; a scene whose instruments only ever want the ground can turn the ray-cast off.
pitchPerPixel : real |
Degrees of pitch per pixel dragged.
rig : var |
The OrbitCamera3D to drive.
springNav : bool |
Pan on the left button too, while this is true.
The scene feeds it a held key (the labs use Space). It is the only state in this layer that changes what an input means, and it is a quasimode rather than a mode for the reason quasimodes exist: a key you are holding down cannot be forgotten about, so nothing else has to display it and no tool can be left starved by it.
A key that gets stuck - focus lost mid-hold - is the scene's to clear, by writing false.
universalPanButtons : int |
Buttons that always drag the world.
The middle button, which no two-button tool can claim.
view : var |
The View3D the gestures happen in.
Supplies the viewport height that scales a pan, and the ray that turns a cursor position into a world point - which is what anchors an orbit and what the wheel zooms towards.
yawPerPixel : real |
Degrees of yaw per pixel dragged.
zoomStep : real |
Distance factor for one wheel notch inwards.
zoomToCursor : bool |
The wheel zooms towards the point under the cursor.
Needs the cursor position - nav.wheel(delta, w.x, w.y). Called with the delta alone it zooms towards the pivot, as it always did.
cancelled() |
A right click - "put it down".
The one click this layer reports, and it reports no place and no thing, because it is not about either: it is the RTS cancel, and what it empties is the scene's to decide - the hand, a half-drawn wire, a selection. A right drag turns the view as ever and says nothing.
Note: The corresponding handler is onCancelled.
zoomedAt(var point) |
string begin(real x, real y, int button, int modifiers) |
string beginAs(string g, real x, real y) |
Starts a drag the scene has chosen itself: "orbit" or "pan".
For a scene whose own rule decides, so it does not have to fake a button to say so. An orbit started this way anchors at (x, y) exactly like one begin took.
void cancel() |
Ends the drag with no coast.
void clearHover() |
Forgets hovering - the cursor left.
void end() |
var groundAt(real x, real y) |
The point on the groundY plane under viewport pixel (x, y).
Null when the ray never gets there. Worked out from the ray rather than picked, so it answers for every pixel of the plane including the ones no geometry covers.
var hoverAt(real x, real y) |
Recomputes hovering for viewport pixel (x, y).
Does nothing while a gesture is running: mid-drag the cursor is driving the camera rather than pointing at anything, and a preview that chased it would flicker across the whole scene. The scene calls this from its move handler when no button is down.
bool move(real x, real y) |
Continues the drag; false when no navigation gesture is running.
var pickAt(real x, real y) |
What is under viewport pixel (x, y): a place and a thing.
{ point, object, x, y }. point is where the ray meets the groundY plane, worked out analytically so it answers for every pixel of the plane including the ones no geometry covers; object is the scene node the same ray hits first, or null.
Both, from one gesture, because instruments want different halves of it: a tape measure asks where, a voltmeter asks what. Reporting them together is what keeps adding an instrument from adding an input path. Null only when there is no view to ask.
A plain query, and nothing here calls it: this layer has no opinion about what a click means, so the scene asks when it wants to know - at a press it decided was its own, or on a move through hoverAt.
bool recenterAt(real x, real y) |
Travels the pivot to the ground point under (x, y).
What a double-click on empty ground should do, and the one bit of click handling this layer offers: a single click belongs to the scene, which knows what is worth focusing - it calls rig.focusOn() itself.
string wants(int button, int modifiers) |
What this press would do, without doing it.
"orbit", "pan" or "" - the question a scene asks before it decides whether the press is its own. Nothing this layer holds can change the answer for the left button: with the default panButtons it is "", now and in every state, which is what makes "the left button is yours" a rule rather than a promise.
The order is the rule, read top to bottom: the middle button always pans; panModifiers pan if a scene declared them; the right button turns; and the left pans only where a scene asked for it in panButtons, or while springNav is held.
void wheel(real angleDelta, real x, real y) |
One wheel event: zooms in on a positive angleDelta.
Given the cursor position it zooms towards what is under it, which is how a wheel gets you somewhere instead of merely closer to the middle. Always live - see zoomToCursor.