diff --git a/docs/src/api.md b/docs/src/api.md index 37d778c..01c6731 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -64,6 +64,17 @@ pose moved ``` +## Own types + +A type of the project's own code (`src/.jl`) tells its constructor, so that the window can +save changes of its shape to its line. `src/gui.jl` holds the project's code for the window (e.g. +`card_rows` of its own types): only the GUI includes it, [`load_project`](@ref) never does, and +its packages are not the project's `[packages]`. + +```@docs +constructor_code +``` + ## Versions of the packages ```@docs diff --git a/skills/beamletopticsprojects/API.md b/skills/beamletopticsprojects/API.md index a5362ee..a8c954a 100644 --- a/skills/beamletopticsprojects/API.md +++ b/skills/beamletopticsprojects/API.md @@ -26,6 +26,7 @@ but not exported are qualified: `BeamletOpticsProjects.install_agent_skill()`. | The form of `system.jl` | `@component`, `@source`, `@group`, `@system`, `id_prefix`, `next_id`, `apply_pose!`, `rotation_matrix` | | Change lines (checked, one write) | `add!`, `set!`, `move!`, `duplicate!`, `remove!`, `add_system!`, `join_system!`, `leave_system!` | | Read lines | `members`, `sources`, `pose`, `moved` | +| Own types | `constructor_code` | | Versions of the packages | `project_versions`, `check_versions`, `VersionCheck` | | Trace and read | `trace`, `look` | | Versions and changes | `save_version`, `versions`, `version_summary`, `diff_projects`, `EntryChange`, `SystemChange`, `GroupChange`, `copy_code` | diff --git a/skills/beamletopticsprojects/FORMAT.md b/skills/beamletopticsprojects/FORMAT.md index 4d312ce..35c6743 100644 --- a/skills/beamletopticsprojects/FORMAT.md +++ b/skills/beamletopticsprojects/FORMAT.md @@ -18,6 +18,8 @@ One line per entry; components first, then sources, then groups, then systems: copy it; a new line has no `uid`, it gets one when the project is saved. - The right-hand side is any expression that builds a BeamletOptics component, group or source, also a call of a function of `src/.jl` (its exported names). SI units: m, wavelengths in m. + For an own type with a method of `constructor_code`, the window writes this expression anew + from that method when a person changes the shape on its card (see "Own code" in `SKILL.md`). - `pos` is the absolute position (m), `rot` the rotation since construction in degrees: first about the global x-axis, then y, then z. Both are optional. - `catalog = (...)` belongs to the GUI (its page "Edit"); delete it when you change the constructor. diff --git a/skills/beamletopticsprojects/SKILL.md b/skills/beamletopticsprojects/SKILL.md index c468836..0bd2337 100644 --- a/skills/beamletopticsprojects/SKILL.md +++ b/skills/beamletopticsprojects/SKILL.md @@ -61,6 +61,7 @@ Bench/ │ with ([versions]) and the packages that src/ uses ([packages]). Do not edit it. ├── system.jl the setup: one line per component, source, group and system (FORMAT.md) ├── src/Bench.jl own code (module Bench): functions that build components, own types. The GUI never writes it. +├── src/gui.jl optional: the window's part (card_rows of own types). Only the GUI loads it (see "Own code"). ├── view.toml the view of the GUI (camera, colors, clip planes). Do not edit it. ├── AGENTS.md what is specific to this project (CLAUDE.md points to it) ├── .claude/skills/ the skills, installed from the packages (not versioned) @@ -138,6 +139,47 @@ s = add_system!(".", [l, d]; sources = [b], by = "claude") trace(load_project("."), s).detectors ``` +## Own code + +`src/.jl` holds the module `` of the project: functions that build components or +groups and own types. Its exported names can be used in `system.jl`. It uses BeamletOptics and the +packages under `[packages]`, never the GUI. + +- **An own type tells its constructor** with a method of `constructor_code`, the code of the + right-hand side of its line without the pose. The window builds the line from it when a person + changes the shape of such an object on its card (e.g. a slider of a width); without a method such + a change lives only in the window and is not saved. The string must parse and build the object as + it is now, in the module of the project; `import ..BeamletOpticsProjects` reaches the package + from the module in any environment: + + ```julia + module Bench + using BeamletOptics + import ..BeamletOpticsProjects + export Block + + struct Block{T} <: BeamletOptics.AbstractObject{T} + shape::BeamletOptics.PlanoSurfaceSDF{T} + width::T + thickness::T + end + Block(width, thickness) = Block(BeamletOptics.PlanoSurfaceSDF(thickness, width), width, thickness) + BeamletOptics.interact3d(::BeamletOptics.AbstractSystem, ::Block, ::Beam, ::Ray) = nothing # absorbs + + BeamletOpticsProjects.constructor_code(b::Block) = "Block($(b.width), $(b.thickness))" + end + ``` + + `constructor_code(obj)` is `nothing` for every type without a method (the components of + BeamletOptics too: their lines are written from their code). After `load_project` in the same + script, call it as `Base.invokelatest(constructor_code, p[:K1])`: the method is newer than the + script. +- **`src/gui.jl` is the window's part** (optional): `card_rows` and other code of BeamletOpticsGUI + for the own types (the skill `beamletopticsgui`). Only the window includes it, into the module + of the project after the rest. `load_project` and `trace` never include it, so you neither load + nor need it, and its packages (the GUI, Makie) are not under `[packages]`. Write it only when the + person asks for a card of an own type; it changes the project like any file of `src/`. + ## The open windows Several people may have the project open in their own windows while you work on it. diff --git a/src/BeamletOpticsProjects.jl b/src/BeamletOpticsProjects.jl index 9585a3f..a203515 100644 --- a/src/BeamletOpticsProjects.jl +++ b/src/BeamletOpticsProjects.jl @@ -28,7 +28,7 @@ export Project, Entry, SystemEntry, GroupEntry, new_project, load_project, save_ announce, leave, fork_team, teams, post!, board, claim!, release!, claims, wait_for, merge_draft!, evaluate, move!, add!, set!, duplicate!, remove!, add_system!, join_system!, leave_system!, members, sources, pose, moved, - project_versions, check_versions, VersionCheck + project_versions, check_versions, VersionCheck, constructor_code public project_name, module_name, normalize_code, EXTENSION, same_entry, same_system, same_group, new_uid, match_entries, install_agent_skill, person, presence_file, session_file, write_presence, read_presences, alive, merge_plan, compose @@ -211,5 +211,6 @@ include("Move.jl") include("Query.jl") include("Edit.jl") include("Systems.jl") +include("Code.jl") end diff --git a/src/Code.jl b/src/Code.jl new file mode 100644 index 0000000..2019398 --- /dev/null +++ b/src/Code.jl @@ -0,0 +1,26 @@ +#= +Own types of a project: the constructor code of an object, by which a change of its shape (e.g. on +a card of the GUI) is written to its line of `system.jl` +=# + +""" + constructor_code(obj) -> Union{Nothing, String} + +The code of the constructor that builds `obj` as it is now, without its pose: the right-hand side +of its line in `system.jl` (the line keeps `pos` and `rot`). `nothing` (the default) if the type of +`obj` does not tell it. + +An own type in the code of a project (`src/.jl`) adds a method, so that a change of its shape +(e.g. by a slider on its card in the GUI) can be saved to its line: + +```julia +# src/Bench.jl: an own type Porro, built by Porro(leg, height) and keeping both +BeamletOpticsProjects.constructor_code(p::Porro) = "Porro(\$(p.leg), \$(p.height))" +``` + +The string must parse and, evaluated in the module of the project (where the exported names of +`src/.jl` are known), build an object equal in shape to `obj`. Print numbers with `repr` or +string interpolation of `Float64`, which round-trip exactly. Without a method, a change of the +shape lives only in the window and is lost when the project is loaded again. +""" +constructor_code(@nospecialize(obj)) = nothing diff --git a/src/New.jl b/src/New.jl index b491412..5ad93a2 100644 --- a/src/New.jl +++ b/src/New.jl @@ -158,7 +158,12 @@ _agents_template(name, marker) = """ - `system.jl`: the setup, one line per component, source and system. - `src/$name.jl`: own code (module `$name`): functions that build components or groups, own - types. Its exported names can be used in `system.jl`. The GUI never writes it. + types. Its exported names can be used in `system.jl`. The GUI never writes it. An own type + tells its constructor with a method of `BeamletOpticsProjects.constructor_code`, so that the + window can save changes of its shape (e.g. on its card) to its line. + - `src/gui.jl` (optional): the code for the window (`card_rows` etc. of the own types). Only + the GUI includes it, into the module of the project; agents neither load nor need it, and + `load_project` and `trace` work without it. Its packages are not under `[packages]`. - `$marker`: the project file that people open. It records the module name, the newest versions the project was saved with (`[versions]`) and the packages that `src/` uses (`[packages]`); the saves write it, do not edit it. diff --git a/src/Write.jl b/src/Write.jl index a032268..d686491 100644 --- a/src/Write.jl +++ b/src/Write.jl @@ -477,12 +477,21 @@ end # Names in `using` and `import` that are no packages of `[packages]` const _NOT_PACKAGES = ("Base", "Core", "Main") +""" +The file of `src/` that holds the project's code for the window (e.g. `card_rows` of its own +types): the GUI includes it into the module of the project after the rest, [`load_project`](@ref) +never does, and the packages it uses (the GUI, Makie) are not the project's `[packages]`. It counts +for [`project_stamp`](@ref) like any file of `src/`. +""" +const _GUI_FILE = "gui.jl" + """ The names of the packages that the own code of the project in the folder `dir` uses: the first name of every `using` and `import` in the `.jl` files of `src/` (without files and folders whose name -starts with `.`, as for [`project_stamp`](@ref)), without relative ones (`using .X`), `Base`, -`Core`, `Main`, BeamletOptics, BeamletOpticsProjects and the names `own` (the project's own -module). A file that does not parse is skipped. +starts with `.`, as for [`project_stamp`](@ref), and without `src/gui.jl`, the window's part, see +`_GUI_FILE`), without relative ones (`using .X`), `Base`, `Core`, `Main`, BeamletOptics, +BeamletOpticsProjects and the names `own` (the project's own module). A file that does not parse is +skipped. """ function _used_packages(dir, own) names = Set{String}() @@ -492,6 +501,7 @@ function _used_packages(dir, own) filter!(d -> !startswith(d, '.'), dirs) for f in files (startswith(f, '.') || !endswith(f, ".jl")) && continue + (root == src && f == _GUI_FILE) && continue ex = try Meta.parseall(read(joinpath(root, f), String); filename = f) catch e diff --git a/test/TestCode.jl b/test/TestCode.jl new file mode 100644 index 0000000..5d673ef --- /dev/null +++ b/test/TestCode.jl @@ -0,0 +1,103 @@ +module TestCode + +using BeamletOptics, BeamletOpticsProjects +using Test + +const P = BeamletOpticsProjects + +# An own type of a project with two shape parameters, which tells its constructor +const MODULE = """ +module Bench +using BeamletOptics +import ..BeamletOpticsProjects +export Block + +struct Block{T} <: BeamletOptics.AbstractObject{T} + shape::BeamletOptics.PlanoSurfaceSDF{T} + width::T + thickness::T +end + +Block(width, thickness) = Block(BeamletOptics.PlanoSurfaceSDF(thickness, width), width, thickness) + +# Absorbs the rays that hit it +BeamletOptics.interact3d(::BeamletOptics.AbstractSystem, ::Block, ::Beam, ::Ray) = nothing + +BeamletOpticsProjects.constructor_code(b::Block) = "Block(\$(b.width), \$(b.thickness))" +end +""" + +const SYSTEM = """ +@component K1 = Block(0.02, 0.005) pos = (0.0, 0.1, 0.0) rot = (0.0, 0.0, 10.0) +@component M1 = RoundPlanoMirror(0.0254, 0.006) pos = (0.0, 0.3, 0.0) +@source B1 = Beam([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1.064e-6) +@system S1 = [K1, M1] sources = [B1] +""" + +function bench(; gui = nothing) + dir = mktempdir() + write(joinpath(dir, "Bench.bmo"), "name = \"Bench\"\n") + mkpath(joinpath(dir, "src")) + write(joinpath(dir, "src", "Bench.jl"), MODULE) + isnothing(gui) || write(joinpath(dir, "src", "gui.jl"), gui) + write(joinpath(dir, "system.jl"), SYSTEM) + return dir +end + +@testset "constructor_code: nothing without a method" begin + for x in (RoundPlanoMirror(0.0254, 0.006), Beam([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1.064e-6), + Detector(0.02), 1, "x", nothing, [1, 2]) + @test constructor_code(x) === nothing + end +end + +@testset "constructor_code: the method of an own type of a project" begin + dir = bench() + p = load_project(dir) + k = p[:K1] + # The method was defined by load_project, in a newer world + code = Base.invokelatest(constructor_code, k) + @test code == "Block(0.02, 0.005)" + # The objects of BeamletOptics in the same project have none + @test Base.invokelatest(constructor_code, p[:M1]) === nothing + # The code builds the object as it is, in the module of the project + b = Base.invokelatest(Core.eval, p.mod, Meta.parse(code)) + @test typeof(b) == typeof(k) && b.width == k.width && b.thickness == k.thickness + # A change of the shape goes to the line; the pose stays + set!(dir, :K1; code = "Block(0.03, 0.004)") + q = load_project(dir) + @test Base.invokelatest(constructor_code, q[:K1]) == "Block(0.03, 0.004)" + @test pose(q, :K1).pos == pose(p, :K1).pos + @test pose(q, :K1).rot ≈ pose(p, :K1).rot + # The project traces with its own type + r = trace(q, :S1) + @test :K1 in r.ids +end + +@testset "src/gui.jl: the window's part" begin + gui = """ + using BeamletOpticsGUI + using NotInstalledXyz + BeamletOpticsGUI.card_rows(b::Bench.Block) = () + """ + dir = bench(; gui) + # load_project never includes it: a package that is not installed does not matter + p = load_project(dir) + @test Base.invokelatest(constructor_code, p[:K1]) == "Block(0.02, 0.005)" + @test !Base.invokelatest(isdefined, p.mod, :NotInstalledXyz) + # Its packages are not the project's + @test isempty(P._used_packages(dir, ("Bench",))) + save_project(p) + @test isempty(project_versions(dir).packages) + # A file gui.jl elsewhere in src/ is own code like any other + mkpath(joinpath(dir, "src", "parts")) + write(joinpath(dir, "src", "parts", "gui.jl"), "using TOML\n") + @test P._used_packages(dir, ("Bench",)) == Set(["TOML"]) + rm(joinpath(dir, "src", "parts"); recursive = true) + # A change of it is a change of the project + s = project_stamp(dir) + write(joinpath(dir, "src", "gui.jl"), gui * "# changed\n") + @test project_stamp(dir) != s +end + +end diff --git a/test/TestNew.jl b/test/TestNew.jl index 779f993..c100063 100644 --- a/test/TestNew.jl +++ b/test/TestNew.jl @@ -73,6 +73,12 @@ end @test !occursin("environment of the person", agents) # the own code of the project has no hint at code for the GUI @test !occursin("card_rows", read(joinpath(dir, "src", "Example.jl"), String)) + # that is src/gui.jl, the window's part, which agents do not load; own types tell their + # constructor for the saves of the window + files = match(r"## Files\n(.*?)(\n## |\z)"s, agents).captures[1] + @test occursin("`src/gui.jl`", files) && occursin("card_rows", files) + @test occursin("agents neither load nor need it", files) + @test occursin("constructor_code", files) @test occursin("`[packages]` of `$(basename(dir)).bmo`", env) @test occursin("Never make this folder", env) @test !occursin("Project.toml", agents) && !occursin(".envs", agents) diff --git a/test/runtests.jl b/test/runtests.jl index 675c768..a52c8bc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -26,6 +26,7 @@ using Test include(joinpath(@__DIR__, "TestQuery.jl")) include(joinpath(@__DIR__, "TestEdit.jl")) include(joinpath(@__DIR__, "TestSystems.jl")) + include(joinpath(@__DIR__, "TestCode.jl")) include(joinpath(@__DIR__, "TestProjectVersions.jl")) include(joinpath(@__DIR__, "TestAqua.jl")) end