Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 17 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ describe it as available in docs or the skill.
| # | Principle | Gap in 0.3 |
|---|---|---|
| P1 | The GUI sits on BeamletOptics systems and visualizes them. All physics is computed by BeamletOptics. Components, sources and systems can be added to and removed from the scene in the GUI. One view holds several systems with several beams each; an object belongs to any number of systems or to none and is drawn once, a source to at most one, and each system is traced on its own. | `StaticSystem`s can not be changed; only components and sources from the catalog can be changed afterwards (page "Edit"); `export_script` writes the constructors of those and of the objects added with `code` (`add_component!`), since the constructor of an object from the user's script is not known otherwise; the glasses of the catalog (`catalog_glasses`) are data of the GUI until BeamletOptics has a glass catalog |
| P2 | The layout is built from widgets, one per BeamletOptics type (e.g. the card of a lens, of a detector, of a system). Widgets without a type are generic windows (`add_panel!`, `add_controls!`, `add_tool!`). | the pages of a card and its view ("Results") are chosen per type by internal traits (`_has_page`, `_has_view`), only `Detector` has a view; tree icons per type are internal |
| P2 | The layout is built from widgets, one per BeamletOptics type (e.g. the card of a lens, of a detector, of a system). Widgets without a type are generic windows (`add_panel!`, `add_controls!`, `add_tool!`). | the pages of a card and its view ("Results") are chosen per type by internal traits (`_has_page`; the view from `card_views`, `_has_view`); tree icons per type are internal |
| P3 | Any layout can be assembled from the widgets and presented to the user. Layouts are composed in code from public widget blocks; `:compact` and `:app` are two such compositions. | the layout interface `AbstractLiveLayout` is internal, the widgets have no public constructors |
| P4 | An extensible API lets users with own BeamletOptics types define widgets that the GUI loads (`card_rows`, `card_actions`, `CardWidget`, `card_input`, `card_show!`; a component package adds them in a package extension on BeamletOpticsGUI). | own views (the page "Results") and icons per type |
| P4 | An extensible API lets users with own BeamletOptics types define widgets that the GUI loads (`card_rows`, `card_actions`, `CardWidget`, `card_input`, `card_show!`; a component package adds them in a package extension on BeamletOpticsGUI). | icons per type |

**Rules that follow from them:**

Expand All @@ -42,6 +42,14 @@ describe it as available in docs or the skill.
`initialize!`, never by the GUI (no `empty!` of a detector): once for all systems before their
sources are solved (`_compute`), since the sources superpose on a detector, and for a component
that is removed.
- **The GUI displays results, it does not compute them.** Results come from BMO or from the
user's own calculations, which the GUI calls and draws. The GUI computes only lightweight
helpers for showing them, such as a centroid or an RMS radius to place a marker, a cut line,
limits or a normalization. Such a helper takes data that BMO returned, is closed-form (sums,
moments, extrema, scaling; no tracing, no integration with the weights of a sampling, no fits),
is marked as a display helper where it is defined, and is never returned by public API: no
widget or view hands results to the user. A value that a user would want in a script belongs in
BMO.
- **A solve in the background owns the beams and detectors**, from its start until its result is
shown or it is cancelled. Nothing else changes or reads them meanwhile: changes of objects go
through `_change!`, which cancels the solve first, and code that reads the beams or the hits
Expand Down Expand Up @@ -83,10 +91,13 @@ describe it as available in docs or the skill.
one of them again as it is and place it with the mouse. Adding, removing and changing are actions of the undo
history of the controls (`_push_action!` in `src/LiveInteraction.jl`, `_record_added!` in
`src/LiveComponents.jl`).
- `src/LiveDetectorView.jl`: the view of a detector on the page "Results" of its card: the kinds
of views per type of hits (spot diagram, PSF, intensity), their results and metrics, and the
widget (thumbnail, zoomable plot). `src/LiveDetectors.jl`: the state of the view per detector,
its computation only while it is shown, and the `detectors` kwarg.
- `src/LiveCardViews.jl`: `CardView`, `card_views` and the built-in views of a `Detector` (spot
diagram, PSF, intensity) as `card_views(::Detector)`. `src/LiveDetectorView.jl`: the view of the
page "Results" of a card: the drawing per form (`:points`, `:field`, `:series`), their results and
display metrics, and the widget (thumbnail, zoomable plot). `src/LiveDetectors.jl`: the state of
the view per object with views, its computation only while it is shown, the recording of the
series after every full solve, and the `detectors` kwarg. `src/LiveResultsView.jl`:
`results_view!`, `ResultsView`, `refresh!`: the widget of the page "Results" in a figure of its own.
- `src/LiveTrace.jl`, `src/LiveProgress.jl`: solving, progress window. `src/LiveSystemTrace.jl`:
tracing per system (`_system_auto`, `_set_system_auto!`, `_trace_system!`, `_trace_set`: systems
that share a `Detector` are traced together, the outdated beams of a system). `src/LiveClip.jl`, `src/LiveMeasure.jl`, `src/LiveCamera.jl`,
Expand Down
12 changes: 12 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ card_input
card_show!
```

## Results views

The views of the page "Results" of a card and their placement in a figure of your own.

```@docs
CardView
card_views
results_view!
ResultsView
refresh!
```

## Agent skill

```@docs
Expand Down
50 changes: 50 additions & 0 deletions docs/src/assets/examples/live_card_views.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using BeamletOptics, BeamletOpticsGUI, GLMakie

