diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 5a386d5d0e8..991be84f8a0 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -65,6 +65,7 @@ end markersize=size, axis = (; scenekw = (;limits=Rect3f(Point3(0), Point3(1)))) ) + update_cam!(ax.scene, Point3f(2.224431, 2.224431, 2.128731), Point3f(0.5957, 0.5957, 0.50000006)) Record(fig, [10, 5, 100, 60, 177]) do i makenew[] = i end @@ -82,6 +83,7 @@ end end end fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0) + update_cam!(ax.scene, Point3f(45.383663, 45.38298, 43.136826), Point3f(12.246061, 12.245379, 9.999225)) screen = display(GLMakie.Screen(;renderloop=(screen) -> nothing, start_renderloop=false), fig.scene) buff = RNG.rand(Point3f, 10^4) .* 20f0; update_loop(meshplot, buff, screen) diff --git a/NEWS.md b/NEWS.md index a78869301dc..098ffb4a05f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ ## master +- Improved 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) - Fixed incorrect placement of contourlabels with transform functions [#3083](https://github.com/MakieOrg/Makie.jl/pull/3083) - Fix automatic normal generation for meshes with shading and no normals [#3041](https://github.com/MakieOrg/Makie.jl/pull/3041). @@ -31,6 +32,7 @@ - Adjusted scaling of scatter/text stroke, glow and anti-aliasing width under non-uniform 2D scaling (Vec2f markersize/fontsize) in GLMakie [#2950](https://github.com/MakieOrg/Makie.jl/pull/2950). - Scaled `errorbar` whiskers and `bracket` correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). - Updated `bracket` when the screen is resized or transformations change [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). +- Added auto-resizing functionality to WGLMakie plot figures [#3042](https://github.com/MakieOrg/Makie.jl/pull/3042) ## v0.19.6 diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index e32e9980322..259d18442e8 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -29,19 +29,11 @@ end meshes = map(colormesh, rectangles) fig, ax, meshplot = mesh(merge(meshes)) scene = ax.scene - center!(scene) cam = cameracontrols(scene) - dir = widths(data_limits(scene)) ./ 2. - dir_scaled = Vec3f( - dir[1] * scene.transformation.scale[][1], - 0.0, - dir[3] * scene.transformation.scale[][2], - ) + cam.settings[:projectiontype][] = Makie.Orthographic cam.upvector[] = (0.0, 0.0, 1.0) - cam.lookat[] = minimum(data_limits(scene)) + dir_scaled - cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 6.3, cam.lookat[][3]) - cam.attributes[:projectiontype][] = Makie.Orthographic - cam.zoom_mult[] = 0.61f0 + cam.lookat[] = Vec3f(0.595, 2.5, 0.5) + cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 0.61, cam.lookat[][3]) update_cam!(scene, cam) fig end @@ -591,7 +583,7 @@ end end end cam = cameracontrols(ax.scene) - cam.attributes.fov[] = 22f0 + cam.fov[] = 22f0 update_cam!(ax.scene, cam, Vec3f(0.625, 0, 3.5), Vec3f(0.625, 0, 0), Vec3f(0, 1, 0)) fig end diff --git a/docs/documentation/cameras.md b/docs/documentation/cameras.md index f935c92c29c..ea7a3e8238f 100644 --- a/docs/documentation/cameras.md +++ b/docs/documentation/cameras.md @@ -1,13 +1,15 @@ # Cameras -A `Camera` is simply a viewport through which the Scene is visualized. `Makie` offers 2D and 3D projections, and 2D plots can be projected in 3D! +A `Camera` is simply a viewport through which the Scene is visualized. `Makie` offers 2D and 3D projections, and 2D plots can be projected in 3D! -To specify the camera you want to use for your Scene, you can set the `camera` attribute. Currently, we offer four types of camera: +To specify the camera you want to use for your Scene, you can set the `camera` attribute. Currently, we offer the following cameras/constructors \apilink{campixel!} +\apilink{cam_relative!} \apilink{cam2d!} -`cam3d!` -`cam3d_cad!` +\apilink{Camera3D} +\apilink{cam3d!} +\apilink{cam3d_cad!} which will mutate the camera of the Scene into the specified type. @@ -15,6 +17,10 @@ which will mutate the camera of the Scene into the specified type. The pixel camera (\apilink{campixel!(scene)}) projects the scene in pixel space, i.e. each integer step in the displayed data will correspond to one pixel. There are no controls for this camera. The clipping limits are set to `(-10_000, 10_000)`. +## Relative Camera + +The relative camera (\apilink{cam_relative!(scene)}) projects the scene into a 0..1 by 0..1 space. There are no controls for this camera. The clipping limits are set to `(-10_000, 10_000)`. + ## 2D Camera The 2D camera (\apilink{cam2d!(scene)}) uses an orthographic projection with a fixed rotation and aspect ratio. You can set the following attributes via keyword arguments in `cam2d!` or by accessing the camera struct `cam = cameracontrols(scene)`: @@ -30,6 +36,61 @@ Note that this camera is not used by `Axis`. It is used, by default, for 2D `LSc {{doc Camera3D}} +`cam3d!` and `cam3d_cad!` but create a `Camera3D` with some specific options. + +## Example - Visualizing the cameras view box + +```julia +using GeometryBasics, LinearAlgebra + +function frustum_snapshot(cam) + r = Rect3f(Point3f(-1, -1, -1), Vec3f(2, 2, 2)) + rect_ps = coordinates(r) .|> Point3f + insert!(rect_ps, 13, Point3f(1, -1, 1)) # fix bad line + + inv_pv = inv(cam.projectionview[]) + return map(rect_ps) do p + p = inv_pv * to_ndim(Point4f, p, 1) + return p[Vec(1,2,3)] / p[4] + end +end + + +ex = Point3f(1,0,0) +ey = Point3f(0,1,0) +ez = Point3f(0,0,1) + +fig = Figure() +scene = LScene(fig[1, 1]) +cc = Makie.Camera3D(scene.scene, projectiontype = Makie.Perspective, far = 3.0) + +linesegments!(scene, Rect3f(Point3f(-1), Vec3f(2)), color = :black) +linesegments!(scene, + [-ex, ex, -ey, ey, -ez, ez], + color = [:red, :red, :green, :green, :blue, :blue] +) +center!(scene.scene) + +cam = scene.scene.camera +eyeposition = cc.eyeposition +lookat = cc.lookat +frustum = map(pv -> frustum_snapshot(cam), cam.projectionview) + +scene = LScene(fig[1, 2]) +_cc = Makie.Camera3D(scene.scene, projectiontype = Makie.Orthographic) +lines!(scene, frustum, color = :blue, linestyle = :dot) +scatter!(scene, eyeposition, color = :black) +scatter!(scene, lookat, color = :black) + +linesegments!(scene, + [-ex, ex, -ey, ey, -ez, ez], + color = [:red, :red, :green, :green, :blue, :blue] +) +linesegments!(scene, Rect3f(Point3f(-1), Vec3f(2)), color = :black) + +fig +``` + ## General Remarks To force a plot to be visualized in 3D, you can set the limits to have a nonzero \(z\)-axis interval, or ensure that a 3D camera type is used. diff --git a/docs/documentation/events.md b/docs/documentation/events.md index d477c32bd88..99ff8e4fc22 100644 --- a/docs/documentation/events.md +++ b/docs/documentation/events.md @@ -376,6 +376,8 @@ Furthermore you can wrap any of the above in `Exclusively` to discard matches wh - `hotkey = Keyboard.left_control & Keyboard.a` is equivalent to `(Keyboard.left_control, Keyboard.a)` - `hotkey = (Keyboard.left_control | Keyboard.right_control) & Keyboard.a` allows either left or right control with a. +Note that the way we used `ispressed` above, the condition will be true for "press" and "repeat" events. You can further restrict to one or the other by checking `event.action`. If you wish to react to a "release" event, you will need to pass `event.key`/`event.button` as a third argument to `ispressed(fig, hotkey, event.key)`. This will tell `ispressed` to assume the key or button is pressed if it is part of the hotkey. + ## Interactive Widgets Makie has a couple of useful interactive widgets like sliders, buttons and menus, which you can learn about in the \myreflink{Blocks} section. diff --git a/src/camera/camera2d.jl b/src/camera/camera2d.jl index ff276348d94..c5d0110744a 100644 --- a/src/camera/camera2d.jl +++ b/src/camera/camera2d.jl @@ -1,8 +1,8 @@ struct Camera2D <: AbstractCamera area::Observable{Rect2f} zoomspeed::Observable{Float32} - zoombutton::Observable{ButtonTypes} - panbutton::Observable{Union{ButtonTypes, Vector{ButtonTypes}}} + zoombutton::Observable{IsPressedInputType} + panbutton::Observable{IsPressedInputType} padding::Observable{Float32} last_area::Observable{Vec{2, Int}} update_limits::Observable{Bool} @@ -11,14 +11,23 @@ end """ cam2d!(scene::SceneLike, kwargs...) -Creates a 2D camera for the given Scene. +Creates a 2D camera for the given `scene`. The camera implements zooming by +scrolling and translation using mouse drag. It also implements rectangle +selections. + +## Keyword Arguments + +- `zoomspeed = 0.1f0` sets the zoom speed. +- `zoombutton = true` sets a button (combination) which needs to be pressed to enable zooming. By default no button needs to be pressed. +- `panbutton = Mouse.right` sets the button used to translate the camera. This must include a mouse button. +- `selectionbutton = (Keyboard.space, Mouse.left)` sets the button used for rectangle selection. This must include a mouse button. """ function cam2d!(scene::SceneLike; kw_args...) cam_attributes = merged_get!(:cam2d, scene, Attributes(kw_args)) do Attributes( area = Observable(Rectf(0, 0, 1, 1)), zoomspeed = 0.10f0, - zoombutton = nothing, + zoombutton = true, panbutton = Mouse.right, selectionbutton = (Keyboard.space, Mouse.left), padding = 0.001, @@ -318,7 +327,9 @@ end """ campixel!(scene; nearclip=-1000f0, farclip=1000f0) -Creates a pixel-level camera for the `Scene`. No controls! +Creates a pixel camera for the given `scene`. This means that the positional +data of a plot will be interpreted in pixel units. This camera does not feature +controls. """ function campixel!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) disconnect!(camera(scene)) @@ -338,7 +349,8 @@ struct RelativeCamera <: AbstractCamera end """ cam_relative!(scene) -Creates a pixel-level camera for the `Scene`. No controls! +Creates a camera for the given `scene` which maps the scene area to a 0..1 by +0..1 range. This camera does not feature controls. """ function cam_relative!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) projection = orthographicprojection(0f0, 1f0, 0f0, 1f0, nearclip, farclip) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 2b695aa0043..f715ac9127b 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -1,36 +1,57 @@ -struct Camera3D <: AbstractCamera +abstract type AbstractCamera3D <: AbstractCamera end + +struct Camera3D <: AbstractCamera3D + # User settings + settings::Attributes + controls::Attributes + + # Interactivity + pulser::Observable{Float64} + selected::Observable{Bool} + + # view matrix eyeposition::Observable{Vec3f} lookat::Observable{Vec3f} upvector::Observable{Vec3f} - - zoom_mult::Observable{Float32} - fov::Observable{Float32} # WGLMakie compat + + # perspective projection matrix + fov::Observable{Float32} near::Observable{Float32} far::Observable{Float32} - pulser::Observable{Float64} - - attributes::Attributes end """ - Camera3D(scene[; attributes...]) + Camera3D(scene[; kwargs...]) -Creates a 3d camera with a lot of controls. +Sets up a 3D camera with mouse and keyboard controls. -The 3D camera is (or can be) unrestricted in terms of rotations and translations. Both `cam3d!(scene)` and `cam3d_cad!(scene)` create this camera type. Unlike the 2D camera, settings and controls are stored in the `cam.attributes` field rather than in the struct directly, but can still be passed as keyword arguments. The general camera settings include +The behavior of the camera can be adjusted via keyword arguments or the fields +`settings` and `controls`. + +## Settings + +Settings include anything that isn't a mouse or keyboard button. -- `fov = 45f0` sets the "neutral" field of view, i.e. the fov corresponding to no zoom. This is irrelevant if the camera uses an orthographic projection. -- `near = automatic` sets the value of the near clip. By default this will be chosen based on the scenes bounding box. The final value is in `cam.near`. -- `far = automatic` sets the value of the far clip. By default this will be chosen based on the scenes bounding box. The final value is in `cam.far`. -- `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. - `projectiontype = Perspective` sets the type of the projection. Can be `Orthographic` or `Perspective`. -- `fixed_axis = false`: If true panning uses the (world/plot) z-axis instead of the camera up direction. -- `zoom_shift_lookat = true`: If true attempts to keep data under the cursor in view when zooming. +- `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. +- `fixed_axis = true`: If true panning uses the (world/plot) z-axis instead of the camera up direction. +- `zoom_shift_lookat = true`: If true keeps the data under the cursor when zooming. - `cad = false`: If true rotates the view around `lookat` when zooming off-center. -The camera view follows from the position of the camera `eyeposition`, the point which the camera focuses `lookat` and the up direction of the camera `upvector`. These can be accessed as `cam.eyeposition` etc and adjusted via `update_cam!(scene, cameracontrols(scene), eyeposition, lookat[, upvector = Vec3f(0, 0, 1)])`. They can also be passed as keyword arguments when the camera is constructed. +- `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. +- `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. +- `keyboard_zoomspeed = 1f0` sets the speed of keyboard based zooms. + +- `mouse_rotationspeed = 1f0` sets the speed of mouse rotations. +- `mouse_translationspeed = 0.5f0` sets the speed of mouse translations. +- `mouse_zoomspeed = 1f0` sets the speed of mouse zooming (mousewheel). + +- `update_rate = 1/30` sets the rate at which keyboard based camera updates are evaluated. +- `circular_rotation = (true, true, true)` enables circular rotations for (fixed x, fixed y, fixed z) rotation axis. (This means drawing a circle with your mouse around the center of the scene will result in a continuous rotation.) -The camera can be controlled by keyboard and mouse. The keyboard has the following available attributes +## Controls + +Controls include any kind of hotkey setting. - `up_key = Keyboard.r` sets the key for translations towards the top of the screen. - `down_key = Keyboard.f` sets the key for translations towards the bottom of the screen. @@ -39,10 +60,10 @@ The camera can be controlled by keyboard and mouse. The keyboard has the followi - `forward_key = Keyboard.w` sets the key for translations into the screen. - `backward_key = Keyboard.s` sets the key for translations out of the screen. -- `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (enlarge, via fov). -- `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (shrink, via fov). -- `stretch_view_key = Keyboard.page_up` sets the key for moving `eyepostion` away from `lookat`. -- `contract_view_key = Keyboard.page_down` sets the key for moving `eyeposition` towards `lookat`. +- `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (translate eyeposition towards lookat). +- `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (translate eyeposition away from lookat). +- `increase_fov_key = Keyboard.b` sets the key for increasing the fov. +- `decrease_fov_key = Keyboard.n` sets the key for decreasing the fov. - `pan_left_key = Keyboard.j` sets the key for rotations around the screens vertical axis. - `pan_right_key = Keyboard.l` sets the key for rotations around the screens vertical axis. @@ -51,101 +72,112 @@ The camera can be controlled by keyboard and mouse. The keyboard has the followi - `roll_clockwise_key = Keyboard.e` sets the key for rotations of the screen. - `roll_counterclockwise_key = Keyboard.q` sets the key for rotations of the screen. -- `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. -- `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. -- `keyboard_zoomspeed = 1f0` sets the speed of keyboard based zooms. -- `update_rate = 1/30` sets the rate at which keyboard based camera updates are evaluated. - -and mouse interactions are controlled by +- `fix_x_key = Keyboard.x` sets the key for fixing translations and rotations to the (world/plot) x-axis. +- `fix_y_key = Keyboard.y` sets the key for fixing translations and rotations to the (world/plot) y-axis. +- `fix_z_key = Keyboard.z` sets the key for fixing translations and rotations to the (world/plot) z-axis. +- `reset = Keyboard.left_control & Mouse.left` sets the key for resetting the camera. This equivalent to calling `center!(scene)`. +- `reposition_button = Keyboard.left_alt & Mouse.left` sets the key for focusing the camera on a plot object. - `translation_button = Mouse.right` sets the mouse button for drag-translations. (up/down/left/right) - `scroll_mod = true` sets an additional modifier button for scroll-based zoom. (true being neutral) - `rotation_button = Mouse.left` sets the mouse button for drag-rotations. (pan, tilt) -- `mouse_rotationspeed = 1f0` sets the speed of mouse rotations. -- `mouse_translationspeed = 0.5f0` sets the speed of mouse translations. -- `mouse_zoomspeed = 1f0` sets the speed of mouse zooming (mousewheel). -- `circular_rotation = (true, true, true)` enables circular rotations for (fixed x, fixed y, fixed z) rotation axis. (This means drawing a circle with your mouse around the center of the scene will result in a continuous rotation.) +## Other kwargs -There are also a few generally applicable controls: +Some keyword arguments are used to initialize fields. These include -- `fix_x_key = Keyboard.x` sets the key for fixing translations and rotations to the (world/plot) x-axis. -- `fix_y_key = Keyboard.y` sets the key for fixing translations and rotations to the (world/plot) y-axis. -- `fix_z_key = Keyboard.z` sets the key for fixing translations and rotations to the (world/plot) z-axis. -- `reset = Keyboard.home` sets the key for fully resetting the camera. This equivalent to setting `lookat = Vec3f(0)`, `upvector = Vec3f(0, 0, 1)`, `eyeposition = Vec3f(3)` and then calling `center!(scene)`. +- `eyeposition = Vec3f(3)`: The position of the camera. +- `lookat = Vec3f(0)`: The point the camera is focused on. +- `upvector = Vec3f(0, 0, 1)`: The world direction corresponding to the up direction of the screen. + +- `fov = 45.0` is the field of view. This is irrelevant if the camera uses an orthographic projection. +- `near = 0.1` sets the position of the near clip plane relative to `eyeposition - lookat`. Must be greater 0. Anything between the camera and the near clip plane is hidden. +- `far = 10.0` sets the position of the far clip plane relative to `eyeposition - lookat`. Anything further away than the far clip plane is hidden. -You can also make adjustments to the camera position, rotation and zoom by calling relevant functions: +Note that updating these observables in an active camera requires a call to `update_cam(scene)` +for them to be applied. For updating `eyeposition`, `lookat` and/or upvector +`update_cam!(scene, eyeposition, lookat, upvector = Vec3f(0,0,1))` is preferred. + +The camera position and orientation can also be adjusted via the functions - `translate_cam!(scene, v)` will translate the camera by the given world/plot space vector `v`. - `rotate_cam!(scene, angles)` will rotate the camera around its axes with the corresponding angles. The first angle will rotate around the cameras "right" that is the screens horizontal axis, the second around the up vector/vertical axis or `Vec3f(0, 0, +-1)` if `fixed_axis = true`, and the third will rotate around the view direction i.e. the axis out of the screen. The rotation respects the current `rotation_center` of the camera. - `zoom!(scene, zoom_step)` will change the zoom level of the scene without translating or rotating the scene. `zoom_step` applies multiplicatively to `cam.zoom_mult` which is used as a multiplier to the fov (perspective projection) or width and height (orthographic projection). """ function Camera3D(scene::Scene; kwargs...) - attr = merged_get!(:cam3d, scene, Attributes(kwargs)) do - Attributes( - # Keyboard controls - # Translations - up_key = Keyboard.r, - down_key = Keyboard.f, - left_key = Keyboard.a, - right_key = Keyboard.d, - forward_key = Keyboard.w, - backward_key = Keyboard.s, - # Zooms - zoom_in_key = Keyboard.u, - zoom_out_key = Keyboard.o, - stretch_view_key = Keyboard.page_up, - contract_view_key = Keyboard.page_down, - # Rotations - pan_left_key = Keyboard.j, - pan_right_key = Keyboard.l, - tilt_up_key = Keyboard.i, - tilt_down_key = Keyboard.k, - roll_clockwise_key = Keyboard.e, - roll_counterclockwise_key = Keyboard.q, - # Mouse controls - translation_button = Mouse.right, - scroll_mod = true, - rotation_button = Mouse.left, - # Shared controls - fix_x_key = Keyboard.x, - fix_y_key = Keyboard.y, - fix_z_key = Keyboard.z, - reset = Keyboard.home, - # Settings - keyboard_rotationspeed = 1f0, - keyboard_translationspeed = 0.5f0, - keyboard_zoomspeed = 1f0, - mouse_rotationspeed = 1f0, - mouse_translationspeed = 1f0, - mouse_zoomspeed = 1f0, - circular_rotation = (true, true, true), - fov = 45f0, # base fov - near = automatic, - far = automatic, - rotation_center = :lookat, - update_rate = 1/30, - projectiontype = Perspective, - fixed_axis = true, - zoom_shift_lookat = false, # doesn't really work with fov - cad = false, - # internal - selected = true - ) - end + overwrites = Attributes(kwargs) + + controls = Attributes( + # Keyboard controls + # Translations + up_key = Keyboard.r, + down_key = Keyboard.f, + left_key = Keyboard.a, + right_key = Keyboard.d, + forward_key = Keyboard.w, + backward_key = Keyboard.s, + # Zooms + zoom_in_key = Keyboard.u, + zoom_out_key = Keyboard.o, + increase_fov_key = Keyboard.b, + decrease_fov_key = Keyboard.n, + # Rotations + pan_left_key = Keyboard.j, + pan_right_key = Keyboard.l, + tilt_up_key = Keyboard.i, + tilt_down_key = Keyboard.k, + roll_clockwise_key = Keyboard.e, + roll_counterclockwise_key = Keyboard.q, + # Mouse controls + translation_button = Mouse.right, + rotation_button = Mouse.left, + scroll_mod = true, + reposition_button = Keyboard.left_alt & Mouse.left, + # Shared controls + fix_x_key = Keyboard.x, + fix_y_key = Keyboard.y, + fix_z_key = Keyboard.z, + reset = Keyboard.left_control & Mouse.left + ) + + replace!(controls, :Camera3D, scene, overwrites) + + settings = Attributes( + keyboard_rotationspeed = 1f0, + keyboard_translationspeed = 0.5f0, + keyboard_zoomspeed = 1f0, + + mouse_rotationspeed = 1f0, + mouse_translationspeed = 1f0, + mouse_zoomspeed = 1f0, + + projectiontype = Makie.Perspective, + circular_rotation = (true, true, true), + rotation_center = :lookat, + update_rate = 1/30, + zoom_shift_lookat = true, + fixed_axis = true, + cad = false + ) + + replace!(settings, :Camera3D, scene, overwrites) cam = Camera3D( - pop!(attr, :eyeposition, Vec3f(3)), - pop!(attr, :lookat, Vec3f(0)), - pop!(attr, :upvector, Vec3f(0, 0, 1)), - - Observable(1f0), - Observable(attr[:fov][]), - Observable(attr[:near][] === automatic ? 0.1f0 : attr[:near][]), - Observable(attr[:far][] === automatic ? 100f0 : attr[:far][]), + settings, controls, + + # Internals - controls Observable(-1.0), + Observable(true), - attr + # Semi-Internal - view matrix + get(overwrites, :eyeposition, Observable(Vec3f(3, 3, 3))), + get(overwrites, :lookat, Observable(Vec3f(0, 0, 0))), + get(overwrites, :upvector, Observable(Vec3f(0, 0, 1))), + + # Semi-Internal - projection matrix + get(overwrites, :fov, Observable(45.0)), + get(overwrites, :near, Observable(0.01)), + get(overwrites, :far, Observable(100.0)), ) disconnect!(camera(scene)) @@ -154,9 +186,9 @@ function Camera3D(scene::Scene; kwargs...) # ticks every so often to get consistent position updates. on(cam.pulser) do prev_time current_time = time() - active = on_pulse(scene, cam, Float32(current_time - prev_time)) - @async if active && attr.selected[] - sleep(attr.update_rate[]) + active = on_pulse(scene, cam, current_time - prev_time) + @async if active && cam.selected[] + sleep(settings.update_rate[]) cam.pulser[] = current_time else cam.pulser.val = -1.0 @@ -165,15 +197,15 @@ function Camera3D(scene::Scene; kwargs...) keynames = ( :up_key, :down_key, :left_key, :right_key, :forward_key, :backward_key, - :zoom_in_key, :zoom_out_key, :stretch_view_key, :contract_view_key, + :zoom_in_key, :zoom_out_key, :increase_fov_key, :decrease_fov_key, :pan_left_key, :pan_right_key, :tilt_up_key, :tilt_down_key, :roll_clockwise_key, :roll_counterclockwise_key ) - + # Start ticking if relevant keys are pressed on(camera(scene), events(scene).keyboardbutton) do event if event.action in (Keyboard.press, Keyboard.repeat) && cam.pulser[] == -1.0 && - attr.selected[] && any(key -> ispressed(scene, attr[key][]), keynames) + cam.selected[] && any(key -> ispressed(scene, controls[key][]), keynames) cam.pulser[] = time() return Consume(true) end @@ -185,37 +217,33 @@ function Camera3D(scene::Scene; kwargs...) deselect_all_cameras!(root(scene)) on(camera(scene), events(scene).mousebutton, priority = 100) do event if event.action == Mouse.press - attr.selected[] = is_mouseinside(scene) + cam.selected[] = is_mouseinside(scene) end return Consume(false) end # Mouse controls - add_translation!(scene, cam) - add_rotation!(scene, cam) + add_mouse_controls!(scene, cam) # add camera controls to scene cameracontrols!(scene, cam) # Trigger updates on scene resize and settings change - on(camera(scene), scene.px_area, attr[:fov], attr[:projectiontype]) do _, _, _ - update_cam!(scene, cam) + on(camera(scene), cam.fov) do _ + if settings.projectiontype[] == Makie.Perspective + update_cam!(scene, cam) + end end - on(camera(scene), attr[:near], attr[:far]) do near, far - near === automatic || (cam.near[] = near) - far === automatic || (cam.far[] = far) + on(camera(scene), scene.px_area, cam.near, cam.far, settings.projectiontype) do _, _, _, _ update_cam!(scene, cam) end # reset on(camera(scene), events(scene).keyboardbutton) do event - if attr.selected[] && event.key == attr[:reset][] && event.action == Keyboard.release + if cam.selected[] && ispressed(scene, controls[:reset][]) # center keeps the rotation of the camera so we reset that here # might make sense to keep user set lookat, upvector, eyeposition # around somewhere for this? - cam.lookat[] = Vec3f(0) - cam.upvector[] = Vec3f(0,0,1) - cam.eyeposition[] = Vec3f(3) center!(scene) return Consume(true) end @@ -226,15 +254,27 @@ function Camera3D(scene::Scene; kwargs...) end # These imitate the old camera +""" + cam3d!(scene[; kwargs...]) + +Creates a `Camera3D` with `zoom_shift_lookat = true` and `fixed_axis = true`. +For more information, see [`Camera3D``](@ref) +""" cam3d!(scene; zoom_shift_lookat = true, fixed_axis = true, kwargs...) = Camera3D(scene, zoom_shift_lookat = zoom_shift_lookat, fixed_axis = fixed_axis; kwargs...) +""" + cam3d_cad!(scene[; kwargs...]) + +Creates a `Camera3D` with `cad = true`, `zoom_shift_lookat = false` and +`fixed_axis = false`. For more information, see [`Camera3D``](@ref) +""" cam3d_cad!(scene; cad = true, zoom_shift_lookat = false, fixed_axis = false, kwargs...) = Camera3D(scene, cad = cad, zoom_shift_lookat = zoom_shift_lookat, fixed_axis = fixed_axis; kwargs...) function deselect_all_cameras!(scene) cam = cameracontrols(scene) - cam isa Camera3D && (cam.attributes.selected[] = false) + cam isa AbstractCamera3D && (cam.selected[] = false) for child in scene.children deselect_all_cameras!(child) end @@ -242,101 +282,186 @@ function deselect_all_cameras!(scene) end -function add_translation!(scene, cam::Camera3D) - translationspeed = cam.attributes[:mouse_translationspeed] - zoomspeed = cam.attributes[:mouse_zoomspeed] - shift_lookat = cam.attributes[:zoom_shift_lookat] - cad = cam.attributes[:cad] - button = cam.attributes[:translation_button] - scroll_mod = cam.attributes[:scroll_mod] +################################################################################ +### Interactivity init +################################################################################ - last_mousepos = RefValue(Vec2f(0, 0)) - dragging = RefValue(false) - function compute_diff(delta) - if cam.attributes[:projectiontype][] == Orthographic - aspect = Float32((/)(widths(scene.px_area[])...)) - aspect_scale = Vec2f(1f0 + aspect, 1f0 + 1f0 / aspect) - return cam.zoom_mult[] * delta .* aspect_scale ./ widths(scene.px_area[]) + +function on_pulse(scene, cam::Camera3D, timestep) + @extractvalue cam.controls ( + right_key, left_key, up_key, down_key, backward_key, forward_key, + tilt_up_key, tilt_down_key, pan_left_key, pan_right_key, roll_counterclockwise_key, roll_clockwise_key, + zoom_out_key, zoom_in_key, increase_fov_key, decrease_fov_key + ) + @extractvalue cam.settings ( + keyboard_translationspeed, keyboard_rotationspeed, keyboard_zoomspeed, projectiontype + ) + + # translation + right = ispressed(scene, right_key) + left = ispressed(scene, left_key) + up = ispressed(scene, up_key) + down = ispressed(scene, down_key) + backward = ispressed(scene, backward_key) + forward = ispressed(scene, forward_key) + translating = right || left || up || down || backward || forward + + if translating + # translation in camera space x/y/z direction + if projectiontype == Perspective + viewnorm = norm(cam.lookat[] - cam.eyeposition[]) + xynorm = 2 * viewnorm * tand(0.5 * cam.fov[]) + translation = keyboard_translationspeed * timestep * Vec3f( + xynorm * (right - left), + xynorm * (up - down), + viewnorm * (backward - forward) + ) else - viewdir = cam.lookat[] - cam.eyeposition[] - return 0.002f0 * cam.zoom_mult[] * norm(viewdir) * delta + # translation in camera space x/y/z direction + viewnorm = norm(cam.eyeposition[] - cam.lookat[]) + translation = 2 * viewnorm * keyboard_translationspeed * timestep * Vec3f( + right - left, up - down, backward - forward + ) end + _translate_cam!(scene, cam, translation) end - # drag start/stop - on(camera(scene), scene.events.mousebutton) do event - if ispressed(scene, button[]) - if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] - last_mousepos[] = mouseposition_px(scene) - dragging[] = true - return Consume(true) - end - elseif event.action == Mouse.release && dragging[] - mousepos = mouseposition_px(scene) - diff = compute_diff(last_mousepos[] .- mousepos) - last_mousepos[] = mousepos - dragging[] = false - translate_cam!(scene, cam, translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) - return Consume(true) - end - return Consume(false) + # rotation + up = ispressed(scene, tilt_up_key) + down = ispressed(scene, tilt_down_key) + left = ispressed(scene, pan_left_key) + right = ispressed(scene, pan_right_key) + counterclockwise = ispressed(scene, roll_counterclockwise_key) + clockwise = ispressed(scene, roll_clockwise_key) + rotating = up || down || left || right || counterclockwise || clockwise + + if rotating + # rotations around camera space x/y/z axes + angles = keyboard_rotationspeed * timestep * + Vec3f(up - down, left - right, counterclockwise - clockwise) + + _rotate_cam!(scene, cam, angles) end - # in drag - on(camera(scene), scene.events.mouseposition) do mp - if dragging[] && ispressed(scene, button[]) - mousepos = screen_relative(scene, mp) - diff = compute_diff(last_mousepos[] .- mousepos) - last_mousepos[] = mousepos - translate_cam!(scene, cam, translationspeed[] * Vec3f(diff[1], diff[2], 0f0)) - return Consume(true) - end - return Consume(false) + # zoom + zoom_out = ispressed(scene, zoom_out_key) + zoom_in = ispressed(scene, zoom_in_key) + zooming = zoom_out || zoom_in + + if zooming + zoom_step = (1f0 + keyboard_zoomspeed * timestep) ^ (zoom_out - zoom_in) + _zoom!(scene, cam, zoom_step, false, false) end - on(camera(scene), scene.events.scroll) do scroll - if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) - zoom_step = (1f0 + 0.1f0 * zoomspeed[]) ^ -scroll[2] - zoom!(scene, cam, zoom_step, shift_lookat[], cad[]) - return Consume(true) - end - return Consume(false) + # fov + fov_inc = ispressed(scene, increase_fov_key) + fov_dec = ispressed(scene, decrease_fov_key) + fov_adjustment = fov_inc || fov_dec + + if fov_adjustment + step = (1 + keyboard_zoomspeed * timestep) ^ (fov_inc - fov_dec) + cam.fov[] = clamp(cam.fov[] * step, 0.1, 179) + end + + # if any are active, update matrices, else stop clock + if translating || rotating || zooming || fov_adjustment + update_cam!(scene, cam) + return true + else + return false end end -function add_rotation!(scene, cam::Camera3D) - rotationspeed = cam.attributes[:mouse_rotationspeed] - button = cam.attributes[:rotation_button] + +function add_mouse_controls!(scene, cam::Camera3D) + @extract cam.controls (translation_button, rotation_button, reposition_button, scroll_mod) + @extract cam.settings ( + mouse_translationspeed, mouse_rotationspeed, mouse_zoomspeed, + cad, projectiontype, zoom_shift_lookat + ) + last_mousepos = RefValue(Vec2f(0, 0)) - dragging = RefValue(false) + dragging = RefValue((false, false)) # rotation, translation + e = events(scene) + function compute_diff(delta) + if projectiontype[] == Perspective + # TODO wrong scaling? :( + ynorm = 2 * norm(cam.lookat[] - cam.eyeposition[]) * tand(0.5 * cam.fov[]) + return ynorm / widths(scene.px_area[])[2] * delta + else + viewnorm = norm(cam.eyeposition[] - cam.lookat[]) + return 2 * viewnorm / widths(scene.px_area[])[2] * delta + end + end + # drag start/stop on(camera(scene), e.mousebutton) do event - if ispressed(scene, button[]) - if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] + # Drag start translation/rotation + if event.action == Mouse.press && is_mouseinside(scene) + if ispressed(scene, translation_button[]) last_mousepos[] = mouseposition_px(scene) - dragging[] = true + dragging[] = (false, true) + return Consume(true) + elseif ispressed(scene, rotation_button[]) + last_mousepos[] = mouseposition_px(scene) + dragging[] = (true, false) return Consume(true) end - elseif event.action == Mouse.release && dragging[] - mousepos = mouseposition_px(scene) - dragging[] = false - rot_scaling = rotationspeed[] * (e.window_dpi[] * 0.005) - mp = (last_mousepos[] .- mousepos) .* 0.01f0 .* rot_scaling - last_mousepos[] = mousepos - rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) - return Consume(true) + # drag stop & repostion + elseif event.action == Mouse.release + consume = false + + # Drag stop translation/rotation + if dragging[][1] + mousepos = mouseposition_px(scene) + diff = compute_diff(last_mousepos[] .- mousepos) + last_mousepos[] = mousepos + dragging[] = (false, false) + translate_cam!(scene, cam, mouse_translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) + consume = true + elseif dragging[][2] + mousepos = mouseposition_px(scene) + dragging[] = (false, false) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) + mp = (last_mousepos[] .- mousepos) .* 0.01f0 .* rot_scaling + last_mousepos[] = mousepos + rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) + consume = true + end + + # reposition + if ispressed(scene, reposition_button[], event.button) && is_mouseinside(scene) + plt, _, p = ray_assisted_pick(scene) + if p !== Point3f(NaN) && to_value(get(plt, :space, :data)) == :data && parent_scene(plt) == scene + # if translation/rotation happens with on-click reposition, + # try uncommenting this + # dragging[] = (false, false) + shift = p - cam.lookat[] + update_cam!(scene, cam, cam.eyeposition[] + shift, p) + end + consume = true + end + + return Consume(consume) end + return Consume(false) end # in drag on(camera(scene), e.mouseposition) do mp - if dragging[] && ispressed(scene, button[]) + if dragging[][2] && ispressed(scene, translation_button[]) mousepos = screen_relative(scene, mp) - rot_scaling = rotationspeed[] * (e.window_dpi[] * 0.005) + diff = compute_diff(last_mousepos[] .- mousepos) + last_mousepos[] = mousepos + translate_cam!(scene, cam, mouse_translationspeed[] * Vec3f(diff[1], diff[2], 0f0)) + return Consume(true) + elseif dragging[][1] && ispressed(scene, rotation_button[]) + mousepos = screen_relative(scene, mp) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) mp = (last_mousepos[] .- mousepos) * 0.01f0 * rot_scaling last_mousepos[] = mousepos rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) @@ -344,80 +469,82 @@ function add_rotation!(scene, cam::Camera3D) end return Consume(false) end -end + #zoom + on(camera(scene), e.scroll) do scroll + if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) + zoom_step = (1f0 + 0.1f0 * mouse_zoomspeed[]) ^ -scroll[2] + zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[]) + return Consume(true) + end + return Consume(false) + end -function on_pulse(scene, cam, timestep) - attr = cam.attributes - # translation - right = ispressed(scene, attr[:right_key][]) - left = ispressed(scene, attr[:left_key][]) - up = ispressed(scene, attr[:up_key][]) - down = ispressed(scene, attr[:down_key][]) - backward = ispressed(scene, attr[:backward_key][]) - forward = ispressed(scene, attr[:forward_key][]) - translating = right || left || up || down || backward || forward +end - if translating - # translation in camera space x/y/z direction - translation = attr[:keyboard_translationspeed][] * timestep * - Vec3f(right - left, up - down, backward - forward) - viewdir = cam.lookat[] - cam.eyeposition[] - _translate_cam!(scene, cam, cam.zoom_mult[] * norm(viewdir) * translation) - end - # rotation - up = ispressed(scene, attr[:tilt_up_key][]) - down = ispressed(scene, attr[:tilt_down_key][]) - left = ispressed(scene, attr[:pan_left_key][]) - right = ispressed(scene, attr[:pan_right_key][]) - counterclockwise = ispressed(scene, attr[:roll_counterclockwise_key][]) - clockwise = ispressed(scene, attr[:roll_clockwise_key][]) - rotating = up || down || left || right || counterclockwise || clockwise +################################################################################ +### Camera transformations +################################################################################ - if rotating - # rotations around camera space x/y/z axes - angles = attr[:keyboard_rotationspeed][] * timestep * - Vec3f(up - down, left - right, counterclockwise - clockwise) - _rotate_cam!(scene, cam, angles) - end +# Simplified methods +""" + translate_cam!(scene, cam::Camera3D, v::Vec3) - # zoom - zoom_out = ispressed(scene, attr[:zoom_out_key][]) - zoom_in = ispressed(scene, attr[:zoom_in_key][]) - zooming = zoom_out || zoom_in +Translates the camera by the given vector in camera space, i.e. by `v[1]` to +the right, `v[2]` to the top and `v[3]` forward. - if zooming - zoom_step = (1f0 + attr[:keyboard_zoomspeed][] * timestep) ^ (zoom_out - zoom_in) - _zoom!(scene, cam, zoom_step, false) - end +Note that this method reacts to `fix_x_key` etc. If any of those keys are +pressed the translation will be restricted to act in these directions. +""" +function translate_cam!(scene, cam::Camera3D, t::VecTypes) + _translate_cam!(scene, cam, t) + update_cam!(scene, cam) + nothing +end - stretch = ispressed(scene, attr[:stretch_view_key][]) - contract = ispressed(scene, attr[:contract_view_key][]) - if stretch || contract - zoom_step = (1f0 + attr[:keyboard_zoomspeed][] * timestep) ^ (stretch - contract) - cam.eyeposition[] = cam.lookat[] + zoom_step * (cam.eyeposition[] - cam.lookat[]) - end - zooming = zooming || stretch || contract +""" + rotate_cam!(scene, cam::Camera3D, angles::Vec3) - # if any are active, update matrices, else stop clock - if translating || rotating || zooming - update_cam!(scene, cam) - return true - else - return false - end +Rotates the camera by the given `angles` around the camera x- (left, right), +y- (up, down) and z-axis (in out). The rotation around the y axis is applied +first, then x, then y. + +Note that this method reacts to `fix_x_key` etc and `fixed_axis`. The former +restrict the rotation around a specific axis when a given key is pressed. The +latter keeps the camera y axis fixed as the data space z axis. +""" +function rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) + _rotate_cam!(scene, cam, angles, from_mouse) + update_cam!(scene, cam) + nothing end -function translate_cam!(scene::Scene, cam::Camera3D, t::VecTypes) - _translate_cam!(scene, cam, t) +zoom!(scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false, false) +""" + zoom!(scene, cam::Camera3D, zoom_step[, cad = false, zoom_shift_lookat = false]) + +Zooms the camera in or out based on the multiplier `zoom_step`. A `zoom_step` +of 1.0 is neutral, larger zooms out and lower zooms in. + +If `cad = true` zooming will also apply a rotation based on how far the cursor +is from the center of the scene. If `zoom_shift_lookat = true` and +`projectiontype = Orthographic` zooming will keep the data under the cursor at +the same screen space position. +""" +function zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false) + _zoom!(scene, cam, zoom_step, cad, zoom_shift_lookat) update_cam!(scene, cam) nothing end -function _translate_cam!(scene, cam, t) + + +function _translate_cam!(scene, cam::Camera3D, t) + @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) + # This uses a camera based coordinate system where # x expands right, y expands up and z expands towards the screen lookat = cam.lookat[] @@ -430,9 +557,9 @@ function _translate_cam!(scene, cam, t) trans = u_x * t[1] + u_y * t[2] + u_z * t[3] # apply world space restrictions - fix_x = ispressed(scene, cam.attributes[:fix_x_key][]) - fix_y = ispressed(scene, cam.attributes[:fix_y_key][]) - fix_z = ispressed(scene, cam.attributes[:fix_z_key][]) + fix_x = ispressed(scene, fix_x_key)::Bool + fix_y = ispressed(scene, fix_y_key)::Bool + fix_z = ispressed(scene, fix_z_key)::Bool if fix_x || fix_y || fix_z trans = Vec3f(fix_x, fix_y, fix_z) .* trans end @@ -443,12 +570,10 @@ function _translate_cam!(scene, cam, t) end -function rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) - _rotate_cam!(scene, cam, angles, from_mouse) - update_cam!(scene, cam) - nothing -end function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) + @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) + @extractvalue cam.settings (fixed_axis, circular_rotation, rotation_center) + # This applies rotations around the x/y/z axis of the camera coordinate system # x expands right, y expands up and z expands towards the screen lookat = cam.lookat[] @@ -458,32 +583,35 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) right = cross(viewdir, up) # +x x_axis = right - y_axis = cam.attributes[:fixed_axis][] ? Vec3f(0, 0, ifelse(up[3] < 0, -1, 1)) : up + y_axis = fixed_axis ? Vec3f(0, 0, ifelse(up[3] < 0, -1, 1)) : up z_axis = -viewdir - fix_x = ispressed(scene, cam.attributes[:fix_x_key][]) - fix_y = ispressed(scene, cam.attributes[:fix_y_key][]) - fix_z = ispressed(scene, cam.attributes[:fix_z_key][]) - cx, cy, cz = cam.attributes[:circular_rotation][] + fix_x = ispressed(scene, fix_x_key)::Bool + fix_y = ispressed(scene, fix_y_key)::Bool + fix_z = ispressed(scene, fix_z_key)::Bool + cx, cy, cz = circular_rotation + rotation = Quaternionf(0, 0, 0, 1) if !xor(fix_x, fix_y, fix_z) # if there are more or less than one restriction apply all rotations + # Note that the y rotation needs to happen first here so that + # fixed_axis = true actually keeps the the axis fixed. rotation *= qrotation(y_axis, angles[2]) rotation *= qrotation(x_axis, angles[1]) rotation *= qrotation(z_axis, angles[3]) else # apply world space restrictions - if from_mouse && ((fix_x && (fix_x == cx)) || (fix_y && (fix_y == cy)) || (fix_z && (fix_z == cz))) + if from_mouse && ((fix_x && cx) || (fix_y && cy) || (fix_z && cz)) # recontextualize the (dy, dx, 0) from mouse rotations so that # drawing circles creates continuous rotations around the fixed axis mp = mouseposition_px(scene) past_half = 0.5f0 .* widths(scene.px_area[]) .> mp flip = 2f0 * past_half .- 1f0 angle = flip[1] * angles[1] + flip[2] * angles[2] - angles = Vec3f(-angle, angle, -angle) + angles = Vec3f(-angle, -angle, angle) # only one fix is true so this only rotates around one axis rotation *= qrotation( - Vec3f(fix_x, fix_z, fix_y) .* Vec3f(sign(right[1]), viewdir[2], sign(up[3])), + Vec3f(fix_x, fix_y, fix_z) .* Vec3f(sign(right[1]), viewdir[2], sign(up[3])), dot(Vec3f(fix_x, fix_y, fix_z), angles) ) else @@ -501,138 +629,144 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) # TODO maybe generalize this to arbitrary center? # calculate positions from rotated vectors - if cam.attributes[:rotation_center][] === :lookat + if rotation_center === :lookat cam.eyeposition[] = lookat - viewdir else cam.lookat[] = eyepos + viewdir end + return end -""" - zoom!(scene, zoom_step) - -Zooms the camera in or out based on the multiplier `zoom_step`. A `zoom_step` -of 1.0 is neutral, larger zooms out and lower zooms in. - -Note that this method only applies to Camera3D. -""" -zoom!(scene::Scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false, false) -function zoom!(scene::Scene, cam::Camera3D, zoom_step, shift_lookat = false, cad = false) - _zoom!(scene, cam, zoom_step, shift_lookat, cad) - update_cam!(scene, cam) - nothing -end -function _zoom!(scene::Scene, cam::Camera3D, zoom_step, shift_lookat = false, cad = false) +function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false) + lookat = cam.lookat[] + eyepos = cam.eyeposition[] + viewdir = lookat - eyepos # -z + if cad - # move exeposition if mouse is not over the center - lookat = cam.lookat[] - eyepos = cam.eyeposition[] - up = cam.upvector[] # +y - viewdir = lookat - eyepos # -z - right = cross(viewdir, up) # +x + # Rotate view based on offset from center + u_z = normalize(viewdir) + u_x = normalize(cross(u_z, cam.upvector[])) + u_y = normalize(cross(u_x, u_z)) rel_pos = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - shift = rel_pos[1] * normalize(right) + rel_pos[2] * normalize(up) - shifted = eyepos + 0.1f0 * sign(1f0 - zoom_step) * norm(viewdir) * shift - cam.eyeposition[] = lookat + norm(viewdir) * normalize(shifted - lookat) - elseif shift_lookat - lookat = cam.lookat[] - eyepos = cam.eyeposition[] - up = normalize(cam.upvector[]) - viewdir = lookat - eyepos - u_z = normalize(-viewdir) - u_x = normalize(cross(up, u_z)) - u_y = normalize(cross(u_z, u_x)) - - if cam.attributes[:projectiontype][] == Perspective - # translate both eyeposition and lookat to more or less keep data - # under the mouse in view - fov = cam.attributes[:fov][] - before = tan(clamp(cam.zoom_mult[] * fov, 0.01f0, 175f0) / 360f0 * Float32(pi)) - after = tan(clamp(cam.zoom_mult[] * zoom_step * fov, 0.01f0, 175f0) / 360f0 * Float32(pi)) - - aspect = Float32((/)(widths(scene.px_area[])...)) - rel_pos = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - shift = rel_pos[1] * u_x + rel_pos[2] * u_y - shift = -(after - before) * norm(viewdir) * normalize(aspect .* shift) + shift = rel_pos[1] * u_x + rel_pos[2] * u_y + shift *= 0.1 * sign(1 - zoom_step) * norm(viewdir) + + cam.eyeposition[] = lookat - zoom_step * viewdir + shift + elseif zoom_shift_lookat + # keep data under cursor + u_z = normalize(viewdir) + u_x = normalize(cross(u_z, cam.upvector[])) + u_y = normalize(cross(u_x, u_z)) + + ws = widths(scene.px_area[]) + rel_pos = (2.0 .* mouseposition_px(scene) .- ws) ./ ws[2] + shift = (1 - zoom_step) * (rel_pos[1] * u_x + rel_pos[2] * u_y) + + if cam.settings.projectiontype[] == Makie.Orthographic + scale = norm(viewdir) else - mx, my = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - aspect = Float32((/)(widths(scene.px_area[])...)) - w = 0.5f0 * (1f0 + aspect) * cam.zoom_mult[] - h = 0.5f0 * (1f0 + 1f0 / aspect) * cam.zoom_mult[] - shift = (1f0 - zoom_step) * (mx * w * u_x + my * h * u_y) + # With perspective projection depth scales shift, but there is no way + # to tell which depth the user may want to keep in view. So we just + # assume it's the same depth as "lookat". + scale = norm(viewdir) * tand(0.5 * cam.fov[]) end - cam.lookat[] = lookat + shift - cam.eyeposition[] = eyepos + shift + cam.lookat[] = lookat + scale * shift + cam.eyeposition[] = lookat - zoom_step * viewdir + scale * shift + else + # just zoom in/out + cam.eyeposition[] = lookat - zoom_step * viewdir end - # apply zoom - cam.zoom_mult[] = cam.zoom_mult[] * zoom_step - return end +################################################################################ +### update_cam! methods +################################################################################ + + +# Update camera matrices function update_cam!(scene::Scene, cam::Camera3D) - @extractvalue cam (lookat, eyeposition, upvector) + @extractvalue cam (lookat, eyeposition, upvector, near, far, fov) - near = cam.near[]; far = cam.far[] - aspect = Float32((/)(widths(scene.px_area[])...)) + view = Makie.lookat(eyeposition, lookat, upvector) - if cam.attributes[:projectiontype][] == Perspective - fov = clamp(cam.zoom_mult[] * cam.attributes[:fov][], 0.01f0, 175f0) - cam.fov[] = fov - proj = perspectiveprojection(fov, aspect, near, far) + aspect = Float32((/)(widths(scene.px_area[])...)) + if cam.settings.projectiontype[] == Makie.Perspective + view_norm = norm(eyeposition - lookat) + proj = perspectiveprojection(fov, aspect, view_norm * near, view_norm * far) else - w = 0.5f0 * (1f0 + aspect) * cam.zoom_mult[] - h = 0.5f0 * (1f0 + 1f0 / aspect) * cam.zoom_mult[] - proj = orthographicprojection(-w, w, -h, h, near, far) + h = norm(eyeposition - lookat); w = h * aspect + proj = orthographicprojection(-w, w, -h, h, h * near, h * far) end - view = Makie.lookat(eyeposition, lookat, upvector) - set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] end -function update_cam!(scene::Scene, camera::Camera3D, area3d::Rect) - @extractvalue camera (lookat, eyeposition, upvector) + +# Update camera position via bbox +function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) bb = Rect3f(area3d) width = widths(bb) - half_width = width ./ 2f0 - middle = maximum(bb) - half_width - old_dir = normalize(eyeposition .- lookat) - camera.lookat[] = middle - neweyepos = middle .+ (1.2*norm(width) .* old_dir) - camera.eyeposition[] = neweyepos - camera.upvector[] = Vec3f(0,0,1) - if camera.attributes[:near][] === automatic - camera.near[] = 0.1f0 * norm(widths(bb)) - end - if camera.attributes[:far][] === automatic - camera.far[] = 3f0 * norm(widths(bb)) - end - if camera.attributes[:projectiontype][] == Orthographic - camera.zoom_mult[] = 0.6 * norm(width) + center = maximum(bb) - 0.5 * width + + old_dir = normalize(cam.eyeposition[] .- cam.lookat[]) + if cam.settings.projectiontype[] == Makie.Perspective + dist = 0.5 * norm(width) / tand(0.5 * cam.fov[]) else - camera.zoom_mult[] = 1f0 + dist = 0.5 * norm(width) end - update_cam!(scene, camera) + + cam.lookat[] = center + cam.eyeposition[] = cam.lookat[] .+ dist * old_dir + cam.upvector[] = Vec3f(0, 0, 1) # Should we reset this? + + update_cam!(scene, cam) + return end -function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = Vec3f(0, 0, 1)) - camera.lookat[] = Vec3f(lookat) +# Update camera position via camera Position & Orientation +function update_cam!(scene::Scene, camera::Camera3D, eyeposition::VecTypes, lookat::VecTypes, up::VecTypes = camera.upvector[]) + camera.lookat[] = Vec3f(lookat) camera.eyeposition[] = Vec3f(eyeposition) - camera.upvector[] = Vec3f(up) + camera.upvector[] = Vec3f(up) + update_cam!(scene, camera) + return +end + +update_cam!(scene::Scene, args::Real...) = update_cam!(scene, cameracontrols(scene), args...) + +""" + update_cam!(scene, cam::Camera3D, ϕ, θ[, radius]) + +Set the camera position based on two angles `0 ≤ ϕ ≤ 2π` and `-pi/2 ≤ θ ≤ pi/2` +and an optional radius around the current `cam.lookat[]`. +""" +function update_cam!( + scene::Scene, camera::Camera3D, phi::Real, theta::Real, + radius::Real = norm(camera.eyeposition[] - camera.lookat[]), + center = camera.lookat[] + ) + st, ct = sincos(theta) + sp, cp = sincos(phi) + v = Vec3f(ct * cp, ct * sp, st) + u = Vec3f(-st * cp, -st * sp, ct) + camera.lookat[] = center + camera.eyeposition[] = center .+ radius * v + camera.upvector[] = u update_cam!(scene, camera) return end + function show_cam(scene) cam = cameracontrols(scene) println("cam=cameracontrols(scene)") @@ -641,4 +775,4 @@ function show_cam(scene) println("cam.upvector[] = ", round.(cam.upvector[], digits=2)) println("cam.fov[] = ", round.(cam.fov[], digits=2)) return -end +end \ No newline at end of file diff --git a/src/camera/old_camera3d.jl b/src/camera/old_camera3d.jl index 2d5f424cccf..e3167262327 100644 --- a/src/camera/old_camera3d.jl +++ b/src/camera/old_camera3d.jl @@ -96,7 +96,12 @@ An alias to [`old_cam3d_turntable!`](@ref). Creates a 3D camera for `scene`, which rotates around the plot's axis. """ -const old_cam3d! = old_cam3d_turntable! +old_cam3d!(scene::Scene; kwargs...) = old_cam3d_turntable!(scene; kwargs...) + +@deprecate old_cam3d! cam3d! +@deprecate old_cam3d_turntable! cam3d! +@deprecate old_cam3d_cad! cam3d_cad! + function projection_switch( wh::Rect2, diff --git a/src/interaction/events.jl b/src/interaction/events.jl index 9fedf07127f..7d599680730 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -75,8 +75,6 @@ function onpick end ################################################################################ -abstract type BooleanOperator end - """ And(left, right[, rest...]) @@ -224,10 +222,10 @@ create_sets(s::Set) = [Set{Union{Keyboard.Button, Mouse.Button}}(s)] # ispressed and logic evaluation """ - ispressed(parent, result::Bool) - ispressed(parent, button::Union{Mouse.Button, Keyboard.Button) - ispressed(parent, collection::Union{Set, Vector, Tuple}) - ispressed(parent, op::BooleanOperator) +ispressed(parent, result::Bool[, waspressed = nothing]) +ispressed(parent, button::Union{Mouse.Button, Keyboard.Button[, waspressed = nothing]) + ispressed(parent, collection::Union{Set, Vector, Tuple}[, waspressed = nothing]) + ispressed(parent, op::BooleanOperator[, waspressed = nothing]) This function checks if a button or combination of buttons is pressed. @@ -251,25 +249,32 @@ Furthermore you can also make any button, button collection or boolean expression exclusive by wrapping it in `Exclusively(...)`. With that `ispressed` will only return true if the currently pressed buttons match the request exactly. -See also: [`And`](@ref), [`Or`](@ref), [`Not`](@ref), [`Exclusively`](@ref), +For cases where you want to react to a release event you can optionally add +a key or mousebutton `waspressed` which is then assumed to be pressed regardless +of it's current state. For example, when reacting to a mousebutton event, you can +pass `event.button` so that a key combination including that button still evaluates +as true. + +See also: [`waspressed`](@ref) [`And`](@ref), [`Or`](@ref), [`Not`](@ref), [`Exclusively`](@ref), [`&`](@ref), [`|`](@ref), [`!`](@ref) """ -ispressed(events::Events, mb::Mouse.Button) = mb in events.mousebuttonstate -ispressed(events::Events, key::Keyboard.Button) = key in events.keyboardstate -ispressed(parent, result::Bool) = result +ispressed(events::Events, mb::Mouse.Button, waspressed = nothing) = mb in events.mousebuttonstate || mb == waspressed +ispressed(events::Events, key::Keyboard.Button, waspressed = nothing) = key in events.keyboardstate || key == waspressed +ispressed(parent, result::Bool, waspressed = nothing) = result -ispressed(parent, mb::Mouse.Button) = ispressed(events(parent), mb) -ispressed(parent, key::Keyboard.Button) = ispressed(events(parent), key) +ispressed(parent, mb::Mouse.Button, waspressed = nothing) = ispressed(events(parent), mb, waspressed) +ispressed(parent, key::Keyboard.Button, waspressed = nothing) = ispressed(events(parent), key, waspressed) @deprecate ispressed(scene, ::Nothing) ispressed(parent, true) # Boolean Operator evaluation -ispressed(parent, op::And) = ispressed(parent, op.left) && ispressed(parent, op.right) -ispressed(parent, op::Or) = ispressed(parent, op.left) || ispressed(parent, op.right) -ispressed(parent, op::Not) = !ispressed(parent, op.x) -ispressed(parent, op::Exclusively) = ispressed(events(parent), op) -ispressed(e::Events, op::Exclusively) = op.x == union(e.keyboardstate, e.mousebuttonstate) +ispressed(parent, op::And, waspressed = nothing) = ispressed(parent, op.left, waspressed) && ispressed(parent, op.right, waspressed) +ispressed(parent, op::Or, waspressed = nothing) = ispressed(parent, op.left, waspressed) || ispressed(parent, op.right, waspressed) +ispressed(parent, op::Not, waspressed = nothing) = !ispressed(parent, op.x, waspressed) +ispressed(parent, op::Exclusively, waspressed = nothing) = ispressed(events(parent), op, waspressed) +ispressed(e::Events, op::Exclusively, waspressed::Union{Mouse.Button, Keyboard.Button}) = op.x == union(e.keyboardstate, e.mousebuttonstate, waspressed) +ispressed(e::Events, op::Exclusively, waspressed = nothing) = op.x == union(e.keyboardstate, e.mousebuttonstate) # collections -ispressed(parent, set::Set) = all(x -> ispressed(parent, x), set) -ispressed(parent, set::Vector) = all(x -> ispressed(parent, x), set) -ispressed(parent, set::Tuple) = all(x -> ispressed(parent, x), set) +ispressed(parent, set::Set, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) +ispressed(parent, set::Vector, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) +ispressed(parent, set::Tuple, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index e7cfa61eb67..4e78e9aefd0 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -511,7 +511,6 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i # cast ray from cursor into screen, find closest point to line pos = position_on_plot(plot, idx) - proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) diff --git a/src/interaction/ray_casting.jl b/src/interaction/ray_casting.jl index f87ffe4bae3..38d7f8f94e5 100644 --- a/src/interaction/ray_casting.jl +++ b/src/interaction/ray_casting.jl @@ -40,7 +40,7 @@ function Ray(scene::Scene, cam::Camera3D, xy::VecTypes{2}) aspect = px_width / px_height rel_pos = 2 .* xy ./ (px_width, px_height) .- 1 - if cam.attributes.projectiontype[] === Perspective + if cam.settings.projectiontype[] === Perspective dir = (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) * tand(0.5 * cam.fov[]) + u_z return Ray(cam.eyeposition[], normalize(dir)) else @@ -124,6 +124,12 @@ function closest_point_on_line(A::Point3f, B::Point3f, ray::Ray) return A .+ clamp(t, 0.0, AB_norm) * u_AB end +function ray_triangle_intersection(A::VecTypes, B::VecTypes, C::VecTypes, ray::Ray, ϵ = 1e-6) + return ray_triangle_intersection( + to_ndim(Point3f, A, 0f0), to_ndim(Point3f, B, 0f0), to_ndim(Point3f, C, 0f0), + ray, ϵ + ) +end function ray_triangle_intersection(A::VecTypes{3}, B::VecTypes{3}, C::VecTypes{3}, ray::Ray, ϵ = 1e-6) # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html @@ -299,8 +305,8 @@ function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) end end end - - @info "Did not find $idx" + + @debug "Did not find intersection for index = $idx when casting a ray on mesh." return Point3f(NaN) end diff --git a/src/types.jl b/src/types.jl index c28e74df6a8..7c741d71e09 100644 --- a/src/types.jl +++ b/src/types.jl @@ -378,3 +378,13 @@ end # The color type we ideally use for most color attributes const RGBColors = Union{RGBAf, Vector{RGBAf}, Vector{Float32}} + + +abstract type BooleanOperator end + +""" + IsPressedInputType + +Union containing possible input types for `ispressed`. +""" +const IsPressedInputType = Union{Bool, BooleanOperator, Mouse.Button, Keyboard.Button, Set, Vector, Tuple} \ No newline at end of file diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 564b1a53418..db9e95aa217 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -106,7 +106,19 @@ function extract_expr(extract_func, dictlike, args) end """ -usage @extract scene (a, b, c, d) + @extract scene (a, b, c, d) + +This becomes + +```julia +begin + a = scene[:a] + b = scene[:b] + c = scene[:d] + d = scene[:d] + (a, b, c, d) +end +``` """ macro extract(scene, args) extract_expr(getindex, scene, args) @@ -265,6 +277,22 @@ function merged_get!(defaults::Function, key, scene::SceneLike, input::Attribute return merge!(input, d) end +function Base.replace!(target::Attributes, key, scene::SceneLike, overwrite::Attributes) + if haskey(theme(scene), key) + _replace!(target, theme(scene, key)) + end + return _replace!(target, overwrite) +end + +function _replace!(target::Attributes, overwrite::Attributes) + for k in keys(target) + haskey(overwrite, k) && (target[k] = overwrite[k]) + end + return +end + + + to_vector(x::AbstractVector, len, T) = convert(Vector{T}, x) function to_vector(x::AbstractArray, len, T) if length(x) in size(x) # assert that just one dim != 1 @@ -345,5 +373,6 @@ function extract_keys(attributes, keys) end # Scalar - Vector getindex -sv_getindex(v::Vector, i::Integer) = v[i] -sv_getindex(x, i::Integer) = x +sv_getindex(v::AbstractVector, i::Integer) = v[i] +sv_getindex(x, ::Integer) = x +sv_getindex(x::VecTypes, ::Integer) = x diff --git a/test/events.jl b/test/events.jl index 31b2b484eb8..464fa170627 100644 --- a/test/events.jl +++ b/test/events.jl @@ -200,7 +200,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) - + # Verify initial camera state @test cc.lookat[] == Vec3f(0) @test cc.eyeposition[] == Vec3f(3) @@ -218,14 +218,14 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # 2) Outside scene, in drag e.mouseposition[] = (1000, 450) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491522) + @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491514) @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) # 3) not in drag e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) e.mouseposition[] = (400, 250) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491522) + @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491514) @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) @@ -243,23 +243,24 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # translation # 1) In scene, in drag + e.mouseposition[] = (400, 250) e.mousebutton[] = MouseButtonEvent(Mouse.right, Mouse.press) e.mouseposition[] = (600, 250) - @test cc.lookat[] ≈ Vec3f(5.4697413, -3.3484206, -2.1213205) - @test cc.eyeposition[] ≈ Vec3f(8.469742, -0.34842062, 0.8786795) + @test cc.lookat[] ≈ Vec3f(1.0146117, -1.0146117, 0.0) + @test cc.eyeposition[] ≈ Vec3f(4.0146117, 1.9853883, 3.0) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) # 2) Outside scene, in drag e.mouseposition[] = (1000, 450) - @test cc.lookat[] ≈ Vec3f(9.257657, -5.4392805, -3.818377) - @test cc.eyeposition[] ≈ Vec3f(12.257658, -2.4392805, -0.81837714) + @test cc.lookat[] ≈ Vec3f(3.6296215, -2.4580488, -1.1715729) + @test cc.eyeposition[] ≈ Vec3f(6.6296215, 0.5419513, 1.8284271) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) # 3) not in drag e.mousebutton[] = MouseButtonEvent(Mouse.right, Mouse.release) e.mouseposition[] = (400, 250) - @test cc.lookat[] ≈ Vec3f(9.257657, -5.4392805, -3.818377) - @test cc.eyeposition[] ≈ Vec3f(12.257658, -2.4392805, -0.81837714) + @test cc.lookat[] ≈ Vec3f(3.6296215, -2.4580488, -1.1715729) + @test cc.eyeposition[] ≈ Vec3f(6.6296215, 0.5419513, 1.8284271) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) @@ -274,22 +275,20 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right @test cc.lookat[] == Vec3f(0) @test cc.eyeposition[] == Vec3f(3) @test cc.upvector[] == Vec3f(0, 0, 1) - @test cc.zoom_mult[] == 1f0 # Zoom + e.mouseposition[] = (400, 250) # for debugging e.scroll[] = (0.0, 4.0) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(3) + @test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) - @test cc.zoom_mult[] ≈ 0.6830134f0 # should not work outside the scene e.mouseposition[] = (1000, 450) e.scroll[] = (0.0, 4.0) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(3) + @test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) - @test cc.zoom_mult[] ≈ 0.6830134f0 end @testset "mouse state machine" begin diff --git a/test/ray_casting.jl b/test/ray_casting.jl index ec88508ba18..135dd82d95d 100644 --- a/test/ray_casting.jl +++ b/test/ray_casting.jl @@ -131,7 +131,7 @@ center!(scene) ray = Makie.Ray(scene, (16.0, 306.0)) pos = Makie.position_on_plot(p, 0, ray) - @test pos ≈ Point3f(10.0, 0.18444633, 9.989262) + @test pos ≈ Point3f(10.0, 0.08616829, 9.989262) end # For recreating the above: