diff --git a/docs/src/projects.md b/docs/src/projects.md index 69b0bb0..87f3c43 100644 --- a/docs/src/projects.md +++ b/docs/src/projects.md @@ -82,7 +82,8 @@ Several windows and agents can work on one project at once, also from a shared d changed what and when, in one line, or, after a click, in a lane per participant. The mouse over a mark shows the change again, a click replays it, a double click frames its objects, dragging along the lanes shows ghosts of how the setup was at that time, and the eye of a lane makes the - camera follow that participant. + camera follow that participant. Framing shows the objects with their surroundings; "‹ back" in + the head of the timeline returns to the view before the first framing. - **Claims.** The first gesture on an object claims it for the person, until it is deselected or 30 s pass without a gesture. A gesture on an object that another participant claims is refused, the status line names the holder ("Bob is editing M1"), and the object shows a lock. A change diff --git a/ext/BeamletOpticsGUIGLMakieExt/session.jl b/ext/BeamletOpticsGUIGLMakieExt/session.jl index 20d61bd..989d17a 100644 --- a/ext/BeamletOpticsGUIGLMakieExt/session.jl +++ b/ext/BeamletOpticsGUIGLMakieExt/session.jl @@ -1408,6 +1408,16 @@ function _project_session(step; dir = mktempdir()) _click!(gui) GUI._activity_tick!(gui) frame!() + # the framing of the double click (without its timing), then "‹ back" in the head + GUI._frame_points!(gui, GUI._tl_points(a, last(m[3]))) + GUI._update_timeline!(gui, time()) + frame!() + back = hit(:back) + _check(!isnothing(back), "the framing shows no button back") + _click!(gui, center(back[1])) + GUI._activity_tick!(gui) + frame!() + _check(isnothing(tl.back), "the button back did not return to the view before") lanes = hit(:lanes)[1] y = minimum(lanes)[2] + widths(lanes)[2] / 2 _drag!(gui, (minimum(lanes)[1] + 10, y), (maximum(lanes)[1] - 10, y)) diff --git a/src/LiveTimeline.jl b/src/LiveTimeline.jl index 3515209..3c49631 100644 --- a/src/LiveTimeline.jl +++ b/src/LiveTimeline.jl @@ -15,6 +15,11 @@ const _TL_EXPANDED = 110.0f0 # Expanded: the head (chevron, title, span), the height of a lane, the foot (the labels of the time # axis), the column of the lanes' names, the lanes shown at most const _TL_HEAD = 24.0f0 +# the framing of an activity: its objects take about a third of the view, at least 0.1 m across, +# see `_frame_points!`; the text of the button back to the view before +const _TL_FRAME_FACTOR = 3.0 +const _TL_FRAME_MIN = 0.1 +const _TL_BACK = "‹ back" const _TL_LANE = 22.0f0 const _TL_FOOT = 18.0f0 const _TL_LABELS = 150.0f0 @@ -84,6 +89,8 @@ mutable struct _Timeline hover_own::Bool sig::Any const widths::Dict{Tuple{String, Bool, Float32}, Float32} + # the view before the first framing of an activity, to which "back" returns (see `_frame_points!`) + back::Any end """ @@ -139,7 +146,7 @@ function _timeline!(gui::LiveView) tip_box, tip_text) tl = _Timeline(scene, t, _tree_font(scene, :regular), _tree_font(scene, :bold), plots, Rect2f(0, 0, 0, 0), false, _TL_SPAN, time(), Tuple{Rect2f, Symbol, Any}[], (0.0f0, 0.0f0), - nothing, (0, -Inf), false, nothing, Dict{Tuple{String, Bool, Float32}, Float32}()) + nothing, (0, -Inf), false, nothing, Dict{Tuple{String, Bool, Float32}, Float32}(), nothing) a.strip = tl _connect_timeline!(gui, tl) _update_timeline!(gui, time()) @@ -178,7 +185,7 @@ function _update_timeline!(gui::LiveView, now::Real) tick = a.expanded ? floor(Int, now / (tl.span / max(_tl_axis_width(rect), 1))) : floor(Int, now / 1) last = a.episodes[end] sig = (rect, a.expanded, length(a.episodes), last.n, tl.span, tick, a.hover, a.cursor, a.follow, - length(a.colors)) + length(a.colors), isnothing(tl.back)) sig == tl.sig && return nothing tl.sig = sig _draw_timeline!(gui, tl, a) @@ -494,10 +501,14 @@ function _draw_expanded!(gui::LiveView, tl::_Timeline, a::_Activity) end hy = Float32(top - _TL_HEAD / 2) label!(Point2f(left + 18, hy), "Activity", muted, :left) - if isnothing(a.cursor) - label!(Point2f(ax1, hy), "last " * _tl_span_label(span), muted, :right) - else - label!(Point2f(ax1, hy), _tl_clock(a.cursor), _rgba(t.accent), :right) + clock = isnothing(a.cursor) ? "last " * _tl_span_label(span) : _tl_clock(a.cursor) + label!(Point2f(ax1, hy), clock, isnothing(a.cursor) ? muted : _rgba(t.accent), :right) + # back to the view before the framing of an activity, see `_frame_points!` + if !isnothing(tl.back) + xb = ax1 - _tl_width(tl, clock, false, _TL_SMALL) - 16 + wb = _tl_width(tl, _TL_BACK, false, _TL_SMALL) + label!(Point2f(xb, hy), _TL_BACK, _rgba(t.accent), :right) + push!(tl.hits, (Rect2f(xb - wb - 4, top - _TL_HEAD, wb + 8, _TL_HEAD), :back, nothing)) end push!(tl.hits, (Rect2f(x0, top - _TL_HEAD, W, _TL_HEAD), :toggle, nothing)) # the grid: the baselines of the lanes and a faint line per tick @@ -741,24 +752,46 @@ end """ _frame_points!(gui, pts) -Moves the camera of the `gui`, animated, such that the bounding sphere of the points `pts` fills the -view, along the current view direction, like `_zoom_to_selection!`; nothing without points. +Moves the camera of the `gui`, animated, along the current view direction such that the points +`pts` take about a third of the view with their surroundings: the bounding sphere of +`_TL_FRAME_FACTOR` times their box, at least `_TL_FRAME_MIN` across, fills the view. The view before +the first framing is kept for "back" in the head of the timeline, see `_frame_back!`. Nothing +without points. """ function _frame_points!(gui::LiveView, pts::AbstractVector{<:Point3}) pts = [p for p in pts if all(isfinite, p)] isempty(pts) && return nothing bb = GeometryBasics.Rect3d(pts) center = Vector{Float64}(minimum(bb) .+ GeometryBasics.widths(bb) ./ 2) - # a single point or a flat box: at least 2 cm - d = max(norm(Vector{Float64}(GeometryBasics.widths(bb))), 0.02) + # the objects with their surroundings, also a single point or a flat box + d = max(_TL_FRAME_FACTOR * norm(Vector{Float64}(GeometryBasics.widths(bb))), _TL_FRAME_MIN) cam = cameracontrols(gui.ax.scene) dist = cam.settings.projectiontype[] == Makie.Perspective ? (d / 2) / sind(cam.fov[] / 2) : d / 2 eye, lookat, up = _current_view(gui) + tl = _timeline(gui) + (isnothing(tl) || !isnothing(tl.back)) || (tl.back = (eye, lookat, up)) o = norm(eye .- lookat) > 0 ? normalize(eye .- lookat) : [0.0, 0.0, 1.0] _animate_camera!(gui, center .+ dist .* o, center, up) return nothing end +""" + _frame_back!(gui) + +"back" in the head of the timeline of the `gui`: ends following a participant and moves the camera, +animated, to the view before the first framing of an activity (see `_frame_points!`), which is +forgotten then. Nothing if there was no framing. +""" +function _frame_back!(gui::LiveView) + tl = _timeline(gui) + (isnothing(tl) || isnothing(tl.back)) && return nothing + eye, lookat, up = tl.back + tl.back = nothing + _set_follow!(gui, nothing) + _animate_camera!(gui, eye, lookat, up) + return nothing +end + # The points of the objects of the episode `n` of the activity `a`: their frames, else their poses function _tl_points(a::_Activity, n::Int) i = findfirst(ep -> ep.n == n, a.episodes) @@ -803,7 +836,7 @@ function _connect_timeline!(gui::LiveView, tl::_Timeline) h = if event.button != Mouse.left nothing elseif a.expanded - _tl_hit(tl, p, :toggle, :eye, :mark, :lanes) + _tl_hit(tl, p, :back, :toggle, :eye, :mark, :lanes) else # the collapsed line takes the click as a whole _tl_hit(tl, p, :toggle) @@ -823,7 +856,10 @@ function _connect_timeline!(gui::LiveView, tl::_Timeline) # the release must be over what was pressed h = tl.shown ? _tl_hit(tl, p, kind) : nothing (isnothing(h) || h[3] != data) && return Consume(true) - if kind === :toggle + if kind === :back + _frame_back!(gui) + _update_timeline!(gui, tl.now) + elseif kind === :toggle _set_expanded!(gui, !a.expanded) elseif kind === :eye _set_follow!(gui, a.follow == data ? nothing : data) diff --git a/test/TestLiveTimeline.jl b/test/TestLiveTimeline.jl index 7474efe..e192406 100644 --- a/test/TestLiveTimeline.jl +++ b/test/TestLiveTimeline.jl @@ -2,6 +2,7 @@ module TestLiveTimeline using GLMakie, BeamletOptics, BeamletOpticsGUI using Makie +using LinearAlgebra: norm using Test const GUI = BeamletOpticsGUI @@ -139,11 +140,25 @@ const GUI = BeamletOpticsGUI click!(gui, center(marks[k][1])) @test any(c -> c[1].n == 2, a.cues) @test gui.controls.selected[] === lens - # a double click frames its objects + # a double click frames its objects with their surroundings: three times their box, at + # least 0.1 m across, fills the view @test isnothing(gui.camera.animation) + before = eye_of(gui) click!(gui, center(marks[k][1])) @test !isnothing(gui.camera.animation) @test gui.camera.animation.lookat1 ≈ [0.0, 0.11, 0.0] atol = 1.0e-6 + diag = norm(Makie.widths(Rect3d(GUI._tl_points(a, 2)))) + d = max(GUI._TL_FRAME_FACTOR * diag, GUI._TL_FRAME_MIN) + @test norm(gui.camera.animation.eye1 - gui.camera.animation.lookat1) ≈ + (d / 2) / sind(cameracontrols(gui.ax.scene).fov[] / 2) rtol = 1.0e-6 + gui.camera.animation = nothing + # "‹ back" in the head returns to the view before the framing + GUI._update_timeline!(gui, now) + click!(gui, center(only(hits(tl, :back))[1])) + @test gui.camera.animation.eye1 ≈ before + @test isnothing(tl.back) && isnothing(a.follow) + GUI._update_timeline!(gui, now) + @test isempty(hits(tl, :back)) gui.camera.animation = nothing eye0 = eye_of(gui)