# An own component type: a screen that absorbs the rays and keeps the points where they hit it
# (x, z relative to its center [m], valid while the screen faces the beam along y without rotation)
struct Screen{T} <: BeamletOptics.AbstractObject{T}
shape::BeamletOptics.PlanoSurfaceSDF{T}
hits::Vector{Point2{Float64}}
lock::ReentrantLock
end

Screen(width, thickness) = Screen(BeamletOptics.PlanoSurfaceSDF(thickness, width),
Point2{Float64}[], ReentrantLock())

# Called for every ray that hits the screen (the solve may use several threads)
function BeamletOptics.interact3d(::BeamletOptics.AbstractSystem, s::Screen, ::Beam, ray::Ray)
p = position(ray) + length(ray) * direction(ray) - position(s)
lock(() -> push!(s.hits, Point2(p[1], p[3])), s.lock)
return nothing
end

# The hits of the last solve are discarded before the next one
BeamletOptics.initialize!(s::Screen) = (lock(() -> empty!(s.hits), s.lock); nothing)

# The functions of the views: they only read the screen and return what is shown
hit_count(s::Screen) = length(s.hits)
hit_points(s::Screen) = copy(s.hits)

# The page "Results" of the card of a screen: the first view is shown first. `:series` records the
# count after each full solve and draws it over the solves, `:points` draws a spot diagram.
BeamletOpticsGUI.card_views(::Screen) = (
CardView(hit_count, :series; name = :count, label = "Count", unit = "rays",
text = (s, n) -> "$(Int(n)) rays hit the screen"),
CardView(hit_points, :points; name = :spot, label = "Hits"))

# A narrow beam and a screen of 10 mm width behind it
beam = CollimatedSource([0.0, 0, 0], [0.0, 1, 0], 4e-3, 1e-6; num_rings = 2, num_rays = 40)
screen = Screen(10e-3, 2e-3)
translate3d!(screen, [0.0, 0.1, 0.0])
system = System([screen])

# Select the screen and drag it sideways: the count over the solves and the spots follow. A script
# gets the same numbers by calling `hit_count(screen)` after `solve_system!`.
gui = live_view(system, beam; labels = Dict(screen => "screen"))

# Open the interactive window when used from the REPL or run as a script
if isinteractive()
display(gui)
elseif abspath(PROGRAM_FILE) == @__FILE__
wait(display(gui))
end
31 changes: 25 additions & 6 deletions docs/src/live_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,10 @@ its visible part.

