pygeomatic 0.2__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {pygeomatic-0.2 → pygeomatic-0.2.2}/PKG-INFO +112 -1
  2. {pygeomatic-0.2 → pygeomatic-0.2.2}/README.md +111 -0
  3. {pygeomatic-0.2 → pygeomatic-0.2.2}/pyproject.toml +1 -1
  4. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/__init__.py +3 -0
  5. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/registry.py +6 -0
  6. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/store.py +2 -1
  7. pygeomatic-0.2.2/src/pygeomatic/tex.py +724 -0
  8. pygeomatic-0.2/src/pygeomatic/tex.py +0 -397
  9. {pygeomatic-0.2 → pygeomatic-0.2.2}/.gitignore +0 -0
  10. {pygeomatic-0.2 → pygeomatic-0.2.2}/LICENSE +0 -0
  11. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/article.py +0 -0
  12. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/coercions.json +0 -0
  13. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/coercions.py +0 -0
  14. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/emit.py +0 -0
  15. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/extensions.py +0 -0
  16. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/__init__.py +0 -0
  17. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/helpers.py +0 -0
  18. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/__init__.py +0 -0
  19. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/annotation_functions.py +0 -0
  20. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/array.py +0 -0
  21. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/autograd_functions.py +0 -0
  22. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/basic_figures.py +0 -0
  23. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/boolean_functions.py +0 -0
  24. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/complex_functions.py +0 -0
  25. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/curve_functions.py +0 -0
  26. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/intersections.py +0 -0
  27. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/ode_functions.py +0 -0
  28. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/planar_geometry.py +0 -0
  29. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/polygons.py +0 -0
  30. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/property_functions.py +0 -0
  31. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/rotation_functions.py +0 -0
  32. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/scalar_functions.py +0 -0
  33. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/special_functions.py +0 -0
  34. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/tangent_functions.py +0 -0
  35. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/tensor_functions.py +0 -0
  36. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/translation_functions.py +0 -0
  37. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/__init__.py +0 -0
  38. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/abs.py +0 -0
  39. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/add.py +0 -0
  40. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/create_overload.py +0 -0
  41. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/div.py +0 -0
  42. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/exp.py +0 -0
  43. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/log.py +0 -0
  44. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/mul.py +0 -0
  45. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/neg.py +0 -0
  46. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/pow.py +0 -0
  47. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/sqrt.py +0 -0
  48. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/sub.py +0 -0
  49. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/generate.py +0 -0
  50. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/inference.py +0 -0
  51. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/macros.json +0 -0
  52. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/macros.py +0 -0
  53. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/nodes.py +0 -0
  54. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/palette.py +0 -0
  55. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/parse.py +0 -0
  56. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/prompting.py +0 -0
  57. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/runner.py +0 -0
  58. {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/system_nodes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeomatic
3
- Version: 0.2
3
+ Version: 0.2.2
4
4
  Summary: Python mirror of the geomatic DSL function library; records calls for deterministic DSL emission
5
5
  Project-URL: Homepage, https://github.com/TinyVolt/pygeomatic
6
6
  Project-URL: Repository, https://github.com/TinyVolt/pygeomatic
@@ -160,6 +160,117 @@ uv run python scripts/compile_articles.py articles/ dist/ # a tree
160
160
  (equivalently `gm.compile_article(md)` in-process or `gm.run_article(md)` in a
161
161
  subprocess; both take `extensions=` / `macros=` / `allow_coercions=`.)
162
162
 
163
+ ### Live formulas (texatlas)
164
+
165
+ Make a KaTeX formula *addressable and reactive* — a slot showing a store node's
166
+ live value, matrix cells highlighted by a store node — with `gm.tex`. It is
167
+ **not** DSL: no `\tex-*` command exists and `emit()` never sees it. Bindings are
168
+ harvested straight from the session into a trailing `<!-- texatlas:v1 … -->`
169
+ comment on the compiled article, which the Nova reader applies at load. Reactivity
170
+ flows through the bound *node*: whatever drives it (a `\scalar` CommandLink, an
171
+ `\animate`) reflows the formula with no re-render.
172
+
173
+ Give the formula an id with a `%id:` line as the first line inside the `$$…$$`
174
+ block, then bind nodes to that id in a fence. (The id syntax is owned entirely
175
+ by the Nova/web reader — pygeomatic never parses the LaTeX; it only needs to
176
+ know which id string a `gm.tex(...)` call refers to.)
177
+
178
+ ````markdown
179
+ $$
180
+ %id:energy
181
+ \int_{a}^{b} x^2 \, dx
182
+ $$
183
+
184
+ ```pygeomatic
185
+ b = gm.scalar(3, out="b")
186
+ energy = gm.tex("energy") # matches %id:energy
187
+ energy.int.upper.bind(b) # show b's value in the upper limit
188
+ ```
189
+
190
+ Change it: {b = 5}(b = gm.scalar(5))
191
+ ````
192
+
193
+ Matrix cells are highlighted by **selectors** built from store nodes — the
194
+ selector returns a weight in `[0,1]`, so a fractional (`\animate`d) node
195
+ crossfades between adjacent rows:
196
+
197
+ ````markdown
198
+ $$
199
+ %id:M
200
+ \begin{pmatrix} a & b & c \\ d & e & f \\ g & h & i \end{pmatrix}
201
+ $$
202
+
203
+ ```pygeomatic
204
+ r = gm.scalar(0, out="r")
205
+ M = gm.tex("M")
206
+ M.highlight(M.rows() == r, color="pink") # row r
207
+ M.triu().highlight(color="blue") # upper triangle
208
+ ```
209
+
210
+ Move it: {row 1}(r = gm.scalar(1)) · {row 2}(r = gm.scalar(2))
211
+ ````
212
+
213
+ - **Value bind**: `t.<family>.<slot>.bind(node, show=, fmt=)`. Families/slots are
214
+ `int`/`sum`/`prod` → `lower`/`upper`/`body`, `frac` → `num`/`denom`,
215
+ `sqrt` → `body` (extend with `gm.register_tex_schema(family, slots)`). `fmt`
216
+ is `".2f"` / `"d"` (omit → trim to ≤4 dp); `show="symbol"` links without
217
+ substituting the glyph. Repeated command? `t.ints[1].upper` picks an
218
+ occurrence (discouraged — an edit silently retargets it).
219
+ - **Highlights**: build a selector over a cell's grid position and paint it.
220
+ Axes are `M.rows()` / `M.cols()` (or module-level `rows` / `cols` / `dim(i)`),
221
+ with arithmetic (`cols - rows`, `+`/`-`). Compare with `==` / `>=` / `<=` /
222
+ `>` / `<` (or `.eq/.ge/.le/.gt/.lt`) against a node or number; select
223
+ axis-aligned boxes with numpy-style slicing (`M[3:, 4:]`, node bounds stay
224
+ reactive); or use the named regions `M.diag()` / `M.triu()` / `M.tril()`.
225
+ Combine with `&` / `|` (`.and_` / `.or_`) and `.scale(node)` to fade a whole
226
+ selection — `scale` by a node + a `\scalar u 1` CommandLink gates a highlight
227
+ behind a click. Palette names (`"pink"`, `"BLUE"`) resolve to hex; raw
228
+ `"#f472b6"` / CSS names pass through. Full reference:
229
+ [docs/tex-highlight-ergonomics.md](docs/tex-highlight-ergonomics.md).
230
+ - **Reveal** (fade a part in): same selector machine, painting **opacity**
231
+ instead of color, so changing a gate node — reassigning it (`b = gm.bool_(True)`,
232
+ `k = gm.scalar(1)`) or animating it (`gm.animate(k, 3)`) in a CommandLink —
233
+ makes a piece of the formula appear. Three targets — an over/underbrace + label
234
+ (`t.underbrace.reveal(b)`, body stays visible; `.label` / `.body` for just
235
+ that part), a derivation line-by-line (`d.rows().reveal(rows < k)` — `k` lines
236
+ shown), or a matrix's rows/columns (`M.reveal(M.cols() < k)`). A bare gate
237
+ node/bool is the all-or-nothing case (`gm.bool_(False)` makes a false gate); a
238
+ scalar gives an animatable sweep. Full reference:
239
+ [docs/tex-reveal.md](docs/tex-reveal.md).
240
+
241
+ Two rules that will bite otherwise:
242
+
243
+ - **Bind replaces content, it never creates structure** — write placeholder
244
+ symbols in every slot you bind (`\int_{a}^{b}`, not a bare `\int`). An empty
245
+ slot fails validation.
246
+ - **The bound node must already exist** when you call `.bind()` / `.eq()` — define
247
+ it (`gm.scalar(…, out="b")`) above the binding so it is part of the runtime env.
248
+
249
+ **Repeated commands (`ints[i]`).** When one formula uses the same command twice
250
+ — e.g. `$$ \int_a^b f + \int_c^d g $$` — `t.int` is ambiguous. `t.ints` is the
251
+ list of all of them: `t.ints[0]` is the first `\int`, `t.ints[1]` the second.
252
+ The index is positional, so editing the formula (reordering or inserting an
253
+ `\int`) silently retargets it — prefer splitting into two formulas with distinct
254
+ ids, each with a single `\int`.
255
+
256
+ **Gating a highlight behind a click (`.scale`).** A bound highlight is on as
257
+ soon as it's bound. To reveal it only after a reader clicks, scale its weight by
258
+ a node that starts at `0`:
259
+
260
+ ```python
261
+ u = gm.scalar(0, out="u") # 0 → highlight invisible
262
+ M.highlight((M.rows() == r).scale(u)) # weight = row-weight × u
263
+ ```
264
+ ```markdown
265
+ {reveal}(u = gm.scalar(1)) # click sets u=1 → highlight appears
266
+ ```
267
+
268
+ `u=0` hides it, `u=1` shows it fully, `u=0.5` at half — the click is just a
269
+ normal CommandLink writing `u`.
270
+
271
+ `gm.harvest_tex_bindings(store)` returns the raw `{texId: {values, highlights}}`
272
+ manifest if you need it directly.
273
+
163
274
  ### Publishing from a content repo (GitHub Action)
164
275
 
165
276
  A content repo publishes compiled articles with one workflow file; a compile
@@ -139,6 +139,117 @@ uv run python scripts/compile_articles.py articles/ dist/ # a tree
139
139
  (equivalently `gm.compile_article(md)` in-process or `gm.run_article(md)` in a
140
140
  subprocess; both take `extensions=` / `macros=` / `allow_coercions=`.)
141
141
 
142
+ ### Live formulas (texatlas)
143
+
144
+ Make a KaTeX formula *addressable and reactive* — a slot showing a store node's
145
+ live value, matrix cells highlighted by a store node — with `gm.tex`. It is
146
+ **not** DSL: no `\tex-*` command exists and `emit()` never sees it. Bindings are
147
+ harvested straight from the session into a trailing `<!-- texatlas:v1 … -->`
148
+ comment on the compiled article, which the Nova reader applies at load. Reactivity
149
+ flows through the bound *node*: whatever drives it (a `\scalar` CommandLink, an
150
+ `\animate`) reflows the formula with no re-render.
151
+
152
+ Give the formula an id with a `%id:` line as the first line inside the `$$…$$`
153
+ block, then bind nodes to that id in a fence. (The id syntax is owned entirely
154
+ by the Nova/web reader — pygeomatic never parses the LaTeX; it only needs to
155
+ know which id string a `gm.tex(...)` call refers to.)
156
+
157
+ ````markdown
158
+ $$
159
+ %id:energy
160
+ \int_{a}^{b} x^2 \, dx
161
+ $$
162
+
163
+ ```pygeomatic
164
+ b = gm.scalar(3, out="b")
165
+ energy = gm.tex("energy") # matches %id:energy
166
+ energy.int.upper.bind(b) # show b's value in the upper limit
167
+ ```
168
+
169
+ Change it: {b = 5}(b = gm.scalar(5))
170
+ ````
171
+
172
+ Matrix cells are highlighted by **selectors** built from store nodes — the
173
+ selector returns a weight in `[0,1]`, so a fractional (`\animate`d) node
174
+ crossfades between adjacent rows:
175
+
176
+ ````markdown
177
+ $$
178
+ %id:M
179
+ \begin{pmatrix} a & b & c \\ d & e & f \\ g & h & i \end{pmatrix}
180
+ $$
181
+
182
+ ```pygeomatic
183
+ r = gm.scalar(0, out="r")
184
+ M = gm.tex("M")
185
+ M.highlight(M.rows() == r, color="pink") # row r
186
+ M.triu().highlight(color="blue") # upper triangle
187
+ ```
188
+
189
+ Move it: {row 1}(r = gm.scalar(1)) · {row 2}(r = gm.scalar(2))
190
+ ````
191
+
192
+ - **Value bind**: `t.<family>.<slot>.bind(node, show=, fmt=)`. Families/slots are
193
+ `int`/`sum`/`prod` → `lower`/`upper`/`body`, `frac` → `num`/`denom`,
194
+ `sqrt` → `body` (extend with `gm.register_tex_schema(family, slots)`). `fmt`
195
+ is `".2f"` / `"d"` (omit → trim to ≤4 dp); `show="symbol"` links without
196
+ substituting the glyph. Repeated command? `t.ints[1].upper` picks an
197
+ occurrence (discouraged — an edit silently retargets it).
198
+ - **Highlights**: build a selector over a cell's grid position and paint it.
199
+ Axes are `M.rows()` / `M.cols()` (or module-level `rows` / `cols` / `dim(i)`),
200
+ with arithmetic (`cols - rows`, `+`/`-`). Compare with `==` / `>=` / `<=` /
201
+ `>` / `<` (or `.eq/.ge/.le/.gt/.lt`) against a node or number; select
202
+ axis-aligned boxes with numpy-style slicing (`M[3:, 4:]`, node bounds stay
203
+ reactive); or use the named regions `M.diag()` / `M.triu()` / `M.tril()`.
204
+ Combine with `&` / `|` (`.and_` / `.or_`) and `.scale(node)` to fade a whole
205
+ selection — `scale` by a node + a `\scalar u 1` CommandLink gates a highlight
206
+ behind a click. Palette names (`"pink"`, `"BLUE"`) resolve to hex; raw
207
+ `"#f472b6"` / CSS names pass through. Full reference:
208
+ [docs/tex-highlight-ergonomics.md](docs/tex-highlight-ergonomics.md).
209
+ - **Reveal** (fade a part in): same selector machine, painting **opacity**
210
+ instead of color, so changing a gate node — reassigning it (`b = gm.bool_(True)`,
211
+ `k = gm.scalar(1)`) or animating it (`gm.animate(k, 3)`) in a CommandLink —
212
+ makes a piece of the formula appear. Three targets — an over/underbrace + label
213
+ (`t.underbrace.reveal(b)`, body stays visible; `.label` / `.body` for just
214
+ that part), a derivation line-by-line (`d.rows().reveal(rows < k)` — `k` lines
215
+ shown), or a matrix's rows/columns (`M.reveal(M.cols() < k)`). A bare gate
216
+ node/bool is the all-or-nothing case (`gm.bool_(False)` makes a false gate); a
217
+ scalar gives an animatable sweep. Full reference:
218
+ [docs/tex-reveal.md](docs/tex-reveal.md).
219
+
220
+ Two rules that will bite otherwise:
221
+
222
+ - **Bind replaces content, it never creates structure** — write placeholder
223
+ symbols in every slot you bind (`\int_{a}^{b}`, not a bare `\int`). An empty
224
+ slot fails validation.
225
+ - **The bound node must already exist** when you call `.bind()` / `.eq()` — define
226
+ it (`gm.scalar(…, out="b")`) above the binding so it is part of the runtime env.
227
+
228
+ **Repeated commands (`ints[i]`).** When one formula uses the same command twice
229
+ — e.g. `$$ \int_a^b f + \int_c^d g $$` — `t.int` is ambiguous. `t.ints` is the
230
+ list of all of them: `t.ints[0]` is the first `\int`, `t.ints[1]` the second.
231
+ The index is positional, so editing the formula (reordering or inserting an
232
+ `\int`) silently retargets it — prefer splitting into two formulas with distinct
233
+ ids, each with a single `\int`.
234
+
235
+ **Gating a highlight behind a click (`.scale`).** A bound highlight is on as
236
+ soon as it's bound. To reveal it only after a reader clicks, scale its weight by
237
+ a node that starts at `0`:
238
+
239
+ ```python
240
+ u = gm.scalar(0, out="u") # 0 → highlight invisible
241
+ M.highlight((M.rows() == r).scale(u)) # weight = row-weight × u
242
+ ```
243
+ ```markdown
244
+ {reveal}(u = gm.scalar(1)) # click sets u=1 → highlight appears
245
+ ```
246
+
247
+ `u=0` hides it, `u=1` shows it fully, `u=0.5` at half — the click is just a
248
+ normal CommandLink writing `u`.
249
+
250
+ `gm.harvest_tex_bindings(store)` returns the raw `{texId: {values, highlights}}`
251
+ manifest if you need it directly.
252
+
142
253
  ### Publishing from a content repo (GitHub Action)
143
254
 
144
255
  A content repo publishes compiled articles with one workflow file; a compile
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pygeomatic"
3
- version = "0.2"
3
+ version = "0.2.2"
4
4
  description = "Python mirror of the geomatic DSL function library; records calls for deterministic DSL emission"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -111,8 +111,11 @@ from .tex import ( # noqa: F401
111
111
  Selector,
112
112
  Tex,
113
113
  TexError,
114
+ cols,
115
+ dim,
114
116
  harvest_tex_bindings,
115
117
  register_tex_schema,
118
+ rows,
116
119
  tex,
117
120
  )
118
121
 
@@ -217,6 +217,12 @@ def _resolve_arg(fdef: FunctionDef, p: P, arg: Any, store: Store) -> tuple[ArgTo
217
217
  if p.type == "Bool":
218
218
  node = _implicit_bool(arg, store)
219
219
  return node.ref, node
220
+ if p.type == "Any":
221
+ # A bare Python bool in an untyped slot is just its 0/1 value (bool
222
+ # is an int subclass), like passing 1/0 — e.g. `gm.bool_(True)`
223
+ # records `\bool 1`. Scalar/Text params stay strict (they reject a
224
+ # bool below), so this only relaxes genuinely untyped parameters.
225
+ return int(arg), int(arg)
220
226
  raise TypeError(
221
227
  f"\\{fdef.keyword}: parameter '{p.name}' expects {p.type}, got bool"
222
228
  )
@@ -247,7 +247,8 @@ class Store:
247
247
  # texatlas bindings (gm.tex): declarative formula↔node links recorded on
248
248
  # a channel SEPARATE from the command tape — they never emit DSL and are
249
249
  # harvested straight from the session (tex.harvest_tex_bindings). Keyed
250
- # by `$$[#id]` formula id {"values": [...], "highlights": [...]}.
250
+ # by formula id (the `%id:` line inside a `$$…$$` block) →
251
+ # {"values": [...], "highlights": [...]}.
251
252
  self.tex_bindings: dict[str, dict] = {}
252
253
  self._token = None
253
254
  # Every canvas starts with the engine's default nodes (`p0`, `T`/`F`,