Skip to content

Leaf shapes

gutenberg_kg.leafshapes

Species leaf outlines for the hero tree.

Each species grows a leaf of its own outline rather than one flattened ellipsoid: a lobed oak leaf, a palmate plane leaf, a fir or pine needle spray, and ovate leaves of different widths for the rest. The outlines are the Knowledge Press web forest's (web/src/game/species.ts in knowledge_press), in true proportions: a leaf runs from its stalk at y = -1 to its tip at y = 1, x across it.

Pure NumPy; :func:leaf_prototype turns an outline into the PyVista mesh a glyph call stamps at every leaf.

Author: Eric G. Suchanek, PhD

LeafShape(half=None, width=0.36, smooth=False) dataclass

A leaf outline.

Parameters:

Name Type Description Default
half tuple[tuple[float, float], ...] | None

For an outlined leaf, the right half from just below the tip down to the stalk, as (x, y) pairs; mirrored for the left half. None for an ovate leaf.

None
width float

For an ovate leaf, its width relative to a chestnut leaf's.

0.36
smooth bool

Round the outline through a Catmull-Rom spline; otherwise the corners stay sharp (needles, the plane leaf's points).

False

leaf_outline(shape)

A closed leaf polygon, starting at the tip, stalk at y = -1.

Parameters:

Name Type Description Default
shape LeafShape

The leaf.

required

Returns:

Type Description
ndarray

(K, 2) outline, mirror-symmetric about x = 0.

Source code in src/gutenberg_kg/leafshapes.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def leaf_outline(shape: LeafShape) -> np.ndarray:
    """
    A closed leaf polygon, starting at the tip, stalk at ``y = -1``.

    :param shape: The leaf.
    :return: ``(K, 2)`` outline, mirror-symmetric about ``x = 0``.
    """
    if shape.half is None:
        k = shape.width / 0.36 * _OVATE_TRUE
        right = _cubic((0, 1), (0.62, 0.58), (0.48, -0.12), (0.1, -0.82), 7)
        left = _cubic((-0.1, -0.82), (-0.48, -0.12), (-0.62, 0.58), (0, 1), 7)
        pts = [*right, (0.0, -1.0), *left[:-1]]
        return _with_stalk([(x * k, y) for x, y in pts])
    right = [(0.0, 1.0), *shape.half]
    half = _catmull_rom(right, 2) if shape.smooth else right
    bottom = half[-1]
    full = [*half, (0.0, bottom[1] - 0.02), *[(-x, y) for x, y in reversed(half[1:])]]
    return _with_stalk(full)

leaf_prototype(species, size)

One species leaf as a flat triangulated mesh, ready to stamp as a glyph.

The blade runs along +x -- the axis a glyph call turns onto each leaf's direction -- and lies in the xy plane. It is size from centre to tip before :func:leaf_scale enlarges it to cover the same area as the ellipsoid leaf of that size.

Parameters:

Name Type Description Default
species str

A key of :data:LEAF_SHAPES; unknown species get the chestnut leaf.

required
size float

The ellipsoid leaf's radius this leaf replaces, in scene units.

required

Returns:

Type Description
PolyData

The prototype PolyData.

Source code in src/gutenberg_kg/leafshapes.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def leaf_prototype(species: str, size: float) -> pv.PolyData:
    """
    One species leaf as a flat triangulated mesh, ready to stamp as a glyph.

    The blade runs along ``+x`` -- the axis a glyph call turns onto each
    leaf's direction -- and lies in the ``xy`` plane.  It is ``size`` from
    centre to tip before :func:`leaf_scale` enlarges it to cover the same
    area as the ellipsoid leaf of that size.

    :param species: A key of :data:`LEAF_SHAPES`; unknown species get the
        chestnut leaf.
    :param size: The ellipsoid leaf's radius this leaf replaces, in scene units.
    :return: The prototype ``PolyData``.
    """
    import pyvista as pv

    size = size * leaf_scale(species)
    outline = leaf_outline(LEAF_SHAPES.get(species, LEAF_SHAPES["chestnut"]))
    pts = np.column_stack([outline[:, 1] * size, outline[:, 0] * size, np.zeros(len(outline))])
    face = np.concatenate([[len(pts)], np.arange(len(pts))])
    return pv.PolyData(pts, face).triangulate()

leaf_scale(species)

How much a species' outline is enlarged so its leaf covers as much as the ellipsoid leaf it replaces, between 1 and :data:MAX_LEAF_SCALE.

Parameters:

Name Type Description Default
species str

A key of :data:LEAF_SHAPES.

required

Returns:

Type Description
float

Uniform scale on the unit outline.

Source code in src/gutenberg_kg/leafshapes.py
189
190
191
192
193
194
195
196
197
198
def leaf_scale(species: str) -> float:
    """
    How much a species' outline is enlarged so its leaf covers as much as the
    ellipsoid leaf it replaces, between 1 and :data:`MAX_LEAF_SCALE`.

    :param species: A key of :data:`LEAF_SHAPES`.
    :return: Uniform scale on the unit outline.
    """
    area = outline_area(leaf_outline(LEAF_SHAPES.get(species, LEAF_SHAPES["chestnut"])))
    return float(np.clip(np.sqrt(ELLIPSOID_LEAF_AREA / area), 1.0, MAX_LEAF_SCALE))

outline_area(outline)

Area enclosed by a closed (K, 2) outline (shoelace formula).

Source code in src/gutenberg_kg/leafshapes.py
183
184
185
186
def outline_area(outline: np.ndarray) -> float:
    """Area enclosed by a closed ``(K, 2)`` outline (shoelace formula)."""
    x, y = outline[:, 0], outline[:, 1]
    return float(abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))) / 2.0)