The results of a detector are on its card. Every card has pages, chosen by a page bar below its head: "Pose" (the rows of
[`card_rows`](@ref); on the card of the selection also the keyboard step and the Move/Rotate mode)
and "Properties" (the property list of the object, see `properties`). The card of a `Detector`
has a third page, "Results", between them, which shows the detector view. The card of a source (a
and "Properties" (the property list of the object, see `properties`). The card of an object with
[`card_views`](@ref) has a third page, "Results", shown first, which shows the view of the object, e.g. the
detector view of a `Detector`; an own type adds its views the same way, see
[Own results on a card](@ref). The card of a source (a
beam, a beam group or a Gaussian beamlet) has the page "Color": a menu sets the color in which it is
drawn ("wavelength" draws every ray in the display color of its own wavelength (`wavelength_color`
of BeamletOptics), so the beams of a group with several wavelengths, or the beams behind a
Expand All @@ -245,8 +247,8 @@ is traced again, and they are not part of [`export_changes`](@ref). In a script,
`beam_kwargs = Dict(source => (; color = :orange, alpha = 0.5, linewidth = 2))` sets them from the
start; `color = :wavelength` (or `(:wavelength, alpha)`) is the default mode. If a long solve runs
in the background while the color or the length changes, that solve starts again. A card with a single
page, e.g. of an inspected point or a measurement, has no page bar. A card opens on "Results" for a
detector and on "Pose" for every other object; a pinned card keeps its page.
page, e.g. of an inspected point or a measurement, has no page bar. A card opens on "Results" for an
object with views and on "Pose" for every other object; a pinned card keeps its page.

The view is expanded by default. Its chevron collapses it to a thumbnail (92 px) with the kind,
the key value (the power or the number of rays), the centroid and the radii; a click on the
Expand All @@ -268,13 +270,20 @@ peak) and at the powers of ten with "log". The image is scaled to its maximum af
i.e. it looks the same while the intensity changes; the ticks of the bar show the change. Use
`colorrange` for a fixed scale.

The kinds depend on the hits, the first is the default:
The kinds are the [`CardView`](@ref)s of [`card_views`](@ref) for a `Detector` (the same API as for an
own type, see [Own results on a card](@ref)), and depend on the hits; the first is the default. With
the kind `:auto`, a view whose function throws gives way to the next one. A `Detector` keeps its page
"Results" without hits, where it reads "no hits":

| Hits | Kinds |
|:-----|:------|
| rays (`RayHit`, `PolarizedRayHit`) | "Spot" (spot diagram), "PSF" (the `intensity` of the rays, which BeamletOptics returns unscaled, hence shown normalized to its peak, with the metrics without the power) |
| Gaussian beamlets (`GaussianBeamletHit`, `AstigmaticGaussianBeamletHit`) | "Intensity" (in W/m², with the power), "Spot" (the 1/e² outlines) |

The power `P` in the line below the intensity is integrated by BeamletOptics (`optical_power` of the
samples and the intensity of the view, in the BeamletOptics release that this version of the GUI
requires); the GUI computes no power itself.

A chosen kind that the current hits do not offer falls back to their default and applies again
when they offer it. Without hits, the view reads "no hits". Moving a detector (or a group that
contains it) empties it (BeamletOptics 0.14): with auto trace off, its view reads "no hits" until
Expand Down Expand Up @@ -303,7 +312,8 @@ computations slower than `trace_budget` show a coarse preview first.
Only shown views are computed: the page "Results" of the card of the selected detector or of a
pinned card that is not collapsed. A thumbnail is computed on a grid of at most 48 points per
axis. A view that is shown later is computed from the hits of the last solve. The spectator mode
hides the cards and with them the views.
hides the cards and with them the views. The exception is a `:series` view of an own type, which is
recorded after every full solve whether or not a card shows it.

The floating card with an expanded view is resizable: a grip at its bottom right corner changes
its size from 160 px up to the size of the 3D view. In the sidebar of the app layout, the view
Expand Down Expand Up @@ -337,11 +347,20 @@ view passes on to it:
| `colorbar = true` | `false` hides the colorbar below the plot of a field |
| `profiles = false` | `true` adds the axis with the intensity along x and z through the centroid |
| `expanded = true` | `false` starts the pinned card with the thumbnail |
| `spot_colors = nothing` | `hit -> color`: the color of each spot of the spot view, a beamlet contour in the color of its hit |

```julia
gui = live_view(system, beam; detectors = [pd => (:intensity, (; colorscale = :log, profiles = true))])
```

```julia
gui = live_view(system, beam; detectors = [pd => (:spot, (; spot_colors = hit -> :red))])
```

The view of a detector can also be placed into a figure of your own with [`results_view!`](@ref),
e.g. in a second window; it takes the same options (`view`, `expanded`, `spot_colors`) and shares them
with the cards of the detector, see [Views in an own figure](@ref).

The option `n` is now `num_points` and throws an `ArgumentError`. The option `history` is no longer
available and throws an `ArgumentError` as well: record the values in
`on_change` and plot them in a panel of [`add_panel!`](@ref), see [Own panels, controls and tools](@ref)
Expand Down
64 changes: 63 additions & 1 deletion docs/src/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ copied unchanged.
| widgets for something in the scene: a component, a group, a source, a system, an own `AbstractObject` subtype | a card | [`card_rows`](@ref), [`card_actions`](@ref) |
| a parameter without a scene object, e.g. a setting of the whole setup | a controls section | [`add_controls!`](@ref) |
| an action, e.g. a button or a toggle with a key | a tool | [`add_tool!`](@ref) |
| a plot or a result that is updated after each solve | a panel | [`add_panel!`](@ref) |
| a result of an object that is updated after each solve (points, a field, a value over the solves) | a view on its card | [`card_views`](@ref) |
| a plot or a result without an object that is updated after each solve | a panel | [`add_panel!`](@ref) |

A card needs a scene object because the object gives the card its anchor (the card is placed next
to the bounding box of the object and follows it), its selection (the card shows what is selected,
Expand Down Expand Up @@ -339,13 +340,69 @@ function add_tilt_controls!(gui, mirror)
end
```

## Own results on a card

A type that computes something worth showing, e.g. the hits of an own detector, gets the page
"Results" of its card with a method of [`card_views`](@ref) that returns [`CardView`](@ref)s. The
GUI only draws: a `CardView` holds a function `f` that computes what is shown from the object, and
a form that says how the window draws it. The window hands no result back; a script that wants the
numbers calls `f` itself. The card of an object with views shows "Results" first, with a switch
between its views (the first is the default); a `Detector` has views of its own, see
[Detector view](@ref).

| Form | `f` returns | Drawn as |
|:--|:--|:--|
| `:points` | `f(obj)`: the points `(x, z)` [m], or `(points, colors)` | spot diagram, centroid cross, number, centroid, RMS and largest radius |
| `:field` | `f(obj; num_points, window)`: `(x, z, I)` | image, colorbar in the `unit`, "log", profiles; zoom calls `f` again with the new `window` |
| `:series` | `f(obj)`: a number | the values of the full solves as a line, the last value, "clear" |

`f` runs in the background task of the solve, reads the object and must not change plots or
observables. An error of `f` is shown in the view. With `preview = false` (the default) `f` is not
called for the previews while an object is dragged, only after the full solve; `preview = true`
also calls it for those. The centroid, the radii and the color scale are computed by the window to
draw the view, they are not results of the view.

A series is recorded by the GUI: one value after each full solve for every object of the systems
that has a `:series` view, also while no card shows it. While an object is dragged nothing is
recorded, the value comes once the movement pauses. The window keeps at most 10 000 values per
object and view, "clear" empties them, removing the object drops them and undo keeps them.

The full [script](https://github.com/StackEnjoyer/BeamletOpticsGUI.jl/blob/main/docs/src/assets/examples/live_card_views.jl)
defines a screen that records its hits, and shows the count as a series and the hits as points:

```julia
BeamletOpticsGUI.card_views(::Screen) = (
CardView(hit_count, :series; name = :count, label = "Count", unit = "rays",
text = (s, n) -> "$(Int(n)) rays hit the screen"),
CardView(hit_points, :points; name = :spot, label = "Hits"))
```

Like the other card functions, a package adds the method in its package extension. To show such a
view in a figure of your own, e.g. next to the window, use [`results_view!`](@ref), see
[Views in an own figure](@ref).

### Views in an own figure

[`results_view!`](@ref) places the view of the page "Results" of an object into the position of a
figure, e.g. in a second window. With the window `gui` it shares the view that is shown, the grid,
the color scale and the zoom with the cards of the object and updates after each solve. Without a
window it computes when it is built, on [`refresh!`](@ref) and on its own widgets (a series shows
only its current value there). `close(view)` removes it.

```julia
fig = Figure()
rv = results_view!(fig[1, 1], gui, screen; view = :spot)
display(GLMakie.Screen(), fig)
```

## Checklist for a new component

- `interact3d(system, object, beam, ray)`: required, see Core design page of BeamletOptics.
- `BeamletOptics.initialize!(object)`: only if `interact3d` stores data in the component, e.g. the
hits of an own detector. It discards them; the live view calls it before every solve.
- `render!`: optional, how the component is drawn.
- `card_rows` (and `card_actions`): optional, its rows and buttons on the card of the live view.
- `card_views`: optional, its views on the page "Results" of the card, see [Own results on a card](@ref).
- `BeamletOptics.properties`: optional, the properties listed on the page "Properties" of the card
and of the inspector, see `properties`.

Expand All @@ -362,6 +419,11 @@ CardRow
CardWidget
card_input
card_show!
CardView
card_views
results_view!
ResultsView
refresh!
add_controls!
add_tool!
```
Loading
Loading