Created
December 26, 2020 23:24
-
-
Save j-fu/00353f0e612dd188e30c3891a47de6a5 to your computer and use it in GitHub Desktop.
conceptual design of interactive multiscene handling based on Makie/Makielayout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module MultiScene | |
# (c) Julius Krummbiegel, Jürgen Fuhrmann | |
using GLMakie | |
############################################################### | |
# | |
# Generic part | |
""" | |
Check if position is within pixel area of scene | |
""" | |
function inscene(scene,pos) | |
area=scene.px_area[] | |
pos[1]>area.origin[1] && | |
pos[1] < area.origin[1]+area.widths[1] && | |
pos[2]>area.origin[2] && | |
pos[2] < area.origin[2]+area.widths[2] | |
end | |
""" | |
Control multiple scene elements via keyboard up/down keys. | |
Each switchkey is assumed to correspond to one of these elements. | |
Pressing a switch key transfers control via up/down resp. page_up/page_down | |
to its associated element. | |
""" | |
function scene_interaction(update_scene,scene,switchkeys::Vector{Symbol}=[:nothing]) | |
mouseposition=Node((0,0)) | |
activeswitch=Node(switchkeys[1]) | |
on(m->mouseposition[]=m, scene.events.mouseposition) | |
on(scene.events.keyboardbuttons) do buttons | |
if inscene(scene,mouseposition[]) | |
for i=1:length(switchkeys) | |
if switchkeys[i]!=:nothing&&ispressed(scene,getproperty(Keyboard,switchkeys[i])) | |
activeswitch[]=switchkeys[i] | |
update_scene(0,switchkeys[i]) | |
return | |
end | |
end | |
if ispressed(scene, Keyboard.up) | |
update_scene(1,activeswitch[]) | |
elseif ispressed(scene, Keyboard.down) | |
update_scene(-1,activeswitch[]) | |
elseif ispressed(scene, Keyboard.page_up) | |
update_scene(10,activeswitch[]) | |
elseif ispressed(scene, Keyboard.page_down) | |
update_scene(-10,activeswitch[]) | |
end | |
end | |
end | |
end | |
""" | |
Combine scene with title header | |
""" | |
textposition(scene)=lift(a->Vec2f0(widths(a)[1] ./ 2 , 0), pixelarea(scene)) | |
header(scene,txt)=text(txt,textsize = 20,raw=true,position=textposition(scene),camera=campixel!,align = (:center, :bottom)) | |
footer(scene,status)=text(lift(a->a,status),textsize = 15,raw=true,position=textposition(scene),camera=campixel!,align = (:center, :bottom)) | |
function annotated_scene(ax,scene;title=" ",status=nothing) | |
if isnothing(status) | |
hbox(scene,header(scene,title),parent=ax.scene) | |
else | |
hbox(footer(scene,status),scene,header(scene,title),parent=ax.scene) | |
end | |
end | |
##################################################### | |
# Handle blocking/unblocking via space key | |
mutable struct Blocker | |
condition::Condition | |
blocked::Bool | |
Blocker(;blocked=false)=new(Condition(),blocked) | |
end | |
# block/unblock, and notify about unblocking | |
function toggle(blocker::Blocker) | |
if blocker.blocked | |
blocker.blocked=false | |
notify(blocker.condition) | |
else | |
blocker.blocked=true | |
end | |
end | |
# | |
# If blocked, wait for notification. | |
# The yield goes here conveniently as well. | |
# | |
function waitblocker(blocker::Blocker) | |
yield() | |
if blocker.blocked | |
wait(blocker.condition) | |
end | |
end | |
# | |
# As we have only one makie window, we also can afford | |
# to have one blocker. | |
# | |
globalblocker=Blocker() | |
waitblocker()=waitblocker(globalblocker) | |
""" | |
Create a scene with given layout grid. Returns an array of subscenes | |
according to the layout parameter. | |
The `,` key switches between focused view showing only one subscene | |
and "gallery view" showing all subscenes at once. | |
""" | |
function multiscene(;layout=(1,1), blocked=false, resolution=(500,500)) | |
(scene, makielayout) = layoutscene(resolution = resolution) | |
offscreen_gl = GridLayout(bbox = BBox(-500, -400, -500, -400)) | |
axs = makielayout[] = [LScene(scene) for _ in CartesianIndices(layout)] | |
gallery_view=Node(true) | |
mouseposition=Node((0,0)) | |
globalblocker.blocked=blocked | |
on(m->mouseposition[]=m, scene.events.mouseposition) | |
function focus(i) | |
for (j, ax) in enumerate(axs) | |
if j != i | |
offscreen_gl[1, 1] = ax | |
else | |
makielayout[1, 1] = ax | |
end | |
end | |
trim!(makielayout) | |
gallery_view[]=false | |
end | |
function show_all() | |
for idx in CartesianIndices(axs) | |
makielayout[Tuple(idx)...]= axs[Tuple(idx)...] | |
end | |
gallery_view[]=true | |
end | |
function child(mouseposition) | |
for i=1:length(scene.children) | |
if inscene(scene.children[i],mouseposition) | |
return i | |
end | |
end | |
return 0 | |
end | |
on(scene.events.keyboardbuttons) do buttons | |
if ispressed(scene, Keyboard.comma) | |
gallery_view[] ? focus(child(mouseposition[])) : show_all() | |
end | |
if ispressed(scene, Keyboard.space) | |
toggle(globalblocker) | |
end | |
end | |
scene,axs | |
end | |
function subscenecontext(ax::LScene; ctx=Dict{Symbol,Any}()) | |
if !haskey(ax.attributes,:subscenecontext) | |
ax.attributes[:subscenecontext]=ctx | |
end | |
ax.attributes[:subscenecontext][] | |
end | |
export multiscene, scene_interaction | |
export subscenecontext,waitblocker | |
export textposition, header, footer, annotated_scene | |
end # Module MultiScene2 | |
###################################################### | |
# | |
# Specific example testing all features. | |
#= | |
Features are up to now | |
- Arrangement of several subscenes with in a given scene grid | |
- ',' key switches between gallery view and focus view showing one subscene | |
- space key can block/unblock main task | |
- handle interactive variables associated with hotkeys via keyboard interaction, | |
separately for each subscene. Rationale: sliders would eat up too much screen real estate | |
- Store context dict in the attributes of LScene | |
=# | |
using .MultiScene | |
using GLMakie | |
# Scene with two internal variables, shown only once | |
function subscene1!(ax) | |
N=10 | |
s=1.0 | |
makedata(s,N)=s*rand(N) | |
makestatus(s,N)="s=$(round(s,digits=2)) N=$(N)" | |
data=Node(makedata(s,N)) | |
status=Node(makestatus(s,N)) | |
scene=lines(lift(a->a,data), color = :red, linewidth = 4) | |
scene_interaction(scene,[:n,:s]) do delta,key | |
# key: which key is "switched on" (of those passed in the array) | |
# delta: increment/decrement value | |
if key==:n | |
N=max(2,N+delta) | |
elseif key==:s | |
s=max(0.1,s+0.01*Float64(delta)) | |
end | |
data[]=makedata(s,N) | |
status[]=makestatus(s,N) | |
update!(scene) | |
end | |
annotated_scene(ax,scene,title="lines",status=status) | |
end | |
# Scene with two internal variables, shown multiple times | |
# as a consequence, data and nodes are stored in a context dict | |
# which is stored in ax.attributes. Not sure if this is a good idea... | |
function subscene2!(ax,k,l) | |
ctx=subscenecontext(ax) | |
N=100 | |
makedata(k,l)=[sinpi(2*k*i/N)*sinpi(2*l*j/N) for i=1:N, j=1:N] | |
makestatus(k,l)="k=$(round(k,digits=2)) l=$(round(k,digits=2))" | |
if isempty(ctx) | |
data=Node(makedata(k,l)) | |
status=Node(makestatus(k,l)) | |
scene=heatmap(lift(a->a,data)) | |
ctx[:data]=data | |
ctx[:status]=status | |
ctx[:k]=k | |
ctx[:l]=l | |
scene_interaction(scene,[:k,:l]) do delta,key | |
ctx[key]=max(0.1,ctx[key]+0.1*delta) | |
status[]=makestatus(ctx[:k], ctx[:l]) | |
data[]=makedata(ctx[:k], ctx[:l]) | |
end | |
annotated_scene(ax,scene,title="heatmap2d",status=status) | |
ctx | |
else | |
ctx[:k]=k | |
ctx[:l]=l | |
ctx[:status][]=makestatus(ctx[:k], ctx[:l]) | |
ctx[:data][]=makedata(ctx[:k], ctx[:l]) | |
waitblocker() | |
ctx | |
end | |
end | |
# simple subscene with one interior variable | |
# so no need to care about switching | |
function subscene3!(ax) | |
N=50 | |
data=Node(rand(N,3)) | |
status=Node("N=$(N)") | |
scene=scatter(lift(a->a,data), color = rand(RGBf0)) | |
scene_interaction(scene) do delta,key | |
N=max(4,N+delta) | |
status[]="N=$(N)" | |
data[]=rand(N,3) | |
end | |
annotated_scene(ax,scene,title="scatter3d",status=status) | |
end | |
# simple subscene with one interior variable | |
function subscene4!(ax) | |
iso=1.7 | |
r = LinRange(-1, 1, 100) | |
cube = [(x.^2 + y.^2 + z.^2) for x = r, y = r, z = r] | |
cubedata=cube .* (cube .> 1.4) | |
data=Node(iso) | |
status=Node("iso=$(round(iso,digits=2))") | |
scene=volume(cubedata, algorithm = :iso, isorange = 0.05, isovalue = lift(a->a,data)) | |
scene_interaction(scene) do delta,key | |
iso=min(2.5,max(1.5,iso+0.05*delta)) | |
data[]=iso | |
status[]="iso=$(round(iso,digits=2))" | |
end | |
annotated_scene(ax,scene,title="cube3d",status=status) | |
end | |
function run() | |
parent,subscenes=multiscene(layout=(2,2)) | |
subscene1!(subscenes[1,1]) | |
subscene2!(subscenes[1,2],1.9,3.5) | |
subscene3!(subscenes[2,1]) | |
subscene4!(subscenes[2,2]) | |
display(parent) | |
# this loop runs "forever" and can be temporarily | |
# stopped by the space key | |
k=2.0 | |
l=2.0 | |
dir=1.0 | |
while true | |
for i=1:1000 | |
k+=dir*0.01 | |
l+=dir*0.01 | |
subscene2!(subscenes[1,2],k,l) | |
end | |
dir=-dir | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment