Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8d0880c
refactor: promote pleat.io to a subpackage (heg + circlepack)
imagirom Jul 17, 2026
2405a29
feat: FOLD v1.2 crease-pattern I/O + open in Origami Simulator
imagirom Jul 17, 2026
7c8bb89
docs: FOLD export and Open-in-Origami-Simulator section
imagirom Jul 17, 2026
200ee39
test: import _build_heg_from_data from pleat.io.circlepack after io s…
imagirom Jul 17, 2026
10cddb8
fix: suppress Origami Simulator's default model to end import race
imagirom Jul 18, 2026
66d080e
feat: origami_simulator_iframe — embed Origami Simulator inline in a …
imagirom Jul 18, 2026
d0d9478
improve origami_simulator_iframe: fullscreen, bottom-right, immediate…
imagirom Jul 18, 2026
c7f0123
refactor: split Origami Simulator into pleat.origami_simulator; simpl…
imagirom Jul 18, 2026
ffd9f6d
feat: G.save() dispatches by extension; G.origami_simulator() on Geom…
imagirom Jul 18, 2026
1a741ba
feat: G.save() with no extension writes the whole bundle (svg+png+heg…
imagirom Jul 18, 2026
58ea9e4
feat: button title + show Origami Simulator button in FoldResult.show()
imagirom Jul 19, 2026
9e3a9e1
fix: reverse FOLD face winding so Origami Simulator shows the coloure…
imagirom Jul 19, 2026
d9a07ec
fix: .heg serialization of string attributes (e.g. hex colour_key)
imagirom Jul 19, 2026
85d336f
refactor: colour creases via color_creases() helper
imagirom Jul 19, 2026
08a3646
docs: update notebooks for the FOLD / G.save() bundle and Origami Sim…
imagirom Jul 19, 2026
8f7c918
chore: drop docs/superpowers/ (design spec + plan) from the repo
imagirom Jul 19, 2026
e156cb8
review: harden OS handshake, guard notebook-only display, robust file…
imagirom Jul 20, 2026
80816c8
docs: fix notebook typos flagged in review (flavours; 'a curved arc')
imagirom Jul 20, 2026
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
26 changes: 23 additions & 3 deletions docs/index.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,26 @@
"cell_type": "markdown",
"id": "8",
"metadata": {},
"source": [
"## Bonus: Open in origami simulator\n",
"\n",
"Use `.origami_simulator()` on a crease pattern to open [Amanda Ghassaeis](https://amandaghassaei.com/) [origami simulator](https://origamisimulator.org/) inline in a jupyter notebook (or in a new tab if called from a script). \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9",
"metadata": {},
"outputs": [],
"source": [
"fold_result.CP.origami_simulator()"
]
},
{
"cell_type": "markdown",
"id": "10",
"metadata": {},
"source": [
"## What next?\n",
"\n",
Expand All @@ -126,7 +146,7 @@
},
{
"cell_type": "markdown",
"id": "9",
"id": "11",
"metadata": {},
"source": [
"## Personal release note\n",
Expand All @@ -143,7 +163,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "pleat_claude",
"display_name": "pleat (3.13.8)",
"language": "python",
"name": "python3"
},
Expand All @@ -157,7 +177,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.19"
"version": "3.13.8"
}
},
"nbformat": 4,
Expand Down
25 changes: 18 additions & 7 deletions docs/notebooks/Alternating_Flagstones.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
"source": [
"## 6. Curved-fold subdivision (Origami Simulator)\n",
"\n",
"Each ridge crease, when folded, ought to trace a *circular arc* — both endpoints are equidistant from the apex star vertex. [`subdivide_ridges_for_curved_fold`](../reference/pleat/alternating_flagstones.md#pleat.alternating_flagstones.subdivide_ridges_for_curved_fold) replaces every ridge with `n` short straight edges chosen so that the polyline approximates that arc. The result imports cleanly into [Origami Simulator](https://origamisimulator.org/)."
"Each ridge crease, when folded, ought to trace a curved arc. [`subdivide_ridges_for_curved_fold`](../reference/pleat/alternating_flagstones.md#pleat.alternating_flagstones.subdivide_ridges_for_curved_fold) replaces every ridge with `n` short straight edges chosen so that the polyline approximates that arc. The result could in principle be folded correctly in [Origami Simulator](https://origamisimulator.org/), however in practice it oftentimes struggles. Assigning correct small fold angles to these extra subdividing creases could be a way to fix this in the future."
]
},
{
Expand All @@ -264,16 +264,19 @@
"metadata": {},
"outputs": [],
"source": [
"subdivided = alternating_flagstones.subdivide_ridges_for_curved_fold(CP_cleaned, n_subdivisions=10)\n",
"from pleat.origami_simulator import origami_simulator_button\n",
"\n",
"\n",
"subdivided = alternating_flagstones.subdivide_ridges_for_curved_fold(CP_cleaned, n_subdivisions=5)\n",
"\n",
"multi_show(\n",
" [CP_cleaned, subdivided],\n",
" titles=['straight ridges', 'subdivided ridges (10 segments)'],\n",
" titles=['straight ridges', 'subdivided ridges (5 segments)'],\n",
" render_faces=False, render_vertices=False, line_width=0.01,\n",
")\n",
"\n",
"# Uncomment below to export for Origami Simulator\n",
"# subdivided.save('alternating_flagstone_subdivided.svg')\n"
"origami_simulator_button(CP_cleaned, title=\"Load Origami Simulator with straight ridges\")\n",
"origami_simulator_button(subdivided, title=\"Load Origami Simulator with subdivided ridges\")\n"
]
},
{
Expand Down Expand Up @@ -311,11 +314,19 @@
"\n",
"tempdir.cleanup()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "pleat",
"display_name": "pleat (3.13.8)",
"language": "python",
"name": "python3"
},
Expand All @@ -329,7 +340,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
"version": "3.13.8"
}
},
"nbformat": 4,
Expand Down
102 changes: 79 additions & 23 deletions docs/notebooks/Saving_and_Exporting.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@
"Once you have a CP you like, you'll want to send it somewhere — a plotter, a folder simulator, or a 3D printer. This notebook covers the export formats `pleat` ships with:\n",
"\n",
"- `.heg` — pleat's native YAML serialization.\n",
"- SVG — vector for laser cutters / pen plotters.\n",
"- A high-level `overlap.save_results` that writes a whole result directory in one call.\n",
"\n",
"STL files can be generated via marching cubes, e.g. for 3D printers. It requires the `[threed]` install extra.\n",
"\n",
"The [FOLD format](https://github.com/edemaine/fold) is currently not supported, but would be nice to have in the future."
"- `.fold` — the standard [FOLD](https://github.com/edemaine/fold) interchange format.\n",
"- SVG — In two flavours, one for pretty pictures, one optimized for laser cutters / pen plotters.\n",
"- A high-level `overlap.save_results` that writes a whole result directory in one call."
]
},
{
Expand All @@ -27,12 +24,10 @@
"source": [
"import matplotlib\n",
"matplotlib.rcParams['figure.figsize'] = (5, 5)\n",
"from pleat import (\n",
" example_graphs,\n",
" example_tilesets,\n",
" rendering,\n",
")\n",
"from pleat.rendering import multi_show\n"
"import pleat\n",
"from pleat.rendering import multi_show, CREASE_PATTERN_PRESET\n",
"\n",
"import tempfile, os"
]
},
{
Expand All @@ -42,8 +37,9 @@
"metadata": {},
"outputs": [],
"source": [
"G = example_graphs.from_tiles(example_tilesets.platonic(4), rings=2)\n",
"G.recompute_lengths_and_angles()"
"G = pleat.example_graphs.from_tiles(pleat.example_tilesets.platonic(n=6), rings=2)\n",
"pleat.shrink_rotate.crease_orientation.assign_this_way_from_center(G)\n",
"G = pleat.shrink_rotate.shrink_rotate_pattern(G, alpha=0.6, factor=0.5)\n"
]
},
{
Expand All @@ -65,7 +61,6 @@
"metadata": {},
"outputs": [],
"source": [
"import tempfile, os\n",
"from pleat import io\n",
"\n",
"with tempfile.TemporaryDirectory() as d:\n",
Expand All @@ -80,6 +75,7 @@
"multi_show(\n",
" [G, G2],\n",
" titles=['original', 'loaded'],\n",
" **CREASE_PATTERN_PRESET\n",
")\n"
]
},
Expand All @@ -88,9 +84,9 @@
"id": "5",
"metadata": {},
"source": [
"## SVG export via `G.save(...)`\n",
"## Export via `G.save(...)`\n",
"\n",
"`G.save('out')` writes both `out.svg` and `out.png`. `G.show()` displays inline (vector SVG in Jupyter) without writing files. The SVG is the same vector drawing that `CairoRenderer` produced — open it in a browser or Inkscape for the full quality."
"`G.save('out')` writes all of `out.svg`, `out.png`, `out.heg` and `out.fold`. If you only want to save a subset, call with the appropriate file ending, e.g. `G.save('out.svg')`."
]
},
{
Expand All @@ -100,10 +96,9 @@
"metadata": {},
"outputs": [],
"source": [
"import tempfile, os\n",
"with tempfile.TemporaryDirectory() as d:\n",
" out = os.path.join(d, 'pattern')\n",
" G.save(out, **rendering.CREASE_PATTERN_PRESET)\n",
" G.save(out, **CREASE_PATTERN_PRESET)\n",
" print('files in temp dir:', sorted(os.listdir(d)))\n",
" print('SVG head:')\n",
" print(open(out + '.svg').read()[:200])"
Expand All @@ -116,23 +111,84 @@
"source": [
"## Plotter-ready SVG via `SvgwriteRenderer`\n",
"\n",
"For laser-cutter / pen-plotter pipelines the dedicated `SvgwriteRenderer` produces an SVG split into `{name}_borders.svg` / `{name}_interior.svg` (so you can use different tool heads for cut vs. score)."
"For laser-cutter / pen-plotter pipelines the dedicated `SvgwriteRenderer` produces an SVG split into `{name}_borders.svg` / `{name}_interior.svg` (so you can use different tool heads for cut vs. score).\n",
"\n",
"These can then e.g. be converted to `.hpgl` with inkscape for plotting, see `plot_cp.py`."
]
},
{
"cell_type": "markdown",
"cell_type": "code",
"execution_count": null,
"id": "8",
"metadata": {},
"outputs": [],
"source": [
"from pleat.rendering import SvgwriteRenderer\n",
"\n",
"with tempfile.TemporaryDirectory() as d:\n",
" out = os.path.join(d, 'pattern')\n",
" renderer = SvgwriteRenderer() \n",
" renderer.render_graph(out + '.svg', G)\n",
" print('files in temp dir:', sorted(os.listdir(d)))\n",
" print('SVG head:')\n",
" print(open(out + '.svg').read()[:200])\n"
]
},
{
"cell_type": "markdown",
"id": "9",
"metadata": {},
"source": [
"## All-in-one with `overlap.save_results`\n",
"\n",
"If you've gone through `fold_complete` (demonstrated in the [Shrink-Rotate notebook](Shrink_Rotate_Tessellations.ipynb)), `save_results(result, path)` writes the CP, both folded views, a back-lit composite, and a plotter-ready SVG in one call."
]
},
{
"cell_type": "markdown",
"id": "10",
"metadata": {},
"source": [
"## FOLD format\n",
"\n",
"[FOLD](https://github.com/edemaine/fold) is the standard origami interchange\n",
"format. `save_fold` writes a `.fold` file (crease pattern with M/V/B assignments\n",
"and fold angles); `load_fold` reads one back."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"from pleat.io import save_fold, load_fold\n",
"\n",
"\n",
"with tempfile.TemporaryDirectory() as d:\n",
" path = os.path.join(d, 'pattern.fold')\n",
" save_fold(path, G)\n",
" # cp.save(path) # also works\n",
" print('wrote', os.path.getsize(path), 'bytes of FOLD')\n",
" G_loaded = load_fold(path)\n",
"\n",
"\n",
"G_loaded.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "pleat",
"display_name": "pleat (3.13.8.final.0)",
"language": "python",
"name": "python3"
},
Expand All @@ -146,7 +202,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
"version": "3.13.8"
}
},
"nbformat": 4,
Expand Down
6 changes: 6 additions & 0 deletions pleat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
# I/O and rendering
import pleat.io

# Origami Simulator (a distinct feature from the FOLD format; depends on it).
# Access via ``pleat.origami_simulator.origami_simulator`` / ``.origami_simulator_button``,
# ``from pleat.origami_simulator import origami_simulator``, or the ``G.origami_simulator()``
# method. Not re-exported at the top level: that name is the module itself.
import pleat.origami_simulator

# Layout, classification, coloring, search
import pleat.layout
import pleat.overlap
Expand Down
56 changes: 49 additions & 7 deletions pleat/half.py
Original file line number Diff line number Diff line change
Expand Up @@ -1646,16 +1646,58 @@ def show(self, **style: object) -> None:
"""
self.render(**style).show()

def save(self, path: str, **style: object) -> None:
"""Render the graph and write it to *path*.

``path`` with no extension writes both ``path.svg`` and ``path.png``.
def save(self, path: str, **kwargs: object) -> None:
"""Save the graph to *path*; the file **extension selects the format**:

- ``.heg`` -- pleat's native half-edge serialization
(:func:`pleat.io.save_graph`). Kwargs: ``overwrite``, ``attributes_to_save``.
- ``.fold`` -- FOLD crease pattern for other origami tools / Origami
Simulator, Euclidean 2D only (:func:`pleat.io.save_fold`). Kwarg: ``overwrite``.
- ``.svg`` / ``.png`` -- a rendered picture; kwargs are forwarded to
:meth:`render` as style.
- no extension -- writes the whole bundle: ``path.svg``, ``path.png``,
``path.heg``, and ``path.fold`` (the ``.fold`` skipped for non-Euclidean
graphs). ``overwrite`` (default ``True``) applies to the ``.heg`` / ``.fold``
files; the remaining kwargs are render style.

Args:
path: Destination path; extension selects the format(s).
**style: Forwarded to :meth:`render`.
path: Destination path. Its extension picks the format -- ``.heg``,
``.fold``, ``.svg``, ``.png``, or none (writes the whole bundle:
``.svg`` + ``.png`` + ``.heg`` + ``.fold``).
**kwargs: Format-specific options (see above): render style for images,
``overwrite`` / ``attributes_to_save`` for ``.heg`` / ``.fold``.
"""
lower = path.lower()
if lower.endswith(".heg"):
from .io import save_graph

save_graph(path, self, **kwargs)
elif lower.endswith(".fold"):
from .io import save_fold

save_fold(path, self, **kwargs)
elif lower.endswith((".svg", ".png")):
self.render(**kwargs).save(path)
else:
# no extension: write the whole bundle
from .io import save_fold, save_graph

overwrite = bool(kwargs.pop("overwrite", True))
self.render(**kwargs).save(path) # path.svg + path.png
save_graph(path, self, overwrite=overwrite)
try:
save_fold(path, self, overwrite=overwrite)
except ValueError:
pass # non-Euclidean geometry: FOLD not applicable, skip it

def origami_simulator(self, *, height: int = 600, new_tab: bool = False) -> None:
"""Show this crease pattern in Origami Simulator (see :mod:`pleat.origami_simulator`).

Requires a Euclidean 2D crease pattern; raises ``ValueError`` otherwise.
"""
self.render(**style).save(path)
from .origami_simulator import origami_simulator

origami_simulator(self, height=height, new_tab=new_tab)

def central_face(self) -> Face:
"""Return the face whose midpoint is closest to the origin (Euclidean only)."""
Expand Down
Loading