Skip to content

Instantly share code, notes, and snippets.

@j-fu
Created December 26, 2020 23:24
Show Gist options
  • Save j-fu/00353f0e612dd188e30c3891a47de6a5 to your computer and use it in GitHub Desktop.
Save j-fu/00353f0e612dd188e30c3891a47de6a5 to your computer and use it in GitHub Desktop.
conceptual design of interactive multiscene handling based on Makie/Makielayout
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