show() works in Jupyter notebooks — Visualizer.show() and
VizSceneHandle.show() now accept a jupyter option (None auto-detects,
True forces the notebook display, False forces a browser tab) and, in
Jupyter, delegate to display() so show() is the single entry point for
showing a scene.
Idempotent display() / show() in Jupyter — within a single cell
execution, repeated calls no longer open a second viewer; they flush the
latest scene state into the already-open viewer, keyed by an optional
viewer_name, the current notebook cell id (via the IPython pre_run_cell
event), or the scene name.
Scene context managers — Visualizer and VizSceneHandle support
with viz: and with viz.scene("name"):, which reset the scene (clearing
entities but keeping the default axes/grid) and call show() on entry, so
each flush() inside the block updates the live viewer (e.g. an
animate() loop), then flush on exit.
animate(auto_clear=True) — Visualizer.animate() (and
VizSceneHandle.animate()) gained an auto_clear flag: each frame flushes
first, then removes every object added after the loop began (anything added
before the loop persists), so per-frame add() calls no longer accumulate.
viz(...) shorthand for new() — Visualizer is now callable:
viz(Point(1, 2, 3), color="...") returns a VizObjectRef (equivalent to
viz.new(...)), making the pre-create + update animation pattern concise.
Visualizer docs restructured around use cases — added
use-cases-scripts.md, use-cases-notebooks.md, and app.md, a use-case
routing list above the topics table in docs/py/viz/index.md, and an
export/ subsection (index.md, html.md, gltf.md, video-image.md)
replacing the single export.md; docs/py/index.md now covers only the
geometric algebra part and links to the Visualization section.
Jupyter notebook and multi-scene examples — added interactive.ipynb,
animation.ipynb, and export.ipynb under py/examples/jupyter/, plus a
py/examples/viz/demo_multi_scene.py script showing two named scenes in
separate browser tabs.
Scene title in the browser tab — the scene title now also sets the
browser tab title (in addition to the viewport overlay), truncated to 40
characters with an ellipsis when longer.
Browser-triggered full-server stop key — a new, opt-in per-scene binding
(default Ctrl+Q) ends wait() and every animate() loop by setting the
global shutdown event, mirroring a terminal Ctrl+C. Enabled via
enable_server_stop_key(), the Visualizer(enable_server_stop_key=True)
flag (main scene), or viz.scene("name", enable_server_stop_key=True);
VizSceneHandle.enable_server_stop_key() scopes it per named scene.
Disabled by default.
Operator visualizations reworked — GeneralRotor now renders with the
same rotor visualization as Rotor (disc arc, torus, and axis line),
displaced to its origin, and Motor renders a displaced rotation (general
rotor) plus a translation arrow along the screw axis.
animate() no longer opens the viewer — Visualizer.animate() (and
VizSceneHandle.animate()) previously opened a browser tab in scripts; it now
only starts the server (headless) and never displays. Call show() first (or
use with viz:) to open the viewer, then drive the loop with animate().
show() is now the single display entry point.
meet()/join() inverted for BasisPGA2/BasisPGA3 to match Gunn/Dorst
— in the plane-based PGA models, MV.meet() is now the intersection (outer
product) and MV.join() the union (regressive product); all other algebras
(E2/E3/P2/P3/N2/N3) keep the previous semantics. Code that used a.meet(b)
on PGA to join two points must switch to a.join(b). The outer/inner
products (^, |) are unchanged. Incidence in PGA2/3 is now documented
(A.dual() ^ B.dual() == 0; see docs/py/basis/basis_pga3.md and
basis_pga2.md).
BasisPGA2/BasisPGA3 rename e0_inv to e0_recip — the null-vector
reciprocal attribute e0_inv is renamed to e0_recip to make clear it is
the reciprocal of e0 (satisfying e0 · e0_recip = 1), not its inverse.
display() hints when the server is not running in Jupyter —
Visualizer.display() and VizSceneHandle.display() now print a hint to
call start_server() (and skip the iframe) when invoked in a Jupyter
notebook before the server is running, instead of rendering an empty frame.
PGA3 line offset sign — create_pga3._line_opns now builds its two
defining planes with d = -(n·origin) (matching _plane_opns), and
analysis_pga3._line_origin_from_planes drops the compensating double
negation, so lines analyze to the correct side of the origin on both the
create → analyze round-trip and the join → analyze path.
PGA meet/join use the outer product (not the span) — the PGA
meet/join now follow Gunn/Dorst exactly: meet is the outer product
(intersection) and join is the regressive product ⋆(⋆A ∧ ⋆B), so the join
of incident elements vanishes (e.g. point.join(line) is zero when the point
lies on the line).
Infinite lines render centered on their closest point — the serializer
now emits the start point (closest − d̂·length/2) for infinite lines, so the
line is drawn centered on the point of the line closest to the origin instead
of stretching one-directionally from it. glTF lines now also honor their
direction and are centered consistently. Planes were already centered.
Animation restarts after a browser q stop — animate() now clears the
scene's per-scene interrupt event at the start of each loop, so re-running a
cell after pressing the q stop key starts a fresh animation instead of
immediately ending. The global Ctrl+C/SIGTERM shutdown is unaffected.
Per-scene titles no longer leak across scenes — SceneConfig.to_dict()
now includes the scene name as a scene field, matching the
scene_update/object_update message shape. Previously a broadcast title
update (e.g. detail.set_title(...)) was applied by every open browser tab,
overwriting the title of the main/other scenes; the frontend now filters
scene_config messages per scene.
Three visualization pages were missing from the nav — texture-labels.md,
object-interaction.md, and the active-elements/ folder were on disk but
absent from the mkdocs.yml nav; the restructured Visualization nav now
includes them.
Plane/space/reflection-plane extent style overrides had no effect — the
serializer always emitted the builtin default as a flat extent field, and
the scene-graph serializer mirrored that flat value back into the style,
overwriting a per-call PlaneStyle(extent=...) (and SpaceStyle /
ReflectionPlaneStyle). The Python side now resolves the extent up front
(entity → style → canonical default) and the renderers read the resolved
content field, matching the line-length behaviour.
Visualization docs restructured into topic folders — the flat
docs/py/viz/ pages were reorganized into scene-objects/, visualizer/,
visualizerapp/, jupyter/, and styles/ (plus the existing export/),
with new section overview pages, a compiled entities.ipynb notebook of
entity styles with embedded static viewers, a dedicated operators.md, and
the jupyter.md page split into live (markdown) and static (notebook)
guides. mkdocs-jupyter was added to compile the notebooks at build time.
Motor normalized to a screw form — Motor now stores a GeneralRotor
(rotation about a displaced axis) plus an axial Translator, computed on
construction from the rotation angle/axis and the translation vector with
pure vector arithmetic (u = t_∥, v = ½(t_⊥ + cot(φ/2)·â×t_⊥)).
analyze_operator, create_motor, the serializer, the renderer, and
_transforms all use this form, with the derivation documented in
motor-decomposition.md.