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
11 changes: 11 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ pose
moved
```

## Own types

A type of the project's own code (`src/<Name>.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
Expand Down
1 change: 1 addition & 0 deletions skills/beamletopticsprojects/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
2 changes: 2 additions & 0 deletions skills/beamletopticsprojects/FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<Name>.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.
Expand Down
42 changes: 42 additions & 0 deletions skills/beamletopticsprojects/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -138,6 +139,47 @@ s = add_system!(".", [l, d]; sources = [b], by = "claude")
trace(load_project("."), s).detectors
```

## Own code

`src/<Name>.jl` holds the module `<Name>` 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.
Expand Down
3 changes: 2 additions & 1 deletion src/BeamletOpticsProjects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -211,5 +211,6 @@ include("Move.jl")
include("Query.jl")
include("Edit.jl")
include("Systems.jl")
include("Code.jl")

end
26 changes: 26 additions & 0 deletions src/Code.jl
Original file line number Diff line number Diff line change
@@ -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/<Name>.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/<Name>.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
7 changes: 6 additions & 1 deletion src/New.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 13 additions & 3 deletions src/Write.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}()
Expand All @@ -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
Expand Down
103 changes: 103 additions & 0 deletions test/TestCode.jl
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions test/TestNew.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading