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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ semantic versioning.

### Added

- Phase 3 corkscrew: the 3-D walk.
- `layout.walk3d_layout` — the 2-D walk lifted by a constant step in z.
- `render.render_path_3d` — a matplotlib 3-D path renderer with per-segment
colours and line widths.
- `text_walk(..., helix=True, z_step=...)` winds the walk into a corkscrew;
`examples/text_walk_3d_demo.py` and docs.
- Phase 2 walk + spiral: the first two presets and their layouts.
- `layout.walk_layout` / `heading_angles` — the 2-D turtle walk (step forward,
turn 90°), ported from the Wittgenstein piece's `compute_path`.
Expand Down
4 changes: 4 additions & 0 deletions docs/api/render.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ Draw laid-out, encoded units as a matplotlib `Figure`.
::: lexograph.render.mpl.render_points

::: lexograph.render.mpl.render_path

::: lexograph.render.mpl3d.render_path_3d

::: lexograph.render.mpl.frame_axes
5 changes: 5 additions & 0 deletions docs/api/walk3d.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 3-D walk layout

The corkscrew: the 2-D walk lifted by a constant step in z.

::: lexograph.layout.walk3d.walk3d_layout
18 changes: 18 additions & 0 deletions docs/examples/text_walk_3d.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 3-D text walk

The bundled chapter as a 3-D corkscrew: the same space-filling turtle walk, but
each sentence also climbs a constant step in z, so the rectangular walk winds
upward. Rendered with matplotlib's `mplot3d`.

```python
--8<-- "examples/text_walk_3d_demo.py"
```

Run it with:

```bash
uv run python examples/text_walk_3d_demo.py
```

It is the same `text_walk` preset as the 2-D walk, with `helix=True`. Raise
`z_step` for a taller, more tightly wound corkscrew.
27 changes: 27 additions & 0 deletions examples/text_walk_3d_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""3-D text-walk demo: the chapter lifted into a corkscrew.

Run with: uv run python examples/text_walk_3d_demo.py
Writes text_walk_3d.png to the current directory.
"""

from lexograph import load_demo_text, segment, text_walk


def main() -> None:
"""Walk the bundled chapter as a 3-D corkscrew, coloured by position."""
text = load_demo_text()
units = segment(text, unit="sentences")
print(f"Winding {len(units)} sentences into a corkscrew.")

fig = text_walk(
text,
colour=list(range(len(units))),
helix=True,
z_step=4.0,
)
fig.savefig("text_walk_3d.png", dpi=150)
print("Saved text_walk_3d.png")


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ nav:
- Examples:
- examples/punctuation_spiral.md
- examples/text_walk.md
- examples/text_walk_3d.md
- examples/spine_demo.md
- Troubleshooting: troubleshooting.md
- API Reference:
Expand All @@ -61,6 +62,7 @@ nav:
- api/segment.md
- api/layout.md
- api/walk.md
- api/walk3d.md
- api/spiral.md
- api/widths.md
- api/channels.md
Expand Down
5 changes: 4 additions & 1 deletion src/lexograph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@
linear_layout,
rendered_widths,
spiral_layout,
walk3d_layout,
walk_layout,
)
from lexograph.presets import punctuation_spiral, text_walk
from lexograph.render import frame_axes, render_path, render_points
from lexograph.render import frame_axes, render_path, render_path_3d, render_points
from lexograph.segment import segment

__all__ = [
"segment",
"linear_layout",
"walk_layout",
"walk3d_layout",
"spiral_layout",
"rendered_widths",
"normalize_size",
Expand All @@ -39,6 +41,7 @@
"Channels",
"render_points",
"render_path",
"render_path_3d",
"frame_axes",
"punctuation_spiral",
"text_walk",
Expand Down
2 changes: 2 additions & 0 deletions src/lexograph/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
from lexograph.layout.linear import linear_layout
from lexograph.layout.spiral import spiral_layout, tangent_angles
from lexograph.layout.walk import heading_angles, walk_layout
from lexograph.layout.walk3d import walk3d_layout
from lexograph.layout.widths import rendered_widths

__all__ = [
"linear_layout",
"walk_layout",
"walk3d_layout",
"heading_angles",
"spiral_layout",
"tangent_angles",
Expand Down
64 changes: 64 additions & 0 deletions src/lexograph/layout/walk3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""The 3-D corkscrew layout: the 2-D walk lifted by a constant step in z.

This is the 2-D turtle walk (:func:`lexograph.layout.walk.walk_layout`) with a
fixed vertical lift added per unit, so the rectangular walk winds upward into a
corkscrew. It mirrors the Wittgenstein piece's helical handwriting layout, where
each sentence both advances the in-plane turtle and climbs a constant ``z_step``.
The geometry is plain coordinate generation; the renderer is matplotlib 3-D.
"""

from __future__ import annotations

from collections.abc import Iterable

import numpy as np

from lexograph._types import Coords
from lexograph.layout.walk import walk_layout

__all__ = ["walk3d_layout"]


def walk3d_layout(
steps: Iterable[float],
*,
turn: float = -90.0,
z_step: float = 1.0,
scale: float = 1.0,
start: tuple[float, float, float] = (0.0, 0.0, 0.0),
heading: tuple[float, float] = (1.0, 0.0),
) -> Coords:
"""Walk the 2-D turtle in the xy-plane while climbing a constant step in z.

Args:
steps: One forward (in-plane) step length per unit.
turn: Degrees to turn the in-plane heading after each step.
z_step: The constant vertical lift added per unit (the corkscrew pitch).
scale: A multiplier applied to every in-plane step length.
start: The starting ``(x, y, z)`` position.
heading: The initial in-plane heading; normalised internally.

Returns:
An ``(N + 1, 3)`` float array of vertices. Row 0 is ``start``; the
``z`` coordinate of row ``i`` is ``start[2] + i * z_step``.

Contract:
- The result has exactly ``N + 1`` rows for ``N`` steps.
- The ``z`` column increases by exactly ``z_step`` between consecutive
rows.
- The ``xy`` columns equal the 2-D walk with the same parameters.

Examples:
>>> walk3d_layout([1.0, 1.0], z_step=2.0).round(6).tolist()
[[0.0, 0.0, 0.0], [1.0, 0.0, 2.0], [1.0, -1.0, 4.0]]
"""
lengths = list(steps)
xy = walk_layout(
lengths,
turn=turn,
scale=scale,
start=(start[0], start[1]),
heading=heading,
)
z = start[2] + np.arange(len(lengths) + 1, dtype=float) * z_step
return np.column_stack([xy, z])
44 changes: 36 additions & 8 deletions src/lexograph/presets/text_walk.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
normalize_size,
)
from lexograph.layout.walk import heading_angles, walk_layout
from lexograph.layout.walk3d import walk3d_layout
from lexograph.layout.widths import rendered_widths
from lexograph.render.mpl import frame_axes
from lexograph.render.mpl3d import render_path_3d
from lexograph.segment.units import sentences as split_sentences

if TYPE_CHECKING:
Expand Down Expand Up @@ -75,12 +77,14 @@ def text_walk(
colour_kind: ColourKind = "auto",
size: Sequence[float] | None = None,
mode: WalkMode = "path",
helix: bool = False,
z_step: float = 1.0,
font: FontProperties | str | Path | None = None,
font_size: float = 12.0,
width_step: bool = True,
turn: float = -90.0,
background: str = "white",
figsize: tuple[float, float] = (10.0, 10.0),
figsize: tuple[float, float] | None = None,
) -> Figure:
"""Draw a text as a space-filling turtle walk over its sentences.

Expand All @@ -93,21 +97,29 @@ def text_walk(
size: One scalar per sentence for the size channel. ``None`` sizes by
sentence length.
mode: ``"path"`` draws a multi-coloured ribbon; ``"glyphs"`` sets each
sentence's text along its segment.
sentence's text along its segment. Only ``"path"`` is supported when
``helix`` is set.
helix: If ``True``, lift the walk into a 3-D corkscrew (each sentence also
climbs ``z_step``) and render it in matplotlib 3-D.
z_step: The vertical lift per sentence for the corkscrew (used when
``helix`` is set).
font: Font to measure widths and (in glyph mode) draw with.
font_size: Base font size in points for width measurement and glyphs.
width_step: If ``True``, step by each sentence's rendered width; if
``False``, step by its character count.
turn: Degrees to turn between sentences (``-90`` is the rectangular walk).
background: Figure and axes background colour.
figsize: Figure size in inches.
figsize: Figure size in inches. Defaults to ``(10, 10)`` flat, or
``(8, 10)`` for the helix.

Returns:
A :class:`matplotlib.figure.Figure` with one axes. Never calls ``show()``.
A :class:`matplotlib.figure.Figure` with one axes (2-D, or 3-D for the
helix). Never calls ``show()``.

Raises:
ValueError: If the text has fewer than two sentences, or a channel length
does not match the sentence count.
ValueError: If the text has fewer than two sentences, a channel length
does not match the sentence count, or ``helix`` is combined with
glyph mode.

Contract:
- Returns a Figure with exactly one axes.
Expand All @@ -118,7 +130,13 @@ def text_walk(
>>> fig = text_walk(load_demo_text())
>>> type(fig).__name__
'Figure'
>>> helix = text_walk(load_demo_text(), helix=True)
>>> type(helix).__name__
'Figure'
"""
if helix and mode == "glyphs":
msg = "glyph mode is not supported for the 3-D helix walk"
raise ValueError(msg)
units = split_sentences(text)
n = len(units)
if n < 2:
Expand All @@ -129,7 +147,6 @@ def text_walk(
steps = rendered_widths(units, prop=font, size=font_size)
else:
steps = np.array([len(u) for u in units], dtype=float)
coords = walk_layout(steps, turn=turn)

colours = _resolve_colours(colour, n, colour_kind)
raw_size = [float(s) for s in size] if size is not None else [len(u) for u in units]
Expand All @@ -138,7 +155,18 @@ def text_walk(
raise ValueError(msg)
weight = normalize_size(raw_size, lo=0.0, hi=1.0)

fig = Figure(figsize=figsize, facecolor=background)
if helix:
coords3d = walk3d_layout(steps, turn=turn, z_step=z_step)
return render_path_3d(
coords3d,
colors=colours,
linewidths=[0.6 + 6.0 * float(w) for w in weight],
background=background,
figsize=figsize or (8.0, 10.0),
)

coords = walk_layout(steps, turn=turn)
fig = Figure(figsize=figsize or (10.0, 10.0), facecolor=background)
ax = fig.subplots()
ax.set_facecolor(background)

Expand Down
3 changes: 2 additions & 1 deletion src/lexograph/render/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Render: draw a laid-out, encoded text as a matplotlib Figure."""

from lexograph.render.mpl import frame_axes, render_path, render_points
from lexograph.render.mpl3d import render_path_3d

__all__ = ["render_points", "render_path", "frame_axes"]
__all__ = ["render_points", "render_path", "render_path_3d", "frame_axes"]
Loading