Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zooming & translation controls to Axis3 #4131

Merged
merged 58 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
385a2b0
zoom prototype
ffreyer Aug 14, 2024
00ed0e3
Merge branch 'master' into ff/Axis3-controls
ffreyer Aug 20, 2024
7040ca7
add zooming as an Interaction
ffreyer Aug 20, 2024
61ab665
add limit reset
ffreyer Aug 20, 2024
6df5852
add translation
ffreyer Aug 20, 2024
b3db9d8
make translation more accurate
ffreyer Aug 20, 2024
d5c125a
add docstrings
ffreyer Aug 20, 2024
199a9fc
fix x/y/z reversed
ffreyer Aug 20, 2024
9eb6639
update changelog [skip ci]
ffreyer Aug 20, 2024
9625654
Merge branch 'master' into ff/Axis3-controls
ffreyer Aug 20, 2024
87aa380
move code to the right places
ffreyer Aug 20, 2024
2510151
fix corrupted gl state
ffreyer Aug 20, 2024
0ae0409
fix CairoMakie text - clip planes interaction
ffreyer Aug 20, 2024
f4553f3
remove comment
ffreyer Aug 21, 2024
f6eeafd
reuse and conform to existing infrastructure
ffreyer Aug 21, 2024
b1083b9
add docs for Axis3 interactions
ffreyer Aug 21, 2024
c95a439
add unit tests & fix zoom coordinate system
ffreyer Aug 21, 2024
c182e94
do plot picking ray cast as part of :cursor zoom
ffreyer Aug 21, 2024
12d8d5b
remove old code
ffreyer Aug 21, 2024
bb8835e
match zoom direction with Axis, LScene
ffreyer Aug 21, 2024
0abac18
update tests
ffreyer Aug 21, 2024
9adcbe2
Merge branch 'master' into ff/Axis3-controls
SimonDanisch Aug 29, 2024
ac7d121
Merge branch 'master' into ff/Axis3-controls
ffreyer Aug 30, 2024
982119e
Merge branch 'ff/Axis3-controls' of https://github.com/MakieOrg/Makie…
ffreyer Aug 30, 2024
2646775
Merge branch 'master' into ff/Axis3-controls
ffreyer Sep 20, 2024
6f6a068
Merge branch 'master' into ff/Axis3-controls
ffreyer Nov 13, 2024
a336fa3
add viewmode = :free
ffreyer Nov 13, 2024
6450d12
tweak refimg to include perspectiveness and be less wide
ffreyer Nov 13, 2024
05934bd
fix some test failures
ffreyer Nov 13, 2024
323da39
revert change to decorations
ffreyer Nov 13, 2024
10b9d9c
fix backend dependency of new refimg
ffreyer Nov 14, 2024
14de291
Merge branch 'master' into ff/Axis3-controls
ffreyer Nov 15, 2024
435ebb9
fix var name in new test
ffreyer Nov 18, 2024
9071f6a
Merge branch 'ff/Axis3-controls' of https://github.com/MakieOrg/Makie…
ffreyer Nov 18, 2024
a755392
Merge branch 'master' into ff/Axis3-controls
ffreyer Nov 23, 2024
d598dcf
avoid rendering issues from too small near
ffreyer Nov 23, 2024
965cee3
skip irrelevant code for viewmode = :free
ffreyer Nov 23, 2024
3626949
fix aspect problems with translations
ffreyer Nov 23, 2024
20c8039
cleanup some stuff
ffreyer Nov 23, 2024
77984cd
reuse in-axis translations for viewmode = :free
ffreyer Nov 23, 2024
e283e96
add center-on-cursor interaction
ffreyer Nov 23, 2024
e8888b3
fix limit reset not resetting targetlimits
ffreyer Nov 23, 2024
90762e3
fix position_on_plot error for 2D meshes
ffreyer Nov 23, 2024
21815d4
move framelines to 3D scene to avoid line inversion issue
ffreyer Nov 25, 2024
288ede8
Merge branch 'breaking-0.22' into ff/Axis3-controls
ffreyer Nov 25, 2024
7f191d1
use Float64
ffreyer Nov 25, 2024
19f2dd7
bandaid fix for render order
ffreyer Nov 25, 2024
77e48e8
Change LimitReset to more closely match Axis
ffreyer Nov 25, 2024
0ebf94e
update docs
ffreyer Nov 25, 2024
3823a3f
Merge branch 'ff/Axis3-controls' of https://github.com/MakieOrg/Makie…
ffreyer Nov 25, 2024
6c9a4f8
fix offset between ticks and grid/frame
ffreyer Nov 25, 2024
fabf49e
fix test error
ffreyer Nov 25, 2024
838d04d
add test for project(scene, point)
ffreyer Nov 25, 2024
cfaabec
add option to clip decorations (default on)
ffreyer Nov 26, 2024
2c698cf
use viewport for clipping
ffreyer Nov 26, 2024
79e78f1
don't clip decorations by default because it clips titles and too muc…
ffreyer Nov 28, 2024
49f5e2f
Merge branch 'breaking-0.22' into ff/Axis3-controls
ffreyer Nov 28, 2024
6bfdce2
make test more robust
ffreyer Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
- Renamed `Tesselation/tesselation` to `Tessellation/tessellation` [GeometryBasics#227](https://github.com/JuliaGeometry/GeometryBasics.jl/pull/227) [#4564](https://github.com/MakieOrg/Makie.jl/pull/4564)
- Added `Makie.mesh` option for `MetaMesh` which applies some of the bundled information [#4368](https://github.com/MakieOrg/Makie.jl/pull/4368), [#4496](https://github.com/MakieOrg/Makie.jl/pull/4496)
- `Voronoiplot`s automatic colors are now defined based on the underlying point set instead of only those generators appearing in the tessellation. This makes the selected colors consistent between tessellations when generators might have been deleted or added. [#4357](https://github.com/MakieOrg/Makie.jl/pull/4357)
- Split `marker_offset` handling from marker centering and fix various bugs with it [#4594](https://github.com/MakieOrg/Makie.jl/pull/4594).
- Added `viewmode = :free` and translation, zoom, limit reset and cursor-focus interactions to Axis3. [4131](https://github.com/MakieOrg/Makie.jl/pull/4131)
- Split `marker_offset` handling from marker centering and fix various bugs with it [#4594](https://github.com/MakieOrg/Makie.jl/pull/4594)
- Added `transform_marker` attribute to meshscatter and changed the default behavior to not transform marker/mesh vertices [#4606](https://github.com/MakieOrg/Makie.jl/pull/4606)
- Fixed some issues with meshscatter not correctly transforming with transform functions and float32 rescaling [#4606](https://github.com/MakieOrg/Makie.jl/pull/4606)

Expand Down
6 changes: 3 additions & 3 deletions CairoMakie/src/primitives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -579,15 +579,15 @@ function draw_glyph_collection(
# TODO: f32convert may run into issues here if markerspace is :data or
# :transformed (repeated application in glyphpos etc)
transform_func = transformation.transform_func[]
p = apply_transform(transform_func, position, space)
transformed = apply_transform(transform_func, position, space)
p = model * to_ndim(Point4d, to_ndim(Point3d, transformed, 0), 1)

Makie.is_data_space(space) && is_clipped(clip_planes, p) && return

Makie.clip_to_space(scene.camera, markerspace) *
Makie.space_to_clip(scene.camera, space) *
Makie.f32_convert_matrix(scene.float32convert, space) *
model *
to_ndim(Point4d, to_ndim(Point3d, p, 0), 1)
p
end

Cairo.save(ctx)
Expand Down
54 changes: 48 additions & 6 deletions ReferenceTests/src/tests/figures_and_makielayout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,48 @@ end
f
end

@reference_test "Axis3 viewmodes, xreversed, aspect, perspectiveness" begin
fig = Figure(size = (800, 1200))

protrusions = (40, 30, 20, 10)
perspectiveness = Observable(0.0)
cat = GeometryBasics.expand_faceviews(load(Makie.assetpath("cat.obj")))
cs = 1:length(Makie.coordinates(cat))

for (bx, by, viewmode) in [(1,1,:fit), (1,2,:fitzoom), (2,1,:free), (2,2,:stretch)]
gl = GridLayout(fig[by, bx])
Label(gl[0, 1:2], "viewmode = :$viewmode")
for (x, rev) in enumerate((true, false))
for (y, aspect) in enumerate((:data, :equal, (1.2, 0.8, 1.0)))
ax = Axis3(gl[y, x], viewmode = viewmode, xreversed = rev, aspect = aspect,
protrusions = protrusions, perspectiveness = perspectiveness)
mesh!(ax, cat, color = cs)

# for debug purposes
# layout area
fullarea = lift(ax.layoutobservables.computedbbox, ax.layoutobservables.protrusions) do bbox, prot
mini = minimum(bbox) - Vec2(prot.left, prot.bottom)
maxi = maximum(bbox) + Vec2(prot.right, prot.top)
return Rect2f(mini, maxi - mini)
end
p = poly!(fig.scene, fullarea, color = RGBf(1, 0.8, 0.6), strokecolor = :red, strokewidth = 1.5)
translate!(p, 0, 0, -10_000)
# axis area = layout area - protrusions
p = poly!(fig.scene, ax.layoutobservables.computedbbox, color = RGBf(0.8, 0.9, 1), strokecolor = :blue, strokewidth = 1.5, linestyle = :dash)
translate!(p, 0, 0, -10_000)
end
end
end

fig

st = Stepper(fig)
Makie.step!(st)

perspectiveness[] = 1.0
Makie.step!(st)
end

@reference_test "Colorbar for recipes" begin
fig, ax, pl = barplot(1:3; color=1:3, colormap=Makie.Categorical(:viridis), figure=(;size=(800, 800)))
Colorbar(fig[1, 2], pl; size=100)
Expand Down Expand Up @@ -459,31 +501,31 @@ end
@reference_test "Button - Slider - Toggle - Textbox" begin
f = Figure(size = (500, 250))
Makie.Button(f[1, 1:2])
Makie.Button(f[2, 1:2], buttoncolor = :orange, cornerradius = 20,
Makie.Button(f[2, 1:2], buttoncolor = :orange, cornerradius = 20,
strokecolor = :red, strokewidth = 2, # TODO: allocate space for this
fontsize = 16, labelcolor = :blue)

IntervalSlider(f[1, 3])
sl = IntervalSlider(f[2, 3], range = 0:100, linewidth = 20,
sl = IntervalSlider(f[2, 3], range = 0:100, linewidth = 20,
color_inactive = :orange, color_active_dimmed = :lightgreen)
Makie.set_close_to!(sl, 30, 70)

Toggle(f[3, 1])
Toggle(f[4, 1], framecolor_inactive = :lightblue, rimfraction = 0.6)
Toggle(f[3, 2], active = true)
Toggle(f[4, 2], active = true, framecolor_inactive = :lightblue,
Toggle(f[4, 2], active = true, framecolor_inactive = :lightblue,
framecolor_active = :yellow, rimfraction = 0.6)

Makie.Slider(f[3, 3])
sl = Makie.Slider(f[4, 3], range = 0:100, linewidth = 20, color_inactive = :cyan,
sl = Makie.Slider(f[4, 3], range = 0:100, linewidth = 20, color_inactive = :cyan,
color_active_dimmed = :lightgreen)
Makie.set_close_to!(sl, 30)

gl = GridLayout(f[5, 1:3])
Textbox(gl[1, 1])
Textbox(gl[1, 2], bordercolor = :red, cornerradius = 0,
Textbox(gl[1, 2], bordercolor = :red, cornerradius = 0,
placeholder = "test string", fontsize = 16, textcolor_placeholder = :blue)
tb = Textbox(gl[1, 3], bordercolor = :black, cornerradius = 20,
tb = Textbox(gl[1, 3], bordercolor = :black, cornerradius = 20,
fontsize =10, textcolor = :red, boxcolor = :lightblue)
Makie.set!(tb, "some string")

Expand Down
59 changes: 59 additions & 0 deletions docs/src/reference/blocks/axis3.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,64 @@
# Axis3

## Axis3 interactions

Like Axis, Axis3 has a few predefined interactions enabled.

### Rotation

You can rotate the view by left-clicking and dragging.
This interaction is registered as `:dragrotate` and uses the `DragRotate` type.

### Zoom

You can zoom in an axis by scrolling in and out.
By default, the zoom is focused on the center of the Axis.
You can set `zoommode = :cursor` to focus the zoom on the cursor instead.
If you press `x`, `y` or `z` while scrolling, the zoom is restricted to that dimension.
If you press two keys simultaneously, the zoom will be restricted to the corresponding plane instead.
These keys can be changed with the attributes `xzoomkey`, `yzoomkey` and `zzoomkey`.
You can also restrict the zoom dimensions all the time by setting the axis attributes `xzoomlock`, `yzoomlock` or `zzoomlock` to `true`.

With `viewmode = :free` the behavior of the zoom changes.
Instead of affecting just the content of the axis, zooming affects the axis as a whole.
It also disables `zoommode = :cursor`.
This interaction is registered as `:scrollzoom` and uses the `ScrollZoom` type.

### Translation

You can translate the view of the Axis3 by right-clicking and dragging.
If you press `x`, `y` or `z` while translating, the translation is restricted to that dimension.
If you press two keys simultaneously, the translation will be restricted to the corresponding plane instead.
These keys can be changed with the attributes `xtranslationkey`, `ytranslationkey` and `ztranslationkey`.
You can also restrict the translation all the time by setting the axis attributes `xtranslationlock`, `ytranslationlock` or `ztranslationlock` to `true`.

With `viewmode = :free` another option for translation is added.
By pressing `control` while right-click dragging, the translation will affect the placement of the axis in the window instead of the content within the axis.
This interaction is registered as `:translation` and uses the `DragPan` type.

### Limit reset

You can reset the limits, i.e. zoom and translation with `ctrl + left click`.
This is the same as calling `reset_limits!(ax)`.
It sets the limits back to the values stored in `ax.limits`.
If they are `nothing` this computes automatic limits.
If you have previously called `limits!`, `xlims!`, `ylims!` or `zlims!` then `ax.limits` will be set and kept by this interaction.
You can reset the rotation of the axis with `shift + left click`.
If `viewmode = :free` this will also reset the translation of the axis (not just of the content).
If you trigger both simultaneously, i.e. press `ctrl + shift + leftclick`, the axis will be fully reset.
This includes `ax.limits` which are reset to `nothing` via `autolimits!(ax)`
This interaction is registered as `:limitreset` and uses the `LimitReset` type.

### Center on point

You can center the axis on your cursor with `alt + left click`.
Note that depending on the plot type, this may mean different things.
For most, a point on the surface of the plot is used.
For `meshscatter`, `scatter` and derived plots the position of the scattered mesh/marker is used.
This interaction is registered as `:cursorfocus` and uses the `FocusOnCursor` type.



## Attributes

```@attrdocs
Expand Down
15 changes: 9 additions & 6 deletions src/camera/projection_math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ from `eyeposition` to `lookat` will be used. All inputs must be
supplied as 3-vectors.
"""
function lookat(eyePos::Vec{3, T}, lookAt::Vec{3, T}, up::Vec{3, T}) where T
return lookat_basis(eyePos, lookAt, up) * translationmatrix(-eyePos)
end
function lookat_basis(eyePos::Vec{3, T}, lookAt::Vec{3, T}, up::Vec{3, T}) where T
zaxis = normalize(eyePos-lookAt)
xaxis = normalize(cross(up, zaxis))
yaxis = normalize(cross(zaxis, xaxis))
Expand All @@ -139,7 +142,7 @@ function lookat(eyePos::Vec{3, T}, lookAt::Vec{3, T}, up::Vec{3, T}) where T
xaxis[2], yaxis[2], zaxis[2], T0,
xaxis[3], yaxis[3], zaxis[3], T0,
T0, T0, T0, T1
) * translationmatrix(-eyePos)
)
end

function lookat(::Type{T}, eyePos::Vec{3}, lookAt::Vec{3}, up::Vec{3}) where T
Expand Down Expand Up @@ -240,8 +243,8 @@ end

Decomposes a transformation matrix into a translation vector, scale vector and
rotation Quaternion. Note that this is only valid for a transformation matrix
created with matching order, i.e.
`transform = translation_matrix * scale_matrix * rotation_matrix`. The model
created with matching order, i.e.
`transform = translation_matrix * scale_matrix * rotation_matrix`. The model
matrix created by `Transformation` is one such matrix.
"""
function decompose_translation_scale_rotation_matrix(model::Mat4{T}) where T
Expand Down Expand Up @@ -285,9 +288,9 @@ end
"""
decompose_translation_scale_matrix(transform::Mat4)

Like `decompose_translation_scale_rotation_matrix(transform)` but skips the
Like `decompose_translation_scale_rotation_matrix(transform)` but skips the
extraction of the rotation component. This still works if a rotation is involved
and requires the same order of operations, i.e.
and requires the same order of operations, i.e.
`transform = translation_matrix * scale_matrix * rotation_matrix`.
"""
function decompose_translation_scale_matrix(model::Mat4{T}) where T
Expand Down Expand Up @@ -390,7 +393,7 @@ function project(proj_view::Mat4{T1}, resolution::Vec2, point::Point{N, T2}) whe
# at this point the visible range is strictly -1..1 so FLoat64 doesn't matter
p = (clip ./ clip[4])[Vec(1, 2)]
p = Vec2{T}(p[1], p[2])
return (0.5 .* (p .+ 1) .* (resolution .- 1)) .+ 1
return 0.5 .* (p .+ 1) .* resolution
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tick alignment with frame lines got worse when I moved the frame to data space. So I spent too long trying to figure out why.

I thought it might be a Float32 vs Float64 thing, so cleaned up the remaining Float32 types in Axis3. Didn't help. I tried forcing lines to convert on the CPU to make sure conversions don't happen till the end. Didn't help.

Then I got frustrated and moved the CairoMakie line point projection to Makie so I could revert moving the frame to data space without having clipping issues. And the alignment issues were still there. Then I tried using that code for ticks as well and it finally went away.

So i tried to figure out why and started comparing. Didn't notice the resolution - 1. Noticed that clip space points were very different, due to the CairoMakie code clipping to a -1..1 box. So I changed things up to avoid it and there was still a 0.5-1px difference. And then I finally noticed this line, changed it and I the issue seems to be fixed now.

Anyway, fixes #3302.

end

# TODO: consider warning here to discourage risky functions
Expand Down
2 changes: 1 addition & 1 deletion src/interaction/events.jl
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,4 @@ ispressed(e::Events, op::Exclusively, waspressed = nothing) = op.x == union(e.ke
# collections
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)
ispressed(parent, set::Tuple, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set)
27 changes: 22 additions & 5 deletions src/interaction/ray_casting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,23 @@ function is_point_on_ray(p::Point3{T1}, ray::Ray{T2}) where {T1 <: Real, T2 <: R
end


function ray_plane_intersection(plane::Plane3{T1}, ray::Ray{T2}, epsilon = 1e-6) where {T1 <: Real, T2 <: Real}
# --- p --- plane with normal (assumed normalized)
# ↓
# : distance d along plane normal direction
# ↖ :
# r ray with direction (assumed normalized)

d = distance(plane, ray.origin) # signed distance
cos_angle = dot(-plane.normal, ray.direction)

if abs(cos_angle) > epsilon
return ray.origin + d / cos_angle * ray.direction
else
return Point3f(NaN)
end
end

################################################################################
### Ray casting (positions from ray-plot intersections)
################################################################################
Expand Down Expand Up @@ -287,15 +304,15 @@ function position_on_plot(plot::Union{Lines, LineSegments}, idx, ray::Ray; apply
p0, p1 = apply_transform_and_model(plot, plot[1][][(idx-1):idx])

pos = closest_point_on_line(f32_convert(plot, p0), f32_convert(plot, p1), ray)

if apply_transform
return inv_f32_convert(plot, Point3d(pos))
else
p4d = inv(plot.model[]) * to_ndim(Point4d, inv_f32_convert(plot, Point3d(pos)), 1)
p3d = p4d[Vec(1, 2, 3)] / p4d[4]
itf = inverse_transform(transform_func(plot))
out = Makie.apply_transform(itf, p3d, to_value(get(plot, :space, :data)))
return out
return out
end
end

Expand All @@ -321,7 +338,7 @@ function position_on_plot(plot::Union{Heatmap, Image}, idx, ray::Ray; apply_tran
end

function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true)
positions = decompose(Point3, plot.mesh[])
positions = decompose(Point3d, plot.mesh[])
ray = transform(inv(plot.model[]), inv_f32_convert(plot, ray))
tf = transform_func(plot)
space = to_value(get(plot, :space, :data))
Expand Down Expand Up @@ -418,7 +435,7 @@ function position_on_plot(plot::Volume, idx, ray::Ray; apply_transform = true)
tf = transform_func(plot)

if tf === nothing

ray = transform(inv(plot.model[]), ray)
pos = ray_rect_intersection(Rect3(min, max .- min), ray)
if apply_transform
Expand All @@ -433,7 +450,7 @@ function position_on_plot(plot::Volume, idx, ray::Ray; apply_transform = true)
w = max - min
ps = Point3d[min + (x, y, z) .* w for x in (0, 1) for y in (0, 1) for z in (0, 1)]
fs = decompose(GLTriangleFace, QuadFace{Int}[
(1, 2, 4, 3), (7, 8, 6, 5), (5, 6, 2, 1),
(1, 2, 4, 3), (7, 8, 6, 5), (5, 6, 2, 1),
(3, 4, 8, 7), (1, 3, 7, 5), (6, 8, 4, 2)
])

Expand Down
Loading
Loading