Skip to content

Commit 10fc5bb

Browse files
authored
Direct component visualization (#98)
* Add default metadata map * Add show for CoordinateSystem * Add dark theme and theme preference * Add CS graphics test * Add workflow tip for graphical display * Make render functions return CS, explain VS Code display more
1 parent fde4846 commit 10fc5bb

File tree

12 files changed

+247
-50
lines changed

12 files changed

+247
-50
lines changed

docs/src/coordinate_systems.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ the GDSII format. Accordingly, they hold `Polygon`s with metadata of type `GDSMe
4444
CellArray
4545
CellReference
4646
GDSMeta
47+
gdslayers(::Cell)
4748
render!(::Cell, ::Polygon, ::GDSMeta)
4849
DeviceLayout.save(::File{format"GDS"}, ::Cell, ::Cell...)
4950
```
@@ -97,8 +98,6 @@ Unlike `Cell`s, a `CoordinateSystem` is not tied to a database unit (a GDSII con
9798

9899
Since `CoordinateSystem`s are intended to be backend-agnostic, a useful pattern is to give
99100
coordinate objects "semantic" metadata, consisting of a layer name `Symbol` as well as `level` and `index` attributes.
100-
This metadata can then be processed with designer-provided methods when rendering to an
101-
output format.
102101

103102
```@docs
104103
SemanticMeta
@@ -108,12 +107,13 @@ output format.
108107
level
109108
```
110109

111-
This means that a `CoordinateSystem` can be rendered to a `Cell` for output to a GDS format.
112-
During rendering, an `entity::GeometryEntity` with metadata `SemanticMeta(:ground_plane)` would be mapped to `to_polygons(entity)` with GDS layer number and datatype (for example, `GDSMeta(1,0)`) corresponding to the ground plane according to a mapping function `map_meta` provided to `render!`.
110+
A `CoordinateSystem` (or any `GeometryStructure`) can be rendered to a `Cell` for output to a GDS format by mapping its metadata to GDSMeta. Specifically, during rendering, an `entity::GeometryEntity` with metadata `SemanticMeta(:my_layer)` will be rendered as one or more polygons (`to_polygons(entity)`). These polygons will have GDSMeta (layer number and datatype) determined by `map_meta(SemanticMeta(:my_layer))`, where `map_meta` is a function supplied as a keyword argument to `render!`. A default hash-based map is supplied to allow quick visualizations when the specific output GDS layers don't matter.
113111

114112
```@docs
115113
Cell(::CoordinateSystem{S}) where {S}
116114
render!(::Cell, ::DeviceLayout.GeometryStructure)
115+
DeviceLayout.default_meta_map
116+
gdslayers(::DeviceLayout.GeometryStructure)
117117
```
118118

119119
Note that `Cell`s inherit the names of rendered `CoordinateSystem`s, so the original coordinate systems ought to have unique names (for example using [`uniquename`](@ref)).

docs/src/fileio.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ Using the [Cairo graphics library](https://cairographics.org), it is possible to
2121
cells into SVG, PDF, and EPS vector graphics formats, or into the PNG raster graphic
2222
format. This enables patterns to be displayed in web browsers, publications, presentations,
2323
and so on. You can save a cell to a graphics file by, e.g. `save("/path/to/file.svg", mycell)`.
24-
Note that cell references and arrays are not saved, so you should flatten cells if desired
25-
before saving them.
2624

2725
Possible keyword arguments include:
2826

docs/src/geometrylevel.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,15 @@ The entities making up the `Path` are rendered into `Polygon`s, but the `Geometr
8888
Let's see how it looks:
8989

9090
```@example 1
91-
save("dogbone_path.svg", flatten(c); layercolors=Dict(0 => (0, 0, 0, 1), 1 => (1, 0, 0, 1)));
91+
save("dogbone_path.svg", c; layercolors=Dict(0 => (0, 0, 0, 1), 1 => (1, 0, 0, 1)));
9292
nothing; # hide
9393
```
9494

9595
```@raw html
9696
<img src="../dogbone_path.svg" style="width: 3in;"/>
9797
```
9898

99-
!!! info
100-
101-
The SVG backend only draws the top-level `Cell`, so it's necessary to [`flatten`](./geometry.md#Flattening) the cell before saving. `flatten` traverses references to place all entities in a single `Cell`, applying transformations as necessary. For further examples, we'll hide the line that flattens and saves to SVG just to display a cell.
99+
For further examples, we'll hide the line that saves to SVG just to display a cell.
102100

103101
We can also add `Cell` references directly to a `Cell`, for example to apply a global rotation:
104102

docs/src/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ attach!(
106106
)
107107
c = Cell("decoratedpath", nm)
108108
render!(c, p, GDSMeta(0))
109-
save("units.svg", flatten(c); layercolors=Dict(0 => (0, 0, 0, 1), 1 => (1, 0, 0, 1)));
109+
save("units.svg", c; layercolors=Dict(0 => (0, 0, 0, 1), 1 => (1, 0, 0, 1)));
110110
nothing; # hide
111111
```
112112

@@ -167,3 +167,5 @@ main() # execute main() at end of script.
167167
In a typical workflow, you'll have a text editor open alongside a Julia REPL. You'll save the above code in a file (e.g., `mycad.jl`) and then run `include("mycad.jl")` from the Julia REPL to generate your pattern.
168168
You'll iteratively revise `mycad.jl` and save your changes.
169169
Subsequent runs should be several times faster than the first, if you `include` the file again from the same Julia session.
170+
171+
If you use a REPL started by the Julia for VS Code extension (`Alt+J Alt+O`), then objects that can be displayed graphically (`Cells` and `CoordinateSystems`) will be shown in a separate tab when returned by REPL execution. This display is not updated interactively with every command, but running `julia> my_cs` will show the latest version. You can zoom in and out to inspect details by holding `Command` (Mac) or `Alt` and scrolling.

src/DeviceLayout.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,8 @@ export Paths,
528528
undecorated
529529

530530
include("cells.jl")
531-
import .Cells: Cell, CellReference, CellArray, cell, layers, text!
532-
export Cells, Cell, CellReference, CellArray, cell, layers, text!
531+
import .Cells: Cell, CellReference, CellArray, cell, gdslayers, layers, text!
532+
export Cells, Cell, CellReference, CellArray, cell, gdslayers, layers, text!
533533

534534
include("utils.jl")
535535

src/backends/graphics.jl

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ using Unitful
33
import Unitful: Length, inch, ustrip
44
import Cairo
55

6-
import DeviceLayout: bounds, datatype, gdslayer, load, save
6+
import DeviceLayout:
7+
bounds, datatype, default_meta_map, element_metadata, gdslayer, load, save, to_polygons
8+
import DeviceLayout: CoordinateSystem, GeometryEntity
79
using ..Points
810
using ..Transformations
911
import ..Rectangles: Rectangle, width, height
@@ -13,16 +15,61 @@ using ..Cells
1315
import FileIO: File, @format_str, stream
1416

1517
using ColorSchemes
18+
using Preferences
1619

17-
using ColorSchemes
18-
# Glasbey color scheme for categorical data (version avoiding greys and light colors)
19-
lcolor(l, scheme=:glasbey_bw_minc_20_maxl_70_n256) = # Make transparent
20-
(
21-
colorschemes[scheme][l + 1].r,
22-
colorschemes[scheme][l + 1].g,
23-
colorschemes[scheme][l + 1].b,
24-
0.5
25-
)
20+
# Available color schemes -- Glasbey themes for categorical data
21+
const LIGHT_MODE_SCHEME = :glasbey_bw_minc_20_maxl_70_n256 # Good for light backgrounds
22+
const DARK_MODE_SCHEME = :glasbey_bw_minc_20_minl_30_n256 # Good for dark backgrounds
23+
24+
# Preference key for color theme
25+
const COLOR_THEME_PREF = "color_theme"
26+
27+
"""
28+
get_color_scheme()
29+
30+
Get the current color scheme based on user preferences.
31+
Returns either `:glasbey_bw_minc_20_maxl_70_n256` (light theme) or
32+
`:glasbey_bw_minc_20_minl_30_n256` (dark theme).
33+
34+
The default is light theme.
35+
"""
36+
function get_color_scheme()
37+
scheme_name = @load_preference(COLOR_THEME_PREF, "light")
38+
return scheme_name == "dark" ? DARK_MODE_SCHEME : LIGHT_MODE_SCHEME
39+
end
40+
41+
"""
42+
set_theme!(theme::String)
43+
44+
Set the color scheme for graphics based on background lightness (`"light"` or `"dark"`).
45+
46+
Light theme uses `:glasbey_bw_minc_20_maxl_70_n256` (avoids light colors, good for light backgrounds).
47+
48+
Dark theme uses `:glasbey_bw_minc_20_minl_30_n256` (avoids dark colors, good for dark backgrounds).
49+
"""
50+
function set_theme!(theme::String)
51+
if theme ["light", "dark"]
52+
error("Theme must be 'light' or 'dark', got: '$theme'")
53+
end
54+
@set_preferences!(COLOR_THEME_PREF => theme)
55+
for i = 0:255
56+
layercolors[i] = lcolor(i)
57+
end
58+
@info "Color scheme set for '$theme' theme."
59+
end
60+
61+
# Generate layer color with transparency
62+
lcolor(l, scheme) = (
63+
colorschemes[scheme][l + 1].r,
64+
colorschemes[scheme][l + 1].g,
65+
colorschemes[scheme][l + 1].b,
66+
0.5
67+
)
68+
69+
# Use preference-based color scheme
70+
lcolor(l) = lcolor(l, get_color_scheme())
71+
72+
# Initialize layercolors with the preferred scheme
2673
const layercolors = Dict([(i => lcolor(i)) for i = 0:255]...)
2774

2875
function fillcolor(options, layer)
@@ -43,10 +90,16 @@ MIMETypes = Union{
4390
MIME"application/pdf",
4491
MIME"application/postscript"
4592
}
46-
function Base.show(io, mime::MIMETypes, c0::Cell{T}; options...) where {T}
93+
function Base.show(
94+
io,
95+
mime::MIMETypes,
96+
geom::Union{Cell{T}, CoordinateSystem{T}};
97+
options...
98+
) where {T}
99+
c0 = flatten(geom)
47100
opt = Dict{Symbol, Any}(options)
48-
bnd = ustrip(bounds(c0))
49-
w, h = width(bnd), height(bnd)
101+
bnd = bounds(c0)
102+
w, h = width(ustrip(bnd)), height(ustrip(bnd))
50103
w1 = haskey(opt, :width) ? lscale(opt[:width]) : 4 * 72
51104
h1 = haskey(opt, :height) ? lscale(opt[:height]) : 4 * 72
52105
bboxes = haskey(opt, :bboxes) ? opt[:bboxes] : false
@@ -71,7 +124,7 @@ function Base.show(io, mime::MIMETypes, c0::Cell{T}; options...) where {T}
71124
Cairo.fill(ctx)
72125
end
73126

74-
ly = collect(layers(c0))
127+
ly = collect(gdslayers(c0))
75128
trans = Translation(-bnd.ll.x, bnd.ur.y) XReflection()
76129

77130
sf = min(w1 / w, h1 / h)
@@ -80,8 +133,8 @@ function Base.show(io, mime::MIMETypes, c0::Cell{T}; options...) where {T}
80133
for l in sort(ly)
81134
Cairo.save(ctx)
82135
Cairo.set_source_rgba(ctx, fillcolor(options, l)...)
83-
for p in c0.elements[gdslayer.(c0.element_metadata) .== l]
84-
poly!(ctx, trans.(ustrip(points(p))))
136+
for el in c0.elements[gdslayer.(default_meta_map.(element_metadata(c0))) .== l]
137+
poly!(ctx, trans(el))
85138
end
86139
Cairo.fill(ctx)
87140
Cairo.restore(ctx)
@@ -122,6 +175,10 @@ function poly!(cr::Cairo.CairoContext, pts)
122175
return Cairo.close_path(cr)
123176
end
124177

178+
poly!(cr::Cairo.CairoContext, p::Polygon) = poly!(cr, ustrip(points(p)))
179+
poly!(cr::Cairo.CairoContext, ps::Vector{<:Polygon}) = poly!.(Ref(cr), ps)
180+
poly!(cr::Cairo.CairoContext, ent::GeometryEntity) = poly!(cr, to_polygons(ent))
181+
125182
function save(f::File{format"SVG"}, c0::Cell; options...)
126183
open(f, "w") do s
127184
io = stream(s)

src/cells.jl

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import DeviceLayout:
2424
import DeviceLayout: flatten, flatten!, order!, traverse!, uniquename # to re-export
2525

2626
export Cell, CellArray, CellReference
27-
export cell, dbscale, layers, flatten, flatten!, order!, traverse!, uniquename
27+
export cell, dbscale, layers, gdslayers, flatten, flatten!, order!, traverse!, uniquename
2828

2929
# Avoid circular definitions
3030
abstract type AbstractCell{S} <: AbstractCoordinateSystem{S} end
@@ -247,13 +247,25 @@ function CoordinateSystems.append_coordsys!(
247247
end
248248

249249
"""
250-
layers(x::Cell)
250+
gdslayers(x::Cell)
251251
252-
Returns the GDS layers of elements in cell `x` as a set. Does *not* return the layers
252+
Returns the unique GDS layers of elements in cell `x`. Does *not* return the layers
253253
in referenced or arrayed cells.
254254
"""
255+
gdslayers(x::Cell) = unique(map(gdslayer, x.element_metadata))
256+
257+
"""
258+
gdslayers(x::GeometryStructure)
259+
260+
Returns the unique GDS layers of elements in `x`, using [`DeviceLayout.default_meta_map`](@ref). Does *not* return the layers
261+
in referenced structures.
262+
"""
263+
gdslayers(x::GeometryStructure) =
264+
unique(map(gdslayer DeviceLayout.default_meta_map, element_metadata(x)))
255265
layers(x::Cell) = unique(map(gdslayer, x.element_metadata))
256266

267+
@deprecate layers(x) gdslayers(x)
268+
257269
"""
258270
text!(c::Cell{S}, str::String, origin::Point=zero(Point{S}), meta::Meta=GDSMeta(); kwargs...) where {S}
259271
@@ -267,8 +279,7 @@ function text!(
267279
meta::GDSMeta=GDSMeta();
268280
kwargs...
269281
) where {S}
270-
push!(c.texts, Texts.Text(; text, origin, kwargs...))
271-
return push!(c.text_metadata, meta)
282+
return text!(c, Texts.Text(; text, origin, kwargs...), meta)
272283
end
273284

274285
function text!(
@@ -278,8 +289,7 @@ function text!(
278289
origin=zero(Point{S}),
279290
kwargs...
280291
) where {S}
281-
push!(c.texts, Texts.Text(; text, origin, kwargs...))
282-
return push!(c.text_metadata, meta)
292+
return text!(c, Texts.Text(; text, origin, kwargs...), meta)
283293
end
284294

285295
"""
@@ -289,12 +299,14 @@ Annotate cell `c` with `Texts.Text` object.
289299
"""
290300
function text!(c::Cell, text::Texts.Text, meta)
291301
push!(c.texts, text)
292-
return push!(c.text_metadata, meta)
302+
push!(c.text_metadata, meta)
303+
return c
293304
end
294305

295306
function text!(c::Cell{S}, texts::Vector{Texts.Text{S}}, meta::Vector{GDSMeta}) where {S}
296307
append!(c.texts, texts)
297-
return append!(c.text_metadata, meta)
308+
append!(c.text_metadata, meta)
309+
return c
298310
end
299311

300312
Base.isempty(c::Cell) = isempty(elements(c)) && isempty(refs(c)) && isempty(c.texts)

src/coordinate_systems.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,16 @@ Place `ent` in `cs` with metadata `metadata`.
135135
"""
136136
function place!(cs::CoordinateSystem, ent::GeometryEntity, metadata::Meta)
137137
push!(cs.elements, ent)
138-
return push!(cs.element_metadata, metadata)
138+
push!(cs.element_metadata, metadata)
139+
return cs
139140
end
140141
place!(cs::CoordinateSystem, geom, layer::Union{Symbol, String}) =
141142
place!(cs, geom, SemanticMeta(layer))
142143

143144
function place!(cs::CoordinateSystem, ents::Vector, metadata::Vector{<:Meta})
144145
append!(cs.elements, ents)
145-
return append!(cs.element_metadata, metadata)
146+
append!(cs.element_metadata, metadata)
147+
return cs
146148
end
147149

148150
"""
@@ -151,7 +153,8 @@ end
151153
Place a reference to `s` in `cs`.
152154
"""
153155
function place!(cs::CoordinateSystem, s::GeometryStructure)
154-
return addref!(cs, s)
156+
addref!(cs, s)
157+
return cs
155158
end
156159

157160
"""

src/metadata.jl

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@
1010
DeviceLayout-native representation of an object's layer information and attributes.
1111
1212
Semantic metadata refers to the meaning of an element without reference to a fixed encoding.
13-
For example, this polygon is in the negative of the ground plane is semantic, while
14-
this polygon is in GDS layer 1, datatype 2 is not. The semantic metadata is used in the
13+
For example, "this polygon is in the negative of the ground plane" is semantic, while
14+
"this polygon is in GDS layer 1, datatype 2" is not. The semantic metadata is used in the
1515
final `render` step, where a layout is converted from a `CoordinateSystem` to a
1616
representation corresponding to a particular output format (e.g., a `Cell` for GDSII).
17-
A call to `render!(cell::Cell{S}, cs::CoordinateSystem; map_meta = identity, kwargs...)`
17+
A call to `render!(cell::Cell{S}, cs::CoordinateSystem; map_meta = default_meta_map, kwargs...)`
1818
will use the `map_meta` function to map each `GeometryEntity`'s metadata to `GDSMeta`.
1919
20+
By default, [`DeviceLayout.default_meta_map`](@ref) is used, which:
21+
22+
- Passes GDSMeta through unchanged
23+
- Converts other metadata types to GDSMeta using a hash-based layer assignment (0-255)
24+
2025
The `level` and `index` fields do not have a strict interpretation imposed by DeviceLayout. (In
2126
this sense they are similar to GDS `datatype`.) The suggested use is as follows:
2227
@@ -142,3 +147,53 @@ layer_inclusion(only_layers, ignore_layers::Union{Symbol, Meta}) =
142147
layer_inclusion(only_layers, [ignore_layers])
143148
layer_inclusion(only_layers::Union{Symbol, Meta}, ignore_layers::Union{Symbol, Meta}) =
144149
layer_inclusion([only_layers], [ignore_layers])
150+
151+
"""
152+
hash_to_gdslayer(meta::Meta) -> Int
153+
154+
Convert metadata `m` to a GDS layer number (0-255) by hashing `(layer(m), level(m))`.
155+
156+
Values are repeatable for different metadata, but only probably distinct.
157+
"""
158+
function hash_to_gdslayer(meta::Meta)
159+
h = hash((layer(meta), level(meta)))
160+
return Int(h % UInt8)
161+
end
162+
163+
"""
164+
default_meta_map(meta::Meta) -> GDSMeta
165+
166+
Default metadata mapping function for rendering to Cell.
167+
168+
This map is for convenient graphical display and should not be relied on in production workflows.
169+
170+
GDSMeta passes through unchanged.
171+
172+
Other Meta types are converted to GDSMeta using a
173+
layer in (0-255) based on the hash of `(layer(m), level(m))` and datatype
174+
`layerindex(m)-1` (clamped to 0-255). This means that other metadata
175+
types are not guaranteed to be mapped to unique GDSMeta.
176+
177+
# Examples
178+
179+
```julia
180+
julia> default_meta_map(GDSMeta(10, 2))
181+
GDSMeta(10, 2)
182+
183+
julia> default_meta_map(SemanticMeta(:metal))
184+
GDSMeta(63, 0) # Hash-based layer, datatype from index
185+
186+
julia> default_meta_map(SemanticMeta(:metal, index=5))
187+
GDSMeta(63, 4) # Same layer, different datatype
188+
```
189+
"""
190+
function default_meta_map(meta::GDSMeta)
191+
return meta
192+
end
193+
194+
function default_meta_map(meta::Meta)
195+
layer_num = hash_to_gdslayer(meta)
196+
# Use layerindex-1 as datatype (0-based), clamped to valid range
197+
datatype_num = clamp(layerindex(meta) - 1, 0, 255)
198+
return GDSMeta(layer_num, datatype_num)
199+
end

0 commit comments

Comments
 (0)