Repository navigation
Windowing and controls
window/terminal.mojo and controls/. A TerminalWindow shows frames in the terminal and reads the keys and the mouse. The controls turn that input into a camera that moves. All of them use the standard library only.

examples/gizmo.mojo draws the gizmo. examples/viewer.mojo opens the window.
| Controls | Module | The camera |
|---|---|---|
OrbitControls |
controls/orbit_controls.mojo |
Orbits a target, with poles. |
MapControls |
controls/map_controls.mojo |
Pans over the ground, and orbits. |
TrackballControls |
controls/trackball_controls.mojo |
Turns about a target with no poles, and can roll over the top. |
ArcballControls |
controls/arcball_controls.mojo |
Turns on a virtual trackball, pans, zooms, and moves to a double click. |
FlyControls |
controls/fly_controls.mojo |
Flies and rolls on its own axes. |
FirstPersonControls |
controls/first_person_controls.mojo |
Walks, and turns toward the pointer. |
PointerLockControls |
controls/pointer_lock_controls.mojo |
Turns by the pointer's movement, and walks. |
Two more controls move objects, not the camera. DragControls drags an object across the view. TransformControls moves, turns and scales one object with a gizmo. SelectionBox selects the objects inside a dragged rectangle.
three.js: a WebGLRenderer canvas, and the controls of the same names in examples/jsm/controls/.
Run the example in a terminal:
make viewerDrag with the left button to orbit. Drag with the right button to pan. Turn the wheel to dolly. Press q, Escape or Ctrl+C to quit.
from controls.orbit_controls import OrbitControls
from core.clock import Clock
from window.terminal import TerminalWindow
var window = TerminalWindow(96, 64)
var controls = OrbitControls(Vector3(0, 0, 0))
var clock = Clock()
while running:
for event in window.poll(Duration(16.0, MILLISECOND)):
controls.handle(event, camera, 64)
_ = controls.update(camera, clock.delta())
window.present(renderer.render(scene, assets, camera))
window.close()examples/viewer.mojo is this loop with a scene, and a key that ends it.
The window is the terminal the program runs in. Mojo's standard library has no window system. The C library's terminal calls are in every process, and std.ffi.external_call reaches them.
| Member | Meaning |
|---|---|
TerminalWindow(width, height, input_fd=0, output_fd=1) |
Open the window. The size is the frame's, in pixels. |
present(frame) |
Draw a Framebuffer of the window's size. |
poll(timeout) -> List[InputEvent] |
Wait up to a Duration for input, and return the events. |
request_size() |
Ask the terminal its size. The answer is a RESIZE event from a later poll. |
resize(width, height) |
Take frames of a new size, and clear the screen. three.js: setSize. |
close() |
Put the terminal back as it was. A second call does nothing. |
encode_frame(frame) -> String |
The bytes present writes. |
A terminal cell is about twice as tall as it is wide. Each cell shows two pixels, one above the other. The character is the upper half block ▀. Its foreground is the top pixel, and its background is the bottom pixel. A frame width pixels wide takes width columns. A frame height pixels high takes height / 2 rows, rounded up.
The colors are 24-bit escape sequences. Every current terminal draws them. The alpha channel is not shown. A color is sent only where it changes along a row.
Opening puts the terminal in raw mode, so that each key arrives when it is pressed. It switches to the alternate screen and hides the cursor. It asks for xterm's SGR mouse reports of presses, drags and the wheel. close undoes all of it. A window that goes out of scope open closes itself.
Raw mode has no signals. Ctrl+C arrives as the key CTRL_C, and the program must end its loop on it.
The window never reads struct termios, whose layout differs by platform. It asks tcgetattr to fill a buffer larger than any platform's, and cfmakeraw to change a copy.
- A size that is not positive.
- An input that is not a terminal.
- A frame of another size.
- A negative or nonfinite timeout, or one too large for native poll milliseconds.
- A
presentor apollafterclose. - A terminal that cannot be written, or that hung up.
The terminal is put back before a failure is raised.
Both window types check the timeout before integer conversion. A fractional millisecond rounds down; a negative fraction raises. The whole-millisecond value must fit the native signed integer.
window/x11.mojo opens a native window on an X server. It loads Xlib when the window opens. Nothing else in the project needs the library.
On Linux, the window loads libX11.so.6. macOS has no Xlib of its own. There, the window loads the first libX11.6.dylib it finds in /opt/X11/lib (XQuartz), /opt/homebrew/lib or /usr/local/lib (Homebrew). library_path makes that choice.
| Member | Meaning |
|---|---|
X11Window(width, height, title="ThreeMojo", display="") |
Open the window. An empty display uses the DISPLAY variable. |
present(frame) |
Draw a Framebuffer of the window's size. |
poll(timeout) -> List[InputEvent] |
Wait up to a Duration for input, and return the events. |
resize(width, height) |
Take frames of a new size. |
close() |
Close the window. A second call does nothing. |
close_requested |
True after the window manager asks the window to close. |
The window gives the same InputEvent values as TerminalWindow. The same loop drives both. A key press is KEY_DOWN, and a key release is KEY_UP. Ctrl with a letter is its control code, as a terminal sends it. A change of size is a RESIZE event. The program must call resize to follow it.
The window manager's close button does not close the window. It sets close_requested, and the program must end its loop on it.
- A size that is not positive.
- A display that cannot be opened.
- A display whose visual is not 24-bit TrueColor with red in the high byte.
- A frame of another size.
- A
present, apollor aresizeafterclose.
tests/test_x11.mojo starts an Xvfb server for each test. You must install it first. On Linux, use apt-get install xvfb. On macOS, use brew install xorg-server.
controls/input.mojo. An InputEvent is one of the six kinds the browser has, or a resize.
| Kind | three.js event | Fields it sets |
|---|---|---|
KEY_DOWN |
keydown |
key |
KEY_UP |
keyup |
key. A terminal never sends it. |
POINTER_DOWN |
pointerdown |
button, x, y
|
POINTER_MOVE |
pointermove |
button held or NO_BUTTON, x, y
|
POINTER_UP |
pointerup |
button, x, y
|
WHEEL |
wheel |
wheel: 1 a notch toward the user, -1 away |
RESIZE |
the window's resize
|
x and y: the size of the largest frame that fits |
Every event also carries shift, alt and ctrl. The window gives x and y in the frame's pixels. The decoder gives a cell's column and row, from zero.
InputKind, PointerButton and Key are types, not bare integers. A Key is an ASCII code from 0 to 127, one of ARROW_UP, ARROW_DOWN, ARROW_LEFT and ARROW_RIGHT, or NO_KEY. The buttons are PRIMARY, MIDDLE and SECONDARY, with the browser's numbers.
InputDecoder.feed(bytes) turns a terminal's bytes into events.
- A byte below 128 is its key. A byte above is dropped.
- An escape that ends the bytes is the Escape key.
-
ESC [ AtoESC [ Dare the arrows.ESC [ 1 ; m Acarries modifiers. -
ESC [ < b ; x ; y Mis a press, a drag or a wheel notch. The same withmis a release. - A sequence cut off before its end is kept for the next call.
-
ESC [ 8 ; rows ; columns tis the terminal's size, aRESIZEin columns and rows. - A sequence the decoder does not know is dropped.
controls/orbit_controls.mojo. The controls orbit a camera around target. handle adds the change one event asks for to what is pending. update applies what is pending and places the camera. Call update once a frame.
| Input | Change |
|---|---|
| Left drag | Rotate. A drag the height of the view is a whole turn. |
| Middle drag | Dolly. Down moves away. |
| Right drag | Pan. The point under the pointer stays under it. |
| Wheel | Dolly by 0.95 a notch. |
| Arrow | Pan by key_pan_speed pixels. |
| Shift or Ctrl with an arrow | Rotate, at key_rotate_speed. |
| Shift or Ctrl with a left or right drag | The drag's rotate and pan swap. |
| Member | Default | Meaning |
|---|---|---|
target |
the origin | The point the camera orbits and looks at. |
enabled |
True |
False ignores all input. |
min_distance, max_distance
|
0, infinity | How near and how far the camera can be, as a Length. |
min_polar_angle, max_polar_angle
|
0, a half turn | How far down from straight above, as an Angle. |
min_azimuth_angle, max_azimuth_angle
|
minus and plus infinity | The limits about y, as an Angle. |
enable_damping, damping_factor
|
False, 0.05 |
Let a change die away over frames. |
enable_rotate, rotate_speed
|
True, 1 |
|
enable_zoom, zoom_speed
|
True, 1 |
|
enable_pan, pan_speed
|
True, 1 |
|
key_pan_speed |
7 | Pixels an arrow pans by. |
key_rotate_speed |
1 | How fast Shift or Ctrl with an arrow rotates. rotate_speed does not change it. |
key_left, key_up, key_right, key_bottom
|
the arrows | The keys that pan and rotate. three.js: keys. |
cursor, min_target_radius, max_target_radius
|
the origin, 0, infinity | A pan keeps the target within these distances of cursor, as a Length. |
auto_rotate, auto_rotate_speed
|
False, 2 |
Turns a minute while no button is held. |
primary_action, middle_action, secondary_action
|
ROTATE, DOLLY, PAN
|
What each button does. three.js: mouseButtons. |
min_zoom, max_zoom
|
0, infinity | How far an orthographic camera can zoom. |
zoom_to_cursor |
False |
Dolly or zoom toward the point under the pointer. |
rotate_left, rotate_up, dolly_in, dolly_out and pan make the same changes from code. update(camera, delta) returns True when the camera moved. The delta is a Duration, for the automatic rotation.
get_polar_angle() and get_azimuthal_angle() return the angles that the last update gave the camera, as an Angle. get_distance(position) returns the distance from a camera position to the target. save_state(camera) keeps the target, the camera position and the zoom. reset(camera) puts them back and stops the current drag.
The camera's offset from the target is a radius and two angles, as three.js's Spherical holds them. theta turns about y from +z. phi goes down from +y. A rotation adds to the angles, and a dolly scales the radius. A pan moves the target and the camera together.
The polar angle stays a millionth of a radian away from each pole. At a pole, the camera's right is undefined. A range about y with its low limit above its high limit passes through a half turn. An angle outside it goes to the nearer limit, as in three.js.
With damping, damping_factor of each pending rotation and pan applies each frame. The rest waits. A dolly is not damped, as in three.js.
handle(event, camera, width, height), update(camera, delta) and pan(dx, dy, camera, width, height) take an OrthographicCamera too. A dolly changes the camera's zoom rather than its distance, kept between min_zoom and max_zoom. A pan moves by the extent the volume covers at that zoom, over the view's size, as three.js's pan measures it.
With zoom_to_cursor set, a wheel notch or a dolly drag goes toward the point under the pointer. A perspective camera moves down the ray through the pointer by what the dolly takes off the distance. An orthographic camera moves so that the point under the pointer stays under it as the zoom changes. The target then sits straight ahead of the camera at the new distance, as in three.js.
The offset is turned into a frame whose y is the camera's up before it is read as angles, as three.js turns it. So the polar angle is measured from the camera's up, and a camera with +z up orbits about z.
screen_space_panning is True by default, as in three.js. A pan up the view then moves the target up the view. Set it to False to move the target forward instead, in the plane at right angles to the camera's up. A zoom to the cursor then keeps the target on that plane. A camera within 20 degrees of level keeps its target, as in three.js.
- Touch input is not ported.
- The controls get no camera when you make them, so they save no state then. three.js saves one.
resetraises until you callsave_state. -
get_distancetakes the camera position, because the controls do not keep the camera.
controls/map_controls.mojo. MapControls(target) returns an OrbitControls with three.js's map settings. Every member and method is OrbitControls's.
| Setting | Value |
|---|---|
primary_action |
PAN |
middle_action |
DOLLY |
secondary_action |
ROTATE |
screen_space_panning |
False |
A drag with the left button moves over the ground. A drag with the right button orbits.
A terminal sends no key-up event. The controls that move while a key is held count the key as held until key_timeout goes by with no repeat. controls/held_keys.mojo keeps this record.
- A
KEY_DOWNholds the key, or starts its time again. - A
KEY_UPlets the key go at once. A source that knows when a key goes up can send it. -
updatelets go of each key that did not repeat withinkey_timeout. - A capital letter is its small letter.
Wwith Shift isw, as three.js'sevent.codenames the key.
The default key_timeout is 0.3 seconds, a Duration. A keyboard repeats a held key about 30 times a second, so a held key stays held. Most keyboards wait about half a second before the first repeat. A held key therefore moves, stops for a moment, then moves steadily. Set key_timeout to 0.6 seconds to remove the stop. A longer timeout makes the camera coast after the key goes up.
controls/fly_controls.mojo. The camera moves along its own axes and turns about them. handle(event, width, height) changes what is held. update(camera, delta) moves and turns the camera for the time delta.
| Input | Change |
|---|---|
W, S
|
Forward, back. |
A, D
|
Left, right. |
R, F
|
Up, down. |
| Up and down arrows | Pitch up, pitch down. |
| Left and right arrows | Yaw left, yaw right. |
Q, E
|
Roll left, roll right. |
| Left button, right button | Forward, back. |
| Pointer | Yaw and pitch by its distance from the middle of the view. |
| Member | Default | Meaning |
|---|---|---|
movement_speed |
1 m/s | A Velocity. |
roll_speed |
0.01 rad/s | An AngularVelocity. |
drag_to_look |
False |
Turn only while a button is held. The buttons then do not move. |
auto_forward |
False |
Move forward with nothing held. |
key_timeout |
0.3 s | See Held keys. |
A turn is three.js's quaternion (x, y, z, 1), normalized. Here x, y and z are the pitch, yaw and roll held, times half of roll_speed and the time. three.js's rollSpeed is that half. Its default of 0.005 is 0.01 radians a second here, the same turn.
A roll changes the camera's up. The target stays ahead of the camera at the same distance. update returns True when the camera moved or turned past three.js's threshold.
three.js sets a speed multiplier on Shift and never reads it. The port leaves it out.
controls/first_person_controls.mojo. The camera walks, and turns toward the pointer. FirstPersonControls(camera) reads where the camera looks. handle(event, width, height) changes what is held and where the pointer is. update(camera, delta) moves and turns the camera.
| Input | Change |
|---|---|
W or the up arrow |
Forward. |
S or the down arrow |
Back. |
A or the left arrow |
Left. |
D or the right arrow |
Right. |
R, F
|
Up, down. |
| Left button, right button | Forward, back, while active_look is on. |
| Pointer | Turn toward it, faster the farther it is from the middle. |
| Member | Default | Meaning |
|---|---|---|
movement_speed |
1 m/s | A Velocity. |
look_speed |
0.005 deg/s | An AngularVelocity for each pixel the pointer is from the middle. |
look_vertical |
True |
False turns only left and right. |
auto_forward |
False |
Move forward with nothing held. |
active_look |
True |
False turns not at all, and the buttons do not move. |
height_speed, height_coef
|
False, 1 per second |
Move forward faster the higher the camera is. |
height_min, height_max
|
0 m, 1 m | The heights height_speed reads between. |
constrain_vertical |
False |
Keep the angle from straight up in a range. |
vertical_min, vertical_max
|
0, a half turn | That range, as an Angle. |
key_timeout |
0.3 s | See Held keys. |
The direction is a latitude and a longitude, as in three.js. The latitude stays within 85 degrees of level. look_at(camera, point) turns the camera and reads the new direction. The camera keeps its up.
A terminal reports the pointer only while a button is held. The camera keeps turning toward where the pointer last was. A browser's camera does the same while the pointer rests.
controls/pointer_lock_controls.mojo. While the controls are locked, a pointer move turns the camera at once. lock() and unlock() set is_locked. No browser asks for permission, so both always succeed.
A turn is 0.002 radians a pixel, times pointer_speed, as in three.js. Right and left turn about the world's y. Up and down turn about the camera's right. The angle from straight up stays between min_polar_angle and max_polar_angle.
A browser reports how far a locked pointer moved. A terminal reports where the pointer is, and only while a button is held. So a turn is the difference between two positions. The first position after a press or after lock only records where the pointer is.
| Member | Default | Meaning |
|---|---|---|
is_locked |
False |
Only a locked pointer turns the camera. |
min_polar_angle, max_polar_angle
|
0, a half turn | As an Angle. |
pointer_speed |
1 | A factor on the turn. |
movement_speed |
1 m/s | How fast a held key walks. |
key_timeout |
0.3 s | See Held keys. |
move_forward(camera, distance) moves along the level ground. move_right(camera, distance) moves along the camera's right. The distance is a Length. get_direction(camera) returns where the camera looks.
three.js leaves the keys to its example. Here update(camera, delta) walks by the keys held: W, A, S, D and the arrows. As in three.js, the camera's up must be +y.
controls/trackball_controls.mojo. The camera turns about target like a trackball. It has no poles, and it can roll over the top. TrackballControls(camera, target) records the camera for reset. handle(event, width, height) records where the pointer went. update(camera) applies it and places the camera.
| Input | Change |
|---|---|
| Left drag | Rotate. The camera's up turns too. |
| Middle drag, or the wheel | Zoom. |
| Right drag | Pan. |
Hold A, S or D
|
Any button rotates, zooms or pans. |
| Member | Default | Meaning |
|---|---|---|
target |
the origin | The point the camera turns about. |
rotate_speed, zoom_speed, pan_speed
|
1, 1.2, 0.3 | |
no_rotate, no_zoom, no_pan
|
False |
Turn an action off. |
static_moving |
False |
Stop a change at once. |
dynamic_damping_factor |
0.2 | How fast a change dies away. |
min_distance, max_distance
|
0, infinity | For a perspective camera, as a Length. |
min_zoom, max_zoom
|
0, infinity | For an orthographic camera. |
rotate_key, zoom_key, pan_key
|
a, s, d
|
three.js: keys. |
primary_action, middle_action, secondary_action
|
ROTATE, DOLLY, PAN
|
three.js: mouseButtons. |
key_timeout |
0.3 s | See Held keys. |
The arithmetic is three.js's. A position is read on a circle as wide as the view. A turn is the distance the pointer moved on that circle, times rotate_speed. A zoom scales the distance by one plus the part of the view the drag covered, times zoom_speed. A wheel notch is 0.025 of the view.
Unless static_moving is set, each frame keeps 1 - dynamic_damping_factor of a pan or a zoom. A turn keeps the square root of it. reset(camera) puts the camera and the target back. A turn that is dying away goes on after a reset, as in three.js.
update takes no time, as in three.js. It takes an optional delta that only ages the held keys, a sixtieth of a second by default. An orthographic camera's pan measures both directions by the view's width, as three.js's does. Touch input is not ported.
controls/drag_controls.mojo. A press picks an object with a Raycaster, and a drag moves it. The objects are nodes of the scene. DragControls(objects) takes the nodes that can be dragged.
handle(event, camera, scene, assets, width, height) takes one event and returns a list of DragEvent records. Each record has a kind and a node. The kinds are the events three.js dispatches, in the same order.
| Kind | three.js | When |
|---|---|---|
HOVER_ON |
hoveron |
The pointer comes over an object. |
HOVER_OFF |
hoveroff |
The pointer leaves an object. |
DRAG_START |
dragstart |
A press picks an object up. |
DRAG_MOVE |
drag |
A move drags the object. |
DRAG_END |
dragend |
A release lets the object go. |
| Input | Change |
|---|---|
| Left or middle drag | Translate. The point under the pointer stays under it. |
| Right drag | Rotate about the camera's up and right. |
| Member | Default | Meaning |
|---|---|---|
objects |
The nodes that can be dragged. | |
enabled |
True |
False ignores all input. |
recursive |
True |
A hit on a node below one of objects counts too. |
transform_group |
False |
Drag the outermost of objects above the node hit. |
rotate_speed |
1 | Radians a turn, per unit of pointer movement in normalized device coordinates. |
raycaster |
Its layers select the nodes that can be picked. |
|
primary_action, middle_action, secondary_action
|
DRAG_TRANSLATE, DRAG_TRANSLATE, DRAG_ROTATE
|
three.js: mouseButtons. NO_DRAG picks and moves nothing. |
A translation keeps the object on the plane through its origin that faces the camera. The object's new position is in its parent's frame, as in three.js. A rotation turns the object about the camera's up and right, in the frame of the object's parent.
handle updates the scene first, so the next event sees the object where the drag put it. selected() and hovered() return the node dragged and the node under the pointer.
three.js drags the outermost Group when transformGroup is set. This port drags the outermost node of objects above the node struck instead. It does not look for a node that group() built.
Everything the Raycaster tests can be picked, lines, points and sprites included. The raycaster's line and point thresholds apply, as in three.js. See Raycasting. Touch input and the cursor style are not ported.
controls/transform_controls.mojo. A gizmo moves, turns or scales one node. attach(node) puts the gizmo on a node, and detach() takes it off. mode sets what a drag does.
| Mode | Handles |
|---|---|
TRANSLATE_MODE |
Arrows along x, y and z, squares for the planes XY, YZ and XZ, and a center XYZ. |
ROTATE_MODE |
Rings about x, y and z, a ring E that faces the eye, and a ball XYZE. |
SCALE_MODE |
Axes with boxes at the ends, the three plane squares, and a center box XYZ. |
handle(event, camera, scene, width, height) takes one event and returns a list of TransformEvent records. A move over a handle sets axis. A left press on a handle starts a drag. The moves then change the node, and a release ends the drag.
| Kind | three.js | When |
|---|---|---|
AXIS_CHANGED |
axis-changed |
The handle under the pointer changes. |
DRAGGING_CHANGED |
dragging-changed |
A drag starts or ends. |
MOUSE_DOWN |
mouseDown |
A press on a handle starts a drag. |
OBJECT_CHANGE |
objectChange |
A move changes the node. |
MOUSE_UP |
mouseUp |
A release ends a drag. |
Each record also holds the mode, the axis and dragging after the event. Stop an OrbitControls while dragging is True, as the three.js example does.
| Member | Default | Meaning |
|---|---|---|
mode |
TRANSLATE_MODE |
What a drag does. |
space |
WORLD_SPACE |
LOCAL_SPACE turns the handles to the node's own axes. |
axis |
NO_HANDLE |
The handle under the pointer, or dragged. |
translation_snap |
none | A Length. A drag moves by whole steps of it. |
rotation_snap |
none | An Angle. A drag turns by whole steps of it. |
scale_snap |
none | A drag scales to whole steps of it. |
size |
1 | A factor on the gizmo's size. |
show_x, show_y, show_z
|
True |
False hides the handles along that axis. |
enabled |
True |
False ignores all input. |
min_x to max_z
|
minus and plus infinity | A translation keeps the node's position between these, as a Length. |
x_color, y_color, z_color, active_color
|
red, green, blue, yellow | The colors of the handles. set_colors(x, y, z, active) sets all four. |
check() refuses an invalid mode, space or axis, and an axis that the mode has no handle for. It also refuses a size or a snap that is not positive. handle and gizmo call it first. reset(scene) puts the node back where the drag started.
The control uses Matrix4.decompose to read transforms. Finite tiny and large scales keep their proportions. Zero axes, nonfinite entries, and scales too large for Float32 raise.
The handles are picked with three.js's invisible picker shapes. These are cones, flat boxes, an octahedron, a box, tori and a sphere. Each handle is at the node's world position and turned to the node's axes in LOCAL_SPACE. Its size keeps it the same size on the screen. The picked handle is the one that the ray meets nearest.
A handle that points at the eye is hidden, and so is a plane that the eye sees edge-on. A hidden handle cannot be picked. This follows three.js's thresholds of 0.99 and 0.2.
A drag follows the pointer on a plane through the node. An axis drags on the plane that holds the axis and faces the eye best. A plane handle drags on its own plane. XYZ, E, XYZE and every ring drag on the plane that faces the camera.
The arithmetic of each mode, snapping included, is three.js's. SCALE_MODE always works in the node's own axes. XYZ, E and XYZE always work in the world's axes.
gizmo(camera, scene) returns the handles of the current mode as line segments in world space. Each point has a color: red for x, green for y and blue for z. The handle under the pointer is yellow. Draw the geometry with a Line in SEGMENTS mode, on a node at the origin, with helper_material. Call gizmo again after the node or the camera moves.
helper(camera, scene) returns the white helper lines of three.js, in world space. During a translation or a scale, each axis of the handle has a long line through the start point. A translation also has a line from the start point to the node. In ROTATE_MODE, the axis of the picked ring has a long line, unless it points at the eye. During an XYZE turn, the line follows the axis of the turn.
three.js draws solid arrows and boxes with materials that ignore depth. Here each shape is drawn by its edges, and the depth test applies. The rings of ROTATE_MODE are drawn as half circles that face the eye, as in three.js.
These parts are not ported:
- Touch input.
- The
changeevent, which three.js sends to ask for a render.
A scale whose start point has no length along an axis keeps that axis's scale. three.js divides by zero there. The E picker is a flat ring, the shape of three.js's torus with two segments around its tube. The pickers are tested from both sides, which gives the same hits for closed shapes. The plane and the handles are found again at each event. In three.js a render between two events finds them.
controls/arcball_controls.mojo. The camera turns on a virtual trackball about a center. A drag that comes back to its start puts the camera back too. ArcballControls(camera, target) turns the camera to target and puts the trackball there.
handle(event, camera, scene, assets, width, height) applies one event to the camera at once, as three.js does. A double click picks in scene. update(camera, delta) moves the clock and runs the animations. Then it does what three.js's update does. Call it once a frame. It returns True when three.js would send change.
| Input | Change |
|---|---|
| Left drag | Turn about the center. |
| Right drag, or Ctrl and a left drag | Pan. The center moves with the camera. |
| Middle drag, or the wheel | Zoom toward the center. |
| Shift and a middle drag, or Shift and the wheel | Change the field of view, and keep the center the same size. |
| Double click on a mesh | Move the center to the point, and zoom in by scale_factor. |
set_mouse_action(operation, mouse, key) changes the table. The operations are PAN_OPERATION, ROTATE_OPERATION, ZOOM_OPERATION and FOV_OPERATION. The inputs are MOUSE_PRIMARY, MOUSE_MIDDLE, MOUSE_SECONDARY and MOUSE_WHEEL. The keys are NO_MODIFIER, CTRL_MODIFIER and SHIFT_MODIFIER. The wheel can only zoom or change the field of view. unset_mouse_action(mouse, key) removes an action, and operation_of(mouse, key) reads one.
| Member | Default | Meaning |
|---|---|---|
target |
the origin | Set it and call update to move the trackball there. A pan does not change it. |
radius_factor |
0.67 | The trackball's size, as a part of the smaller side of the view. Set it with set_tb_radius. |
scale_factor |
1.1 | The zoom of one wheel notch, and of a focus. |
rotate_speed |
1 | A factor on the turn. |
enable_animations |
True |
A turn let go goes on, and a focus takes time. |
damping_factor |
25 rad/s² | How fast a turn let go slows down, as an AngularAcceleration. |
w_max |
20 rad/s | The fastest a turn let go can start, as an AngularVelocity. |
focus_animation_time |
500 ms | How long a focus takes, as a Duration. |
cursor_zoom |
False |
Zoom toward the pointer. It needs enable_pan. |
enable_pan, enable_rotate, enable_zoom, enable_focus
|
True |
Turn an operation off. |
enable_grid |
False |
Show a grid during a pan. |
enable_gizmos |
True |
Show the gizmo. |
adjust_near_far |
False |
Move the near and far planes with a zoom. |
min_distance, max_distance
|
0, infinity | For a perspective camera, as a Length. |
min_zoom, max_zoom
|
0, infinity | For an orthographic camera. |
min_fov, max_fov
|
5°, 90° | As an Angle. |
raycaster |
Its layers select what a double click can pick. |
The pointer is read on a sphere about the center. Farther out, it is read on a hyperboloid, which joins the sphere smoothly. The turn is about the axis at right angles to the start and the point now. Its angle is the larger of the angle between them and their distance over the radius. A turn let go less than 120 ms after the last move goes on at the mean speed of the last two moves. It then slows by damping_factor.
A focus moves the center to the point that a double click hits on a mesh. A line, points and a sprite have no face, so a double click passes over them, as in three.js. Two clicks count when each is shorter than 250 ms, and when they are less than 300 ms and 24 pixels apart.
copy_state(camera) returns an ArcballSnapshot. It holds the fields that three.js writes to the clipboard as JSON. These are the camera's matrix, up, near, far, zoom and field of view, the gizmo's matrix, and target. paste_state(camera, snapshot) puts them back. It refuses a snapshot that no camera can take:
- A field of view outside a half turn.
- A near plane behind the camera.
- A far plane that is not beyond the near plane.
- A zoom that is not positive.
save_state(camera) records the camera and the trackball. reset(camera) puts them back. Before a save_state, reset goes to where the controls began.
gizmo() returns the trackball as three circles in world space: red about x, green about y and blue about z. Draw them with a Line in SEGMENTS mode on a node at the origin. Use helper_material(controls.gizmo_opacity(), transparent=True). The opacity is 1 during a turn and 0.6 at other times. set_gizmos_visible(False) hides the gizmo.
grid() returns the grid that three.js shows during a pan with enable_grid. It faces the camera through the center where the pan began. It is three times the view, with 60 cells each way. Both return an empty geometry when there is nothing to draw. Call them again each frame.
The camera is read at each call and written back. Its up is always its own y axis. After a turn, three.js sets up to the first up turned by the camera. That is the same for a first up of +y. For another first up, three.js's up can point along the view.
The controls read time from update. An event happens at the time of the last update. A pointer is read at the center of its pixel. A camera that rides a scene node is refused, as three.js requires.
enable_gizmos hides the gizmo, as three.js documents. three.js has the member but does not read it. three.js works out the grid's cells in floating point, and here they are 60.
Touch input is not ported. So there is no turn about the view axis, which only touch starts in three.js. The start and end events are not ported either.
controls/selection_box.mojo. A SelectionBox finds the objects inside a rectangle on the view. This is three.js's SelectionBox from examples/jsm/interactive.
var box = SelectionBox()
# For each pointer event of the window:
var picked = box.handle(event, camera, scene, assets, width, height)
if picked:
print(len(picked.value().meshes))A left press starts a rectangle. Each move and the release return a Selection of what is inside it. Other events return none. select(camera, scene, assets, start, end) selects through two corners in normalized device coordinates.
An object is inside when the center of its geometry's bounding sphere is inside the frustum of the rectangle. This is the test of three.js. An instanced mesh is tested one instance at a time, at the position of each instance.
A Selection has one list for each kind of object: meshes, skinned_meshes, lines and points. Each list holds indices into the scene's list of that kind, in the order of the scene's nodes. instanced_meshes holds each instanced mesh, and instances holds the selected instances of each one.
- three.js returns one list of objects. A scene here keeps one list for each kind, so a selection does too.
- A batched mesh is not selected. three.js tests the center of its shared geometry, which this port does not keep.
-
deepis none by default, and then there is no far plane. three.js's default ofNumber.MAX_VALUEhas the same effect. - three.js turns the far plane's normal but keeps its constant. This port does the same, so a
deepgives the same selection. - A rectangle with no width or height gets a small width. three.js adds
Number.EPSILON, which aFloat32loses. This port adds oneFloat32step.
The tests hold each control to three.js r180's own numbers, DragControls, TransformControls, ArcballControls and SelectionBox included. The three.js controls ran headless in Node, with the same camera, view and input as each test.
The window asks the terminal its size with xterm's ESC [ 18 t, which works on every platform, where ioctl does not. A terminal that does not answer sends no RESIZE. Give such a terminal a size that fits: a terminal smaller than the frame wraps or cuts it. examples/viewer.mojo asks once a second and follows the answer.
The window needs a terminal that speaks xterm's sequences. The current terminals on Linux and macOS do, and so does Windows Terminal under WSL 2.
A frame is drawn in full every time. The example's frame of 96 by 64 pixels is about 20 kilobytes. A frame whose every pixel differs from its neighbor is about 120 kilobytes.
Tutorials
How-to
Reference
- Scene graph
- Rotations
- Cameras
- Geometry
- Geometry addons
- Lofts and clipping groups
- Sculptor
- Procedural generators
- Meshes and assets
- Lines
- Points and sprites
- Helpers
- Scene objects
- Animated objects
- Raycasting
- Curves and paths
- Animation
- Skinning
- Materials
- Node materials
- TSL functions
- Lights
- Lighting addons
- Fog
- Textures
- Renderer
- Renderer hooks and material flags
- Rasterization
- GPU backend
- Render target
- Post-processing
- GPU computation
- Compute nodes
- Progressive light map
- Gaussian splats
- Voxel global illumination
- SVG renderer
- Image files
- Image decode queue
- Model files
- More model files
- Exporters
- Scene JSON
- Windowing and controls
- Math
- Norm consumers
- Math addons
- Units
- Coverage tool
- Commands
- Examples
- Benchmarks
- Extensions
- Shared physics
- Physics query snapshots
- Continuous collision
- Tick force restoration
- Water
- Animals
- Buildings
- CARLA
- Femur
- Tibia
- Fibula
- Patella
- Knee
- Muscles
- Vessels
- Lymph
- Nerves
- Integument
- Leg
- Foot
- Pelvis
- Torso
- Arm
- Hand
- Head
- Genome
- Game humanoid
- Humanoid fidelity
- Audio-aligned game faces
- Converted assets
- Mesh quality
- Segment inertia
Explanation
- Software rasterizer
- Scene graph as an array
- Linear color
- Normals are not colors
- Perspective correction
- Fixed-point coverage
- Per-fragment shading
- Mipmaps
- Sorted transparency
- Types and checks
- Render hooks as a trait
- Shared CPU and GPU code
- Bands and threads
- PNG structure
- Extensions beside the port
- CUDA teardown hang
- Mojo compiler hang