diff --git a/docs/src/components.md b/docs/src/components.md index 8179abc..2b1b950 100644 --- a/docs/src/components.md +++ b/docs/src/components.md @@ -103,7 +103,7 @@ takes numbers and glasses: | Beamsplitters | Thin beamsplitter, Round thin beamsplitter, Plate beamsplitter, Round plate beamsplitter, Cube beamsplitter, Compensator plate | | Prisms | Right-angle prism | | Polarizers | Polarization filter, Round filter, Linear polarizer | -| Detectors | Detector | +| Detectors | Detector, with the switch "stop beams": off, the beams pass it unchanged (`Detector(edge_length, false)`) and reach the components behind it | The icons at the top of the catalog select the group, the tiles below the entry. The boxes of the form take its numbers; the other arguments of the constructor keep their defaults, e.g. the diff --git a/ext/BeamletOpticsGUIGLMakieExt/session.jl b/ext/BeamletOpticsGUIGLMakieExt/session.jl index b728508..f75387b 100644 --- a/ext/BeamletOpticsGUIGLMakieExt/session.jl +++ b/ext/BeamletOpticsGUIGLMakieExt/session.jl @@ -806,6 +806,25 @@ function _session(step, layout::Symbol, fixture::_Fixture = _fixture()) # the form of a lens with surfaces: the last one as an asphere, whose fields come with the next # frame; the lens is placed with the button of the window, which closes when it is dropped aspheric = Ref{Any}(nothing) + # the form of an entry with a switch, e.g. the detector: a click switches it + step("catalog: switch") do + widget = GUI._catalog_widget(GUI._catalog_window(gui)) + i = findfirst(e -> any(p -> p isa GUI.CatalogToggle, e.params), widget.entries) + isnothing(i) && return nothing + k = findfirst(==(widget.entries[i].group), widget.groups) + _click!(gui, _rect_center(widget.group_buttons[k].box)) + frame!() + j = findfirst(tile -> first(tile) == i, widget.tile_buttons) + _check(!isnothing(j), "the click did not choose the group of the entry with a switch") + _click!(gui, _rect_center(last(widget.tile_buttons[j]).box)) + frame!() + _check(widget.entry == i, "the click did not choose the entry with a switch") + s = findfirst(p -> p isa GUI.CatalogToggle, widget.entries[i].params) + before = GUI._catalog_strings(widget)[s] + _click!(gui, _rect_center(widget.inputs[s])) + frame!() + _check(GUI._catalog_strings(widget)[s] != before, "the click did not switch the toggle") + end step("catalog: aspheric surface") do widget = GUI._catalog_widget(GUI._catalog_window(gui)) i = findfirst(e -> any(p -> p isa GUI.CatalogSurface, e.params), widget.entries) @@ -1121,6 +1140,24 @@ function _catalog_session(step, entries = component_catalog()) settle!() end end + # a switch on the page "Edit", e.g. the detector that lets the beams pass, then "Apply" + s = findfirst(p -> p isa GUI.CatalogToggle, entry.params) + if !isnothing(s) + step("catalog, $(entry.name): edit switch + apply") do + gui.controls.selected[] === obj || select!(gui, obj) + frame!() + card = last(filter(c -> GUI._card_object(gui, c) === obj, GUI._edit_cards(gui))) + GUI._set_page!(gui, card, :edit) + frame!() + _click!(gui, _rect_center(GUI._card_widget(card, GUI._edit_toggle(s)))) + frame!() + notify(GUI._card_widget(card, :edit_apply).clicks) + settle!() + # the new object takes the place of the old one and is selected + _check(gui.controls.selected[] !== obj, "the $(entry.name) was not built again") + obj = gui.controls.selected[] + end + end step("catalog, $(entry.name): delete") do gui.controls.selected[] === obj || select!(gui, obj) _key!(gui, Keyboard.delete) diff --git a/skills/beamletopticsgui/API.md b/skills/beamletopticsgui/API.md index 03f51b6..4372aa6 100644 --- a/skills/beamletopticsgui/API.md +++ b/skills/beamletopticsgui/API.md @@ -139,7 +139,9 @@ a radius [mm, sign of BeamletOptics, `Inf` for a plane], and for an even asphere constant `k` and the coefficients A4 to A16 in mm^(1 - order) as on a data sheet (A2 is zero). A lens with an asphere is built and exported as the `Lens`, `DoubletLens` or `TripletLens` of its surfaces, e.g. `Lens(SphericalSurface(r, d), EvenAsphericalSurface(r, d, k, [0.0, A4, ...]), -thickness, n)`; a cemented surface belongs to both elements that it joins. "Place" +thickness, n)`; a cemented surface belongs to both elements that it joins. The detector has a +switch "stop beams" (on by default); off, the beams pass it unchanged (`Detector(edge_length, +false)`) and reach the components behind it. "Place" attaches the chosen component to the mouse, a click drops it, `Esc` cancels, `Delete` removes the selected component or source. It moves in the plane of the table (perpendicular to `rotation_axis`, at the height of the source of its system) and snaps onto beams while `snap` is on; the first component or source of diff --git a/src/API.jl b/src/API.jl index feea984..691253f 100644 --- a/src/API.jl +++ b/src/API.jl @@ -655,6 +655,22 @@ struct CatalogSurface radius::Float64 end +""" + CatalogToggle(name, default::Bool; keyword = nothing) + +A switch of an entry of the built-in catalog (internal): the catalog and the page "Edit" show a +`Toggle`, and the constructor gets its state as a `Bool`, e.g. the `stop` of a `Detector`. Its text +(see `_catalog_value`) is `"true"` or `"false"`. `name` and `keyword` as for [`CatalogParam`](@ref). +""" +struct CatalogToggle + name::String + default::Bool + keyword::Union{Nothing, Symbol} +end + +CatalogToggle(name::AbstractString, default::Bool; keyword::Union{Nothing, Symbol} = nothing) = + CatalogToggle(String(name), default, keyword) + """ CatalogEntry(name, constructor; group = "Components", params = [], code_name = string(nameof(constructor)), icon = nothing, source = false) @@ -692,7 +708,7 @@ struct CatalogEntry name::String group::String constructor::Any - params::Vector{Union{CatalogParam, CatalogGlass, CatalogSurface}} + params::Vector{Union{CatalogParam, CatalogGlass, CatalogSurface, CatalogToggle}} code_name::String icon::Union{Nothing, Symbol, Makie.BezierPath} source::Bool @@ -704,7 +720,7 @@ function CatalogEntry(name::AbstractString, constructor; group::AbstractString = # an unknown name throws icon isa Symbol && _icon(icon) return CatalogEntry(String(name), String(group), constructor, - Union{CatalogParam, CatalogGlass, CatalogSurface}[params...], String(code_name), icon, source) + Union{CatalogParam, CatalogGlass, CatalogSurface, CatalogToggle}[params...], String(code_name), icon, source) end """ diff --git a/src/LiveCards.jl b/src/LiveCards.jl index e8045f8..21cf8e6 100644 --- a/src/LiveCards.jl +++ b/src/LiveCards.jl @@ -1196,6 +1196,29 @@ _floating_cards(gui::LiveView, @nospecialize(obj)) = filter(c -> c.pinned && c.o """Returns `true` if a pinned card of the `gui` floats next to `obj` in the 3D view.""" _is_floating(gui::LiveView, @nospecialize(obj)) = any(c -> c.pinned && c.obj === obj, gui.cards.all) +""" + _take_pins!(gui, old) -> pins + _give_pins!(gui, pins, new) + +Hand the cards pinned to `old` over to `new`, which takes its place, see `_replace!`: +`_take_pins!` detaches them from `old` before it is removed, which would unpin them, and +`_give_pins!` pins them to `new` once it is in the view. A floating card keeps its spot, its page, +if `new` has it, and whether it is collapsed; until then it is pinned to no object and hidden. A +layout that docks pinned cards hands those over as well, see `_take_pins!(::AppView, old)`. +""" +function _take_pins!(gui::LiveView, @nospecialize(old)) + floating = _floating_cards(gui, old) + foreach(c -> c.obj = nothing, floating) + return (; floating, docked = ()) +end + +function _give_pins!(gui::LiveView, pins, @nospecialize(new)) + # the card builds its rows for `new`, see `_update_card!` + foreach(c -> (c.obj, c.key, c.pose) = (new, nothing, nothing), pins.floating) + isempty(pins.floating) || _update_cards!(gui) + return nothing +end + """ _float!(gui, obj) _dock!(gui, obj) diff --git a/src/LiveCatalog.jl b/src/LiveCatalog.jl index 94ed468..dfa9c99 100644 --- a/src/LiveCatalog.jl +++ b/src/LiveCatalog.jl @@ -17,6 +17,7 @@ The default of the parameter `p` as a value, see `_catalog_args`: the number of """ _catalog_default(p::CatalogParam) = p.default _catalog_default(p::CatalogGlass) = p.default == _GLASS_CONSTANT ? p.n : p.default +_catalog_default(p::CatalogToggle) = p.default """ The argument of the constructor for the value `v` of the parameter `p`: the number (an `Int` for a @@ -26,11 +27,13 @@ index `v`, a `ConstantRefractiveIndex`. _catalog_arg(p::CatalogParam, v::Real) = p.integer ? Int(v) : Float64(v) _catalog_arg(::CatalogGlass, v::AbstractString) = _glass(v) _catalog_arg(::CatalogGlass, v::Real) = ConstantRefractiveIndex(Float64(v)) +_catalog_arg(::CatalogToggle, v::Bool) = v """The argument of the constructor for the value `v` of the parameter `p` as Julia code, see `_catalog_arg`.""" _catalog_arg_code(p::CatalogParam, v::Real) = repr(_catalog_arg(p, v)) _catalog_arg_code(::CatalogGlass, v::AbstractString) = _glass_code(_glass(v)) _catalog_arg_code(p::CatalogGlass, v::Real) = _glass_code(_catalog_arg(p, v)) +_catalog_arg_code(::CatalogToggle, v::Bool) = repr(v) """ _catalog_args([f,] entry, values) -> (positional, keywords) @@ -56,7 +59,7 @@ end _catalog_args(entry::CatalogEntry, values) = _catalog_args(_catalog_arg, entry, values) # The keyword as which the parameter `p` is passed, `nothing` for a positional argument -_catalog_keyword(p::Union{CatalogParam, CatalogGlass}) = p.keyword +_catalog_keyword(p::Union{CatalogParam, CatalogGlass, CatalogToggle}) = p.keyword """ _catalog_object(entry, values) -> Union{ObjectOrGroup, AbstractBeam, AbstractBeamGroup} @@ -125,6 +128,7 @@ glass or its constant refractive index. """ _catalog_string(p::CatalogParam) = _catalog_number_string(p.default / p.scale) _catalog_string(p::CatalogGlass) = p.default == _GLASS_CONSTANT ? _catalog_number_string(p.n) : p.default +_catalog_string(p::CatalogToggle) = string(p.default) """ _catalog_value(p, s) -> Union{Float64, String} @@ -139,6 +143,9 @@ whole number. A [`CatalogGlass`](@ref) gives the name of the glass if `s` is one of [`catalog_glasses`](@ref), otherwise the constant refractive index that `s` holds, `p.n` for an empty box. +A `CatalogToggle` gives `true` for `"true"` and `false` for `"false"`, its default for an empty +text. + Throws an `ArgumentError` for a text that is neither. """ function _catalog_value(p::CatalogParam, s::AbstractString) @@ -165,6 +172,14 @@ function _catalog_value(p::CatalogGlass, s::AbstractString) return x end +function _catalog_value(p::CatalogToggle, s::AbstractString) + text = strip(s) + isempty(text) && return p.default + v = tryparse(Bool, text) + isnothing(v) && throw(ArgumentError("invalid input \"$text\" for $(p.name), enter true or false")) + return v +end + """ _catalog_component(entry, strings) -> (; obj, origin) @@ -286,7 +301,8 @@ theme tokens `theme`, from top to bottom: `Textbox` per number, per glass its menu and, for "constant", the box of the refractive index, as `(; menu, box)`, and per surface its menu, the box of its radius and, for "aspheric", the boxes of its conic constant and of its coefficients, as - `(; menu, radius, conic, coefficients)`, see `_fill_catalog_surfaces!`. `boxes` and `menus` hold + `(; menu, radius, conic, coefficients)`, see `_fill_catalog_surfaces!`, and a `Toggle` per + switch (`CatalogToggle`). `boxes` and `menus` hold all of them; the `menus` also hold the menus of the presets of the numbers, which set their boxes, see `_catalog_preset_menu!`. - `place`: the button "Place" @@ -332,6 +348,7 @@ function _catalog_input_string(p::CatalogGlass, input::NamedTuple) # "constant" was chosen and its box is not built yet, see `_flush_catalog_form!` return isnothing(input.box) ? _catalog_number_string(p.n) : String(input.box.displayed_string[]) end +_catalog_input_string(::CatalogToggle, input::Toggle) = string(input.active[]) # The text of a surface from its menu and its boxes, see `_surface_string` function _catalog_input_string(::CatalogSurface, input::NamedTuple) @@ -851,8 +868,9 @@ its inputs (see `_catalog_strings`): first the surfaces (see `_fill_catalog_surf numbers, each with its name, a `Textbox` and its unit, in two columns from the number of its style on (see `_catalog_style`), then the glasses, each with its name and the menu of the glasses and "constant", for which a box takes the refractive index, right of the menu or below it. A number with presets has their menu right of its unit, or -below its box where the box of "constant" is below its menu, see `_catalog_preset_menu!`. The boxes -and the menus take the keyboard like those of the controls, see `_register_widget!`. +below its box where the box of "constant" is below its menu, see `_catalog_preset_menu!`. Last the +switches (`CatalogToggle`), each with its name and a `Toggle`. The boxes and the menus take the +keyboard like those of the controls, see `_register_widget!`. """ function _fill_catalog_form!(gui::LiveView, w::_CatalogWidget, strings) params = _catalog_entry(w).params @@ -933,6 +951,15 @@ function _fill_catalog_form!(gui::LiveView, w::_CatalogWidget, strings) end) end end + toggles = findall(p -> p isa CatalogToggle, params) + if !isempty(toggles) + grid = _catalog_form_grid(w.form[part += 1, 1]) + for (row, i) in enumerate(toggles) + Label(grid[row, 1], params[i].name; label...) + w.inputs[i] = Toggle(grid[row, 2]; active = _catalog_value(params[i], strings[i]), + _card_style(t, Toggle)..., halign = :left) + end + end foreach(b -> _register_widget!(gui, b), w.boxes) foreach(m -> _register_widget!(gui, m), w.menus) # drawn with the cameras of their sizes like the widgets of a new card, see `_settle_cards!` diff --git a/src/LiveCatalogEntries.jl b/src/LiveCatalogEntries.jl index 3c56d9a..ff30912 100644 --- a/src/LiveCatalogEntries.jl +++ b/src/LiveCatalogEntries.jl @@ -76,8 +76,9 @@ _entry_code(::Union{typeof(_line_source), typeof(_fan_source)}, entry::CatalogEn The entries of [`component_catalog`](@ref): the sources of BeamletOptics and all its components with a constructor of numbers and glasses, with the defaults of one inch optics. Only these -arguments are parameters, the others keep the defaults of the constructor, e.g. the `stop` of a -`Detector` and the `thickness` and the `hole_diameter` of the curved mirrors. The radii of curvature +arguments are parameters, the others keep the defaults of the constructor, e.g. the `thickness` and +the `hole_diameter` of the curved mirrors. The `stop` of a `Detector` is a switch (`CatalogToggle`) +"stop beams", on by default: off, the beams pass the detector unchanged. The radii of curvature follow BeamletOptics: positive if the center of the surface lies behind it (towards +y), i.e. `R1 > 0` and `R2 < 0` for a biconvex lens, `Inf` for a plane surface of a `SphericalLens`. @@ -203,7 +204,8 @@ function _builtin_catalog() ("Round filter", :polarizer, RoundPolarizationFilter, diameter), ("Linear polarizer", :linear_polarizer, RoundLinearPolarizer, diameter, len("front thickness", 1e-3), len("back thickness", 1e-3), CatalogGlass())) + # off: the beams pass the detector unchanged, see `Detector` group!("Detectors", - ("Detector", :detector, Detector, len("edge length", 10e-3))) + ("Detector", :detector, Detector, len("edge length", 10e-3), CatalogToggle("stop beams", true))) return entries end diff --git a/src/LiveEdit.jl b/src/LiveEdit.jl index ac6aefa..2e6fa84 100644 --- a/src/LiveEdit.jl +++ b/src/LiveEdit.jl @@ -36,6 +36,8 @@ end # The names of the widgets of the parameter `i`: its box and, of a glass, its menu _edit_box(i::Int) = Symbol(:edit_, i) _edit_menu(i::Int) = Symbol(:edit_glass_, i) +# ... of a switch, its toggle +_edit_toggle(i::Int) = Symbol(:edit_toggle_, i) # Of a surface: its menu, and the box of its field `k`, see `_edit_surface`: the radius in the box # of the parameter, then the conic constant and the coefficients _edit_surface_menu(i::Int) = Symbol(:edit_surface_, i) @@ -153,7 +155,8 @@ end Puts the component or source `new` with its `origin` in the place of `old` in the `gui`: `new` gets the pose of `old`, i.e. it is moved and rotated from its pose as constructed like `old` was from its own, then `old` is removed and `new` is added to the same system with the label of `old` and -selected, and the card of the selection shows its page "Edit" again. The replacement is one action +selected, and the card of the selection shows its page "Edit" again. The cards pinned to `old`, +floating or docked, are pinned to `new`, see `_take_pins!`. The replacement is one action of the undo history: undo brings `old` back, redo `new`. """ function _replace!(gui::LiveView, old, new, origin) @@ -173,9 +176,12 @@ function _replace!(gui::LiveView, old, new, origin) new_snap = Ref{Any}(_replacement_snapshot(gui, old, new, origin, old_snap)) old_state = Ref{Any}(old_snap) swap!(out, out_state, in, in_state) = begin + # the cards pinned to `out` stay, pinned to `in` + pins = _take_pins!(gui, out) out_state[] = _remove_unrecorded!(gui, out) delete!(comp.edits, out) _restore!(gui, in, in_state[]) + _give_pins!(gui, pins, in) _show_edit_page!(gui, in) return nothing end @@ -185,12 +191,16 @@ function _replace!(gui::LiveView, old, new, origin) return new end -"""Shows the page "Edit" of the cards of `obj` again, e.g. after it replaced the object of the page.""" +""" +Shows the page "Edit" of the cards of `obj` again, e.g. after it replaced the object of the page. +A pinned card keeps its page, see `_give_pins!`. +""" function _show_edit_page!(gui::LiveView, @nospecialize(obj)) _update_cards!(gui) _update_inspector!(gui) for c in _edit_cards(gui) - (_card_object(gui, c) === obj && :edit in _card_pages(gui, obj)) && _set_page!(gui, c, :edit) + (!c.pinned && _card_object(gui, c) === obj && :edit in _card_pages(gui, obj)) && + _set_page!(gui, c, :edit) end return nothing end @@ -266,6 +276,15 @@ function _choose_edit_glass!(gui::LiveView, @nospecialize(obj), p::CatalogGlass, return nothing end +# The toggle of the switch `i` changed to `v` +function _toggle_edit!(gui::LiveView, @nospecialize(obj), i::Int, v) + v isa Bool || return nothing + # The card shows its values again after an input: keep what was typed into its boxes + _read_edit_inputs!(gui, obj) + _set_edit_string!(gui, obj, i, string(v)) + return nothing +end + # Enter in the box of the parameter `i`: its input, then "Apply" function _enter_edit!(gui::LiveView, @nospecialize(obj), i::Int, s) # the other boxes as they are shown, then the entered one @@ -357,7 +376,8 @@ The rows of the parameter `p`, the input `i`, on the page "Edit" of `obj`: a box menu and the box of a constant refractive index for a glass, and for a surface its menu ("spherical", "aspheric"), the box of its radius and, only while "aspheric" is chosen, the boxes of the conic constant `k` and of the coefficients A4, A6, ... [mm^(1 - order)], two in a row. The rows -of a surface thus depend on its input, see `_rebuild_edit_rows!`. +of a surface thus depend on its input, see `_rebuild_edit_rows!`. A switch (`CatalogToggle`) has a +`Toggle`. """ _edit_param_rows(::LiveView, _, p::CatalogParam, i::Int) = (CardRow(_edit_label(p.name), CardWidget(Textbox; name = _edit_box(i), placeholder = " ", width = 76, @@ -382,6 +402,12 @@ function _edit_param_rows(gui::LiveView, @nospecialize(obj), p::CatalogSurface, return Tuple(rows) end +# The toggle of a switch: its state is the input, which "Apply" builds, like the menu of a glass +_edit_param_rows(::LiveView, _, p::CatalogToggle, i::Int) = (CardRow(_edit_label(p.name), + CardWidget(Toggle; name = _edit_toggle(i), + value = (gui, o) -> _catalog_value(p, _edit_strings(gui, o)[i]), + on = (gui, o, v) -> _toggle_edit!(gui, o, i, v))),) + _edit_param_rows(::LiveView, _, p::CatalogGlass, i::Int) = ( CardRow(_edit_label(p.name), CardWidget(Menu; name = _edit_menu(i), options = String[_glass_names()..., _GLASS_CONSTANT], width = 128, diff --git a/src/LiveInspector.jl b/src/LiveInspector.jl index dfc71b2..c5bd93e 100644 --- a/src/LiveInspector.jl +++ b/src/LiveInspector.jl @@ -681,6 +681,36 @@ function _unpin!(gui::AppView, @nospecialize(obj)) return nothing end +# The docked cards are built again for `new`, in their places, on their pages, collapsed if they +# were, see `_take_pins!` +function _take_pins!(gui::AppView, @nospecialize(old)) + pinned = gui.layout.inspector.pinned + docked = [(; index = _index(pinned, c), c.page, c.collapsed, c.view_expanded) + for c in _docked_cards(gui, old)] + foreach(c -> _remove_pinned!(gui, c), _docked_cards(gui, old)) + return (; invoke(_take_pins!, Tuple{LiveView, Any}, gui, old).floating, docked) +end + +function _give_pins!(gui::AppView, pins, @nospecialize(new)) + invoke(_give_pins!, Tuple{LiveView, Any, Any}, gui, pins, new) + isempty(pins.docked) && return nothing + insp = gui.layout.inspector + for d in pins.docked + c = _dock_pinned!(gui, new) + d.page in _card_pages(gui, new) && (c.page = d.page) + c.view_expanded = d.view_expanded + _set_collapsed!(c, d.collapsed) + # back to its place among the docked cards + insert!(insp.pinned, min(d.index, length(insp.pinned)), pop!(insp.pinned)) + end + for (k, c) in enumerate(insp.pinned) + insp.pinned_grid[k, 1] = c.parent + end + _refresh_inspector!(gui; force = true) + _on_pinned!(gui) + return nothing +end + """Returns the pinned cards of the app layout of the `gui` that are docked for `obj`.""" _docked_cards(gui::AppView, @nospecialize(obj)) = filter(c -> c.obj === obj, gui.layout.inspector.pinned) _is_docked(gui::AppView, @nospecialize(obj)) = any(c -> c.obj === obj, gui.layout.inspector.pinned) diff --git a/test/TestLiveCatalog.jl b/test/TestLiveCatalog.jl index 9c697c4..19a090f 100644 --- a/test/TestLiveCatalog.jl +++ b/test/TestLiveCatalog.jl @@ -408,6 +408,39 @@ ray_of(λ::Real) = Beam([0.0, 0, 0], [0.0, 1, 0], λ) end end + @testset "switch ($layout)" for layout in (:compact, :app) + gui, sys, m = _fixture(; layout) + w = _choose!(gui, "Detector") + widget = w.widget + # the box of the edge length and the toggle of "stop beams", on by default + toggle = widget.inputs[2] + @test toggle isa Toggle && toggle.active[] + @test "stop beams" in w.labels && _texts(w.boxes) == ["10"] + @test GUI._catalog_strings(widget) == ["10", "true"] + function placed() + obj = GUI._place_catalog!(gui, GUI._catalog_entry(widget), GUI._catalog_strings(widget)) + code = gui.components.placement.origin.code + GUI._end_placement!(gui) + return obj, code + end + obj, code = placed() + @test BMO.stop(obj) && code == "Detector(0.01, true)" + # off: the detector lets the beams pass + toggle.active[] = false + @test GUI._catalog_strings(widget) == ["10", "false"] + obj, code = placed() + @test !BMO.stop(obj) && code == "Detector(0.01, false)" + # the state is kept when the catalog widget is built again with the texts of its form + GUI._build_catalog_form!(gui, widget, GUI._catalog_strings(widget)) + @test !widget.inputs[2].active[] + # another entry and back: the default again + _choose!(gui, "Thin lens") + _choose!(gui, "Detector") + @test GUI._catalog_strings(widget) == ["10", "true"] + @test sys.objects == [m] + close(gui) + end + @testset "presets ($layout)" for layout in (:compact, :app) sizes = ["half inch" => 12.7e-3, "inch" => 25.4e-3] width = CatalogParam("width", 10e-3; unit = "mm", scale = 1e-3, presets = sizes) diff --git a/test/TestLiveCatalogEntries.jl b/test/TestLiveCatalogEntries.jl index f402d64..63232be 100644 --- a/test/TestLiveCatalogEntries.jl +++ b/test/TestLiveCatalogEntries.jl @@ -211,7 +211,19 @@ const GUI = BeamletOpticsGUI prism = _entry(entries, RightAnglePrism) @test GUI._catalog_code(prism, [10e-3, 10e-3, 1.7]) == "RightAnglePrism(0.01, 0.01, ConstantRefractiveIndex(1.7))" detector = _entry(entries, Detector) - @test GUI._catalog_code(detector, [5e-3]) == "Detector(0.005)" + # the switch "stop beams": on by default, off lets the beams pass + @test _defaults(detector) == Any[10e-3, true] + @test GUI._catalog_code(detector, Any[5e-3, true]) == "Detector(0.005, true)" + @test GUI._catalog_code(detector, Any[5e-3, false]) == "Detector(0.005, false)" + @test BMO.stop(GUI._catalog_object(detector, Any[5e-3, true])) + @test !BMO.stop(GUI._catalog_object(detector, Any[5e-3, false])) + p = detector.params[2] + @test p isa GUI.CatalogToggle && GUI._catalog_string(p) == "true" + @test GUI._catalog_value(p, "false") === false && GUI._catalog_value(p, " true ") === true + @test GUI._catalog_value(p, "") === true + @test_throws ArgumentError GUI._catalog_value(p, "maybe") + c = GUI._catalog_component(detector, ["5", "false"]) + @test !BMO.stop(c.obj) && c.origin.code == "Detector(0.005, false)" # an angle in degrees as a keyword oap = _entry(entries, OffAxisParabolicMirror) @test GUI._catalog_code(oap, _defaults(oap)) == "OffAxisParabolicMirror(0.05, 0.0254; angle = 90.0)" diff --git a/test/TestLiveEdit.jl b/test/TestLiveEdit.jl index 6b63aa4..8fa02db 100644 --- a/test/TestLiveEdit.jl +++ b/test/TestLiveEdit.jl @@ -197,6 +197,89 @@ const GUI = BeamletOpticsGUI close(gui) end + @testset "a detector that lets the beams pass, $layout" for layout in (:compact, :app) + # the detector of the catalog in front of a second one, which only gets the beam if the + # first one lets it pass + back = Detector(10e-3) + translate3d!(back, [0, 0.2, 0]) + sys = System([back]) + beam = Beam([0.0, 0, 0], [0.0, 1, 0], 632.8e-9) + gui = _live_view(sys => beam; layout) + c = _built("Detector") + translate3d!(c.obj, [0, 0.1, 0]) + add_component!(gui, c.obj; origin = c.origin, label = "PD") + targets() = [BMO.object(BMO.intersection(r)) for r in BMO.rays(beam) if !isnothing(BMO.intersection(r))] + @test BMO.stop(c.obj) && _in(c.obj, targets()) && !_in(back, targets()) + @test c.origin.code == "Detector(0.01, true)" + + # the page: the box of the edge length and the toggle of "stop beams" + card = _show!(gui, c.obj, :edit) + toggle = GUI._card_widget(card, GUI._edit_toggle(2)) + @test toggle isa Toggle && toggle.active[] + # switched off, with a length typed without Enter, which the switch keeps + _type!(card, 1, "20") + toggle.active[] = false + @test GUI._edit_strings(gui, c.obj) == ["20", "false"] + @test _in(c.obj, sys.objects) + notify(GUI._card_widget(_card(gui), :edit_apply).clicks) + new = only(o for o in sys.objects if o !== back) + @test new !== c.obj && !BMO.stop(new) && GUI._label(gui, new) == "PD" + @test gui.components.origin[new].code == "Detector(0.02, false)" + @test occursin("PD = Detector(0.02, false)", GUI._export_code(gui)[1]) + # the beam passes it onto the second detector, and both record it + @test isnothing(gui.trace.error) && !gui.trace.stale + @test _in(new, targets()) && _in(back, targets()) + @test !isnothing(BMO.hits(new)) && !isnothing(BMO.hits(back)) + @test !GUI._card_widget(_card(gui), GUI._edit_toggle(2)).active[] + + # undo brings the detector back that stops the beam + @test GUI._undo!(gui.controls) + @test _in(c.obj, sys.objects) && !_in(back, targets()) + close(gui) + end + + @testset "pinned cards stay, $layout ($where)" for (layout, where) in + ((:compact, :floating), (:app, :floating), (:app, :docked)) + back = Detector(10e-3) + translate3d!(back, [0, 0.2, 0]) + sys = System([back]) + gui = _live_view(sys => Beam([0.0, 0, 0], [0.0, 1, 0], 632.8e-9); layout) + c = _built("Detector") + translate3d!(c.obj, [0, 0.1, 0]) + add_component!(gui, c.obj; origin = c.origin, label = "PD") + # its card pinned, in the app layout docked or floating in the 3D view, on the page "Edit" + GUI._toggle_pin!(gui, c.obj) + where === :floating && layout === :app && GUI._float!(gui, c.obj) + pinned(obj) = where === :floating ? GUI._floating_cards(gui, obj) : GUI._docked_cards(gui, obj) + card = only(pinned(c.obj)) + GUI._update_cards!(gui) + GUI._set_page!(gui, card, :edit) + # the selection is elsewhere: "Apply" on the pinned card + gui.controls.selected[] = nothing + GUI._update_cards!(gui) + GUI._card_widget(card, GUI._edit_toggle(2)).active[] = false + notify(GUI._card_widget(card, :edit_apply).clicks) + new = only(o for o in sys.objects if o !== back) + @test new !== c.obj && !BMO.stop(new) + # the card is pinned to the new detector, still on the page "Edit", with its switch off + @test isempty(pinned(c.obj)) + card = only(pinned(new)) + @test GUI._is_pinned(gui, new) && card.page === :edit + GUI._update_cards!(gui) + @test !GUI._card_widget(card, GUI._edit_toggle(2)).active[] + layout === :app && @test length(gui.layout.inspector.pinned) == (where === :docked) + # a pinned card keeps another page, e.g. "Results" + GUI._set_page!(gui, card, :results) + _type!(_show!(gui, new, :edit), 1, "20") + notify(GUI._card_widget(_card(gui), :edit_apply).clicks) + newer = only(o for o in sys.objects if o !== back) + @test newer !== new && only(pinned(newer)).page === :results + # undo hands the card back + @test GUI._undo!(gui.controls) + @test only(pinned(new)).page === :results && isempty(pinned(newer)) + close(gui) + end + @testset "text typed without Enter" begin sys = System() gui = _live_view(sys => Beam([0.0, 0, 0], [0.0, 1, 0], 632.8e-9)) diff --git a/test/TestPrecompileSession.jl b/test/TestPrecompileSession.jl index 12ea40a..805ff7b 100644 --- a/test/TestPrecompileSession.jl +++ b/test/TestPrecompileSession.jl @@ -42,8 +42,12 @@ using Test empty!(names) ext._close!(ext._catalog_session(step)...) @test isempty(failed) - # each entry is placed, dragged and removed - @test length(names) == 3 * length(component_catalog()) + # each entry is placed, dragged and removed; one with a switch is also switched on its + # page "Edit", e.g. the detector + switched = count(e -> any(p -> p isa BeamletOpticsGUI.CatalogToggle, e.params), component_catalog()) + @test switched >= 1 + @test length(names) == 3 * length(component_catalog()) + switched + @test any(n -> occursin("edit switch", n), names) end end