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.
- {pygeomatic-0.2 → pygeomatic-0.2.2}/PKG-INFO +112 -1
- {pygeomatic-0.2 → pygeomatic-0.2.2}/README.md +111 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/pyproject.toml +1 -1
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/__init__.py +3 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/registry.py +6 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/store.py +2 -1
- pygeomatic-0.2.2/src/pygeomatic/tex.py +724 -0
- pygeomatic-0.2/src/pygeomatic/tex.py +0 -397
- {pygeomatic-0.2 → pygeomatic-0.2.2}/.gitignore +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/LICENSE +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/article.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/coercions.json +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/coercions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/emit.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/extensions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/__init__.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/helpers.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/__init__.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/annotation_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/array.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/autograd_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/basic_figures.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/boolean_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/complex_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/curve_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/intersections.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/ode_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/planar_geometry.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/polygons.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/property_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/rotation_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/scalar_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/special_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/tangent_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/tensor_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/implementations/translation_functions.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/__init__.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/abs.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/add.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/create_overload.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/div.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/exp.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/log.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/mul.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/neg.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/pow.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/sqrt.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/functions/overloads/sub.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/generate.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/inference.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/macros.json +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/macros.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/nodes.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/palette.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/parse.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/prompting.py +0 -0
- {pygeomatic-0.2 → pygeomatic-0.2.2}/src/pygeomatic/runner.py +0 -0
- {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
|
|
@@ -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
|
|
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`,
|