Layer Overview¶
Layer is a concept that holds the specification of the 4 key components: data,
mark, channels and transform. A layer may
be complete if all its associated components are not None, or partial if one or more component is
None. A layer is created with the hkw.layer method.
Here we have created an empty layer, where all of data, mark, channels and transform are None.
An empty layer cannot be rendered because the data component is required for rendering. Fortunately, it is very easy to build on top of an existing layer with a set of overwrite functions. For example,
where l1 is a new layer created from l0 layer with the data component set to shape.obj. The
method .data is an example of such overwrite functions. The other overwrite functions are .mark,
.channel and .transform. These overwrite functions can be chained together based on the fluent
interface design pattern.
Note that the overwrite functions do not change the caller object (i.e. l0 in the above
example). This design allows the base layer l0 to be reused over and over again. Here is a more
complex example.
mesh = lagrange.io.load_mesh("shape.obj")
position_attr_name = mesh.attr_name_vertex_to_position
base = (
hkw.layer()
.data(mesh)
.mark(hkw.mark.Point)
.channel(size=0.1)
.transform(
hkw.transform.Filter(
data=position_attr_name, condition=lambda p: p[0] > 0
)
)
)
This visualization shows all vertices of the input mesh with positive X coordinate as spheres with
radius 0.1. Note that, in addition to filename, .data method can also take an actual Lagrange
SurfaceMesh object.
Lastly, it is also possible to directly specify the components as arguments to hkw.layer method.
Task-oriented helpers¶
Common visualization intents have concise methods. Each method expands into the same marks, channels, textures, transforms, and composition nodes described by the core grammar, so serialization and backend behavior remain unchanged.
Color by a scalar attribute¶
The default workflow needs only the data and attribute name. Hakowan infers the
domain and supplies viridis plus an automatic legend:
Specify only the options that differ from those defaults:
colored = base.color_by(
"temperature",
domain=(0, 100),
legend=hkw.Legend(title="Temperature", units="°C"),
)
color_by() creates a diffuse material whose reflectance is a ScalarField.
It also supports categorical fields, reversed or custom colormaps, an explicit
output range, custom legends, and two-sided rendering. Use hkw.inspect() when
the scalar attribute name or element domain is not known.
Overlay mesh edges¶
# Default: diameter is 0.5% of the scene or ROI-box diagonal.
with_edges = colored.show_edges(color="black")
# Half-pixel diameter at the camera target plane.
pixel_edges = colored.show_edges(width=0.5, width_space="screen")
# Legacy object-space radius; visible diameter is twice this value.
world_edges = colored.show_edges(width=0.005, width_space="world")
show_edges() returns the original visualization overlaid with a curve-mark
view of the same data. Its width always describes visible diameter in
"scene" and "screen" spaces, but retains radius semantics in legacy
"world" space.
The CLI uses screen space: --wire-thickness 0.5 means a 0.5-pixel diameter.
Add vector glyphs¶
with_velocity = colored.glyph_vectors(
"velocity",
scale=0.2,
size=0.01,
color="white",
normalize=False,
end_type="arrow",
)
Vector scale controls glyph length and size controls thickness. By default,
the glyph layer is overlaid on the input; pass overlay=False to return only
the vector visualization.
Slice with a plane¶
offset is signed distance along the normalized plane normal. Use point=
instead when the plane must pass through a specific point.
Isolate a connected component¶
By default, Hakowan computes connected-component IDs in a temporary
component attribute and keeps the requested facet group. To use existing
labels instead:
The generated filter uses the restricted expression system and remains fully serializable.
Compare two layers¶
comparison = before.compare(
after,
axis="x",
gap=0.1,
normalize=True,
labels=("Before", "After"),
)
compare() is a convenience over juxtapose(). Labels name layers in the
interactive WebGL controls; static Blender and Mitsuba images do not draw them.
Arrange layers in a grid¶
hkw.grid() wraps a flat, row-major sequence without manually nesting
horizontal and vertical juxtaposition nodes:
matrix = hkw.grid(
[distance_0, distance_1, distance_2, surface_0, surface_1, surface_2],
columns=3,
column_axis="x",
row_axis="z",
row_gap=0.1,
column_gap=0.05,
normalize=True,
)
Specify exactly one of columns or rows. The first input row appears at the
top, and a ragged final row is centered. gap sets both directions by default;
row_gap and column_gap override either direction independently. Gap values
may be negative to move adjacent views closer together or make them overlap.
normalize=True gives every input layer a common scale before packing. The
result is an ordinary composed Layer, so it serializes and renders through
the same paths as nested juxtapose() calls.
Layer composition¶
In the following example, we will demonstrate the idea of layer composition.
base = hkw.layer("shape.obj")
surface_view = base.mark(hkw.mark.Surface)
point_view = base.mark(hkw.mark.Point)
edge_view = base.mark(hkw.mark.Curve)
composite_view = surface_view + point_view + edge_view
Here, surface_view is a visualization of the surface geometry, while point_view and edge_view
are the visualizations of vertices and edges of the geometry. The addition operations combines all
three views together to form a composite view that visualizes all three elements.
Layer comparison¶
While + overlays layers in the same coordinate space, the | operator places layers side by
side so they can be compared.
This lays out the two layers in a horizontal row, automatically translating them apart so they do not overlap. The original relative scale of each layer is preserved.
The & operator is the vertical analogue of |: it stacks layers in a column instead of a row.
!!! note
Python binds & tighter than |, so a | b & c parses as a | (b & c). Parenthesise when
mixing the two operators.
For more control, use the juxtapose method, which
| and & call with default settings (axis="x" and axis="y" respectively):
comparison = base.juxtapose(
other,
axis="y", # lay out along Y instead of the default X
gap=0.2, # spacing between cells, as a fraction of the mean cell size
normalize=True, # scale each cell to equal size before placing
)
Gap values may be negative when the default bounding-sphere separation leaves more space than the composition needs.
Each operand of | becomes one cell. Cells may themselves be composite layers, so +, |, and
& combine freely:
# Compare a bare surface against the same surface with its wireframe overlaid.
surface = hkw.layer("shape.obj").mark(hkw.mark.Surface)
edges = hkw.layer("shape.obj").mark(hkw.mark.Curve)
comparison = surface | (surface + edges)
Nesting | and & builds a 2-D grid — each operator lays out its own operands along its axis,
so a row of cells can be stacked over another, or several rows tiled into a matrix:
top = a | b # a row
grid = (a | b) & c # the a–b row stacked above c
matrix = (a | b) & (c | d) # a 2x2 grid
Cells are spaced by their bounding spheres, so they never overlap — even as you rotate each cell in the interactive viewer.
Annotations¶
Attach deterministic screen-space labels with annotate():
layer = hkw.layer("shape.obj").annotate(
"Simulation A",
position=(0.5, 0.05),
anchor="center",
background="black",
)
Annotations follow layer inheritance and are deduplicated when composite views are compiled. See Legends and annotations.