qmlmathplot 0.5.0__tar.gz → 0.6.0__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 (94) hide show
  1. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/PKG-INFO +58 -115
  2. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/README.md +52 -113
  3. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/pyproject.toml +5 -2
  4. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/pyproject.toml.orig +8 -2
  5. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/__init__.py +48 -27
  6. qmlmathplot-0.6.0/src/qmlmathplot/calc/__init__.py +11 -0
  7. qmlmathplot-0.6.0/src/qmlmathplot/calc/__main__.py +61 -0
  8. qmlmathplot-0.6.0/src/qmlmathplot/calc/build.py +103 -0
  9. qmlmathplot-0.6.0/src/qmlmathplot/client.py +243 -0
  10. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/curve.py +267 -172
  11. qmlmathplot-0.6.0/src/qmlmathplot/curveplotter.py +638 -0
  12. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/ggb_compute.py +1 -9
  13. qmlmathplot-0.6.0/src/qmlmathplot/ggb_eligibility.py +69 -0
  14. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/ggb_path.py +61 -29
  15. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/interval.py +10 -65
  16. qmlmathplot-0.6.0/src/qmlmathplot/interval_reference.py +526 -0
  17. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/model.py +1 -158
  18. qmlmathplot-0.6.0/src/qmlmathplot/pathcurve.py +214 -0
  19. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/plot.py +36 -3
  20. qmlmathplot-0.6.0/src/qmlmathplot/program.py +32 -0
  21. qmlmathplot-0.6.0/src/qmlmathplot/protocol.py +216 -0
  22. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/CpuCurves.qml +2 -1
  23. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotCurves.qml +4 -4
  24. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotView.qml +4 -3
  25. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/rhi.py +145 -31
  26. qmlmathplot-0.6.0/src/qmlmathplot/shaders/interval_path.comp +105 -0
  27. qmlmathplot-0.6.0/src/qmlmathplot/shaders/path.frag +10 -0
  28. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/path.vert +3 -0
  29. qmlmathplot-0.6.0/src/qmlmathplot/shaders/path_state.glsl +222 -0
  30. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/view.py +2 -2
  31. qmlmathplot-0.5.0/src/qmlmathplot/canvas.py +0 -107
  32. qmlmathplot-0.5.0/src/qmlmathplot/figure.py +0 -516
  33. qmlmathplot-0.5.0/src/qmlmathplot/pathcurve.py +0 -167
  34. qmlmathplot-0.5.0/src/qmlmathplot/pyplot.py +0 -370
  35. qmlmathplot-0.5.0/src/qmlmathplot/shaders/interval_path.comp +0 -332
  36. qmlmathplot-0.5.0/src/qmlmathplot/shaders/oned_sampled.frag +0 -490
  37. qmlmathplot-0.5.0/src/qmlmathplot/shaders/path.frag +0 -4
  38. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/LICENSE +0 -0
  39. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/LICENSES/EUPL-1.2.txt +0 -0
  40. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/__main__.py +0 -0
  41. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/animation.py +0 -0
  42. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/app.py +0 -0
  43. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/camera.py +0 -0
  44. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/export.py +0 -0
  45. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/ggb_reference.py +0 -0
  46. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/input.py +0 -0
  47. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/mpl.py +0 -0
  48. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/overlay.py +0 -0
  49. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotFurniture.qml +0 -0
  50. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotInput.qml +0 -0
  51. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotLegend.qml +0 -0
  52. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qml/PlotOverlay.qml +0 -0
  53. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/qsb.py +0 -0
  54. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaderbuild.py +0 -0
  55. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/__init__.py +0 -0
  56. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/arg_reduction.glsl +0 -0
  57. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/real_pow.glsl +0 -0
  58. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/seg_dist.glsl +0 -0
  59. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/span_cov.glsl +0 -0
  60. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/common/uniforms.glsl +0 -0
  61. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/fullscreen.vert +0 -0
  62. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/implicit.frag +0 -0
  63. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/shaders/interval_lib.glsl +0 -0
  64. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/LICENSE.matplotlib +0 -0
  65. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/README.md +0 -0
  66. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/Solarize_Light2.mplstyle +0 -0
  67. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/bmh.mplstyle +0 -0
  68. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/classic.mplstyle +0 -0
  69. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/dark_background.mplstyle +0 -0
  70. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/default.mplstyle +0 -0
  71. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/fast.mplstyle +0 -0
  72. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/fivethirtyeight.mplstyle +0 -0
  73. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/ggplot.mplstyle +0 -0
  74. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/grayscale.mplstyle +0 -0
  75. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/petroff10.mplstyle +0 -0
  76. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-bright.mplstyle +0 -0
  77. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-colorblind.mplstyle +0 -0
  78. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-dark-palette.mplstyle +0 -0
  79. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-dark.mplstyle +0 -0
  80. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-darkgrid.mplstyle +0 -0
  81. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-deep.mplstyle +0 -0
  82. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-muted.mplstyle +0 -0
  83. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-notebook.mplstyle +0 -0
  84. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-paper.mplstyle +0 -0
  85. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-pastel.mplstyle +0 -0
  86. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-poster.mplstyle +0 -0
  87. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-talk.mplstyle +0 -0
  88. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-ticks.mplstyle +0 -0
  89. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-white.mplstyle +0 -0
  90. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8-whitegrid.mplstyle +0 -0
  91. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/seaborn-v0_8.mplstyle +0 -0
  92. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes/tableau-colorblind10.mplstyle +0 -0
  93. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/themes.py +0 -0
  94. {qmlmathplot-0.5.0 → qmlmathplot-0.6.0}/src/qmlmathplot/widget.py +0 -0
@@ -1,17 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qmlmathplot
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Embeddable function plotter for Qt Quick / QtWidgets (implicit per-pixel drawing, MVVM)
5
5
  Author: I-AM-A-NOOB
6
6
  Author-email: I-AM-A-NOOB <suzc08@outlook.com>
7
7
  License-Expression: GPL-3.0-only
8
8
  License-File: LICENSE
9
9
  License-File: LICENSES/EUPL-1.2.txt
10
- Requires-Dist: sympy>=1.14
11
10
  Requires-Dist: pyside6>=6.12
11
+ Requires-Dist: numpy>=1.26
12
+ Requires-Dist: sympy>=1.14 ; extra == 'calc'
12
13
  Requires-Dist: matplotlib>=3.8 ; extra == 'matplotlib'
14
+ Requires-Dist: matplotlib>=3.8 ; extra == 'mpl'
13
15
  Requires-Python: >=3.11
16
+ Provides-Extra: calc
14
17
  Provides-Extra: matplotlib
18
+ Provides-Extra: mpl
15
19
  Description-Content-Type: text/markdown
16
20
 
17
21
  # QMLMathPlot
@@ -28,6 +32,24 @@ The state is a **camera on an infinite canvas**: a `Plot` holds a `Camera` (wher
28
32
  and a list of `Curve`s (what is drawn), and the QML component `PlotView` binds to it. Several
29
33
  panels are several `Plot`s in a Qt layout — there is no figure, no axes box and no page.
30
34
 
35
+ ## Breaking changes in 0.6
36
+
37
+ - **The model no longer parses expressions.** A calculator turns text into a `Program`
38
+ (`qmlmathplot.protocol`) and the model consumes it; the UI process does not import sympy.
39
+ The in-process default (`InlineCalculator`) needs the `calc` extra
40
+ (`pip install qmlmathplot[calc]`); a host that isolates the calculator injects
41
+ `ProcessCalculator` — see `examples/process_calculator.py`.
42
+ - `Curve` and `add_curve` are **asynchronous**: shaders arrive with the program. Wait for
43
+ `shadersChanged`, and read `curve.error` for a failing edit (the previous drawing stays).
44
+ `Curve.expr` is a deprecated, lazily imported sympy view.
45
+ - Routing is `Program.kind`: `intervals` (compute shader), `adaptive` (CPU `CurvePlotter`),
46
+ `implicit` (level set). `add_curve("a*x")` reports the parameter error on the curve
47
+ instead of raising.
48
+ - **Removed:** `Figure`, `Axes`, `FigureCanvasQt`, `qmlmathplot.pyplot`. `MathPlotWidget`
49
+ stays.
50
+ - **QML:** `import QmlMathPlot 2.0`; `cpuRenderer: true` became `renderMode: "cpu"`
51
+ (`"auto"` / `"gpu"` / `"cpu"`).
52
+
31
53
  ## Quick start
32
54
 
33
55
  One line in a QtWidgets layout (least effort):
@@ -49,16 +71,15 @@ An un-themed plot looks like matplotlib (white background, tab10 colours, grey g
49
71
  axes through the origin with the tick labels riding them) — the default style sheet is applied
50
72
  at construction, so `plot.theme = "default"` is the reset.
51
73
 
52
- `add_curve()` (and `Curve.expression`) accept the expression **either as a string or as a sympy
53
- expression**: `plot.add_curve(sp.sin(1 / sp.Symbol("x")))` is the same curve as
54
- `plot.add_curve("sin(1/x)")`. The stored and reported form is always the canonical string — QML,
55
- a project file and the text field want one — while the parsed expression is cached and exposed as
56
- `Curve.expr`, which is what re-baking and the matplotlib hand-off use. The object form exists so a
57
- curve can be composed without string interpolation:
74
+ `add_curve()` (and `Curve.expression`) take the expression as text; a sympy object is also
75
+ accepted and converted to its canonical string once. The model never parses it: a calculator
76
+ turns the text into a `Program` (`kind`, shader sources, `math`/`numpy` programs), so the UI
77
+ process can stay sympy-free. `Curve.expr` still exists as a deprecated, lazily imported sympy
78
+ view for the compatibility layer, and the matplotlib hand-off uses it.
58
79
 
59
80
  ```python
60
81
  curve = plot.add_curve("sin(x)")
61
- plot.add_curve(sp.diff(sp.sympify(curve.expression, locals={"x": sp.Symbol("x")})), label="f'")
82
+ plot.add_curve(sp.diff(sp.sympify(curve.expression)), label="f'") # needs the [calc] extra
62
83
  ```
63
84
 
64
85
  `MathPlotWidget` keeps one curve's worth of convenience (`expression`, `error`,
@@ -77,14 +98,14 @@ Standalone window / command line:
77
98
  ```
78
99
  uv run qmlmathplot "sin(1/x)" # equivalent to python examples/minimal.py …
79
100
  uv run qmlmathplot --backend d3d11 "sin(1/x)" # force a specific backend
80
- python examples/explorer_qtwidgets.py # function input box + sibling widgets
101
+ python examples/widget_minimal.py # QtWidgets host
102
+ python examples/process_calculator.py # calculator in its own process
81
103
  ```
82
104
 
83
105
  The examples live in `examples/`: `minimal.py` is the smallest window;
84
- `explorer_qtwidgets.py` deliberately puts the plot widget into a "grabby"
85
- environment (input box, `QScrollArea`, `QSplitter`, overlay) to verify the widgets
86
- do not fight each other; `explorer_qtquick.py` (plus `explorer_qtquick.qml`) is
87
- the pure Qt Quick demo.
106
+ `explorer_qtquick.py` (plus `explorer_qtquick.qml`) is the pure Qt Quick demo;
107
+ `process_calculator.py` runs the calculator in its own process and can prove the isolation
108
+ with `--check-isolation`; `widget_minimal.py` embeds `MathPlotWidget` in a QtWidgets layout.
88
109
 
89
110
  ## Embedding into an app
90
111
 
@@ -116,7 +137,7 @@ app.exec()
116
137
  ```
117
138
 
118
139
  ```qml
119
- import QmlMathPlot 1.0
140
+ import QmlMathPlot 2.0
120
141
 
121
142
  PlotView { // inject a Plot, or let the component create its own
122
143
  anchors.fill: parent
@@ -171,74 +192,8 @@ Matplotlib *vocabulary* is kept where it is vocabulary (limits, grid, title, tic
171
192
  *mechanism* is refused — no `Figure`/`Axes`/`subplots` in the core (one view per widget, no
172
193
  bounded page), no redraw loop (the scene is live), no toolbar and no legend (host chrome), and
173
194
  an export is a canvas region at a pixel size rather than a re-rendered page (§9, implemented —
174
- see *Export* below). A **thin compatibility facade** (`Figure`, `Axes`, `FigureCanvasQt`,
175
- `qmlmathplot.pyplot`) provides the familiar names for porting scripts; it maps onto the same
176
- model and refuses the mechanisms that do not fit (see *Matplotlib-style facade* below).
177
-
178
- ## Matplotlib-style facade
179
-
180
- For porting scripts and for code that expects Matplotlib's vocabulary, the package ships a
181
- **thin** compatibility layer. It maps onto the same `Plot`/`Camera` model and refuses the
182
- mechanisms that do not fit an infinite, live canvas.
183
-
184
- ```python
185
- from qmlmathplot import Figure, FigureCanvasQt
186
-
187
- fig = Figure(figsize=(6.4, 4.8)) # stored for the window size only; no page is implied
188
- ax = fig.gca() # exactly one axes per figure
189
- ax.plot("sin(1/x)", label="sin(1/x)")
190
- ax.set_xlim(-1, 1)
191
- ax.set_ylim(-2, 2)
192
- ax.set_title("sin(1/x)")
193
- ax.set_xlabel("x")
194
- ax.set_ylabel("y")
195
- ax.grid(True)
196
- ax.set_aspect("equal")
197
-
198
- canvas = FigureCanvasQt(fig) # a QWidget, like MathPlotWidget
199
- layout.addWidget(canvas)
200
- canvas.draw() # no-op: the Qt scene is already live
201
- ```
202
-
203
- The same model through a pyplot-shaped module (global current figure, for scripts and
204
- notebooks; applications should keep the object API):
205
-
206
- ```python
207
- import qmlmathplot.pyplot as plt
208
-
209
- plt.figure()
210
- plt.plot("sin(1/x)")
211
- plt.xlim(-1, 1)
212
- plt.title("sin(1/x)")
213
- plt.xlabel("x")
214
- plt.grid(True)
215
- plt.savefig("out.png") # Qt raster export
216
- plt.show() # blocks in a script; use block=False in a host
217
- ```
218
-
219
- What maps: one figure/one axes (`subplots()` and `subplot(111)`), `plot(expression)`,
220
- `xlim`/`ylim`, `title`/`xlabel`/`ylabel`, `grid(axis=...)`, `aspect`, `savefig`/`to_image`,
221
- themes, `legend`, `annotate`/`text`, `axhline`/`axvline`, `set_xticks`/`set_yticks` and
222
- `tick_params`. `plt.gcf()`/`gca()`/`sca()`/`close()`/`show()` follow Matplotlib's
223
- current-figure shape.
224
-
225
- What is deliberately different:
226
-
227
- - **One view per figure.** `subplots(2, 1)`/`add_subplot(211)` raise `NotImplementedError`;
228
- several panels are several `Figure` objects in a Qt layout, not a `GridSpec`.
229
- - **Expressions, not x/y arrays.** `ax.plot("sin(x)")` is native; `ax.plot(x, y)` is refused
230
- because the Qt renderer evaluates an expression per pixel. For sampled data use
231
- `Plot.to_matplotlib()` (the hand-off), or add a data-series artist.
232
- - **`draw()` / `draw_idle()` are no-ops.** The scene repaints from model signals; there is no
233
- deferred renderer or blitting.
234
- - **`savefig` is a region at a pixel size**, not a page at a dpi. `width`/`height` are device
235
- pixels and `dpi` is a multiplier; `backend="matplotlib"` is the vector route.
236
- - **Log axes and full annotation styling are not implemented.** `set_xscale()`/`set_yscale()`
237
- raise with the reason; annotations support text + a straight arrow + pixel offset, not
238
- Matplotlib's bbox/connectionstyle engine.
239
- - **Themes are per figure**, not `rcParams`; `plt` has no global style state beyond the current
240
- figure.
241
- - **`mpl_connect`/`blit` are absent.** Qt signals are the event system.
195
+ see *Export* below). 0.6 removed the Matplotlib-shaped facade entirely: the model is `Plot` /
196
+ `Camera` / `Curve`, and the optional matplotlib hand-off is `Plot.to_matplotlib()`.
242
197
 
243
198
  ## Implicit curves
244
199
 
@@ -541,20 +496,17 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
541
496
 
542
497
  | Layer | File | Responsibility |
543
498
  |---|---|---|
544
- | Model | `src/qmlmathplot/model.py` | expression → GLSL (no Qt dependency, unit-testable) |
499
+ | Calculator | `src/qmlmathplot/calc/` | text → `Program` (GLSL, `math` and `numpy` sources); the only code that imports sympy. Qt-free, runs in-process or as `python -m qmlmathplot.calc` |
545
500
  | Shaders | `src/qmlmathplot/shaders/` + `shaderbuild.py` | GLSL resources, `@NAME@` substitution, shared `@INCLUDE` blocks, validation (no template strings in Python) |
546
501
  | Camera | `src/qmlmathplot/camera.py` | `Camera`: centre / zoom / aspect, the derived ranges, pan/zoom/reset, the nice-number tick algorithm |
547
502
  | Input | `src/qmlmathplot/input.py` | `ViewInput`: keyboard switch, pan step, rebindable shortcuts, key matching |
548
503
  | Animation | `src/qmlmathplot/animation.py` | `CameraAnimator`: the one eased interpolator behind wheel/keyboard moves |
549
- | Curves | `src/qmlmathplot/curve.py` | `Curve` (expression → baked `.qsb`, style, error; later edits bake on the thread pool) and `CurveListModel` |
504
+ | Curves | `src/qmlmathplot/curve.py` | `Curve` (requests a `Program`, bakes its GLSL on the thread pool, keeps the last drawing on failure) and `CurveListModel` |
550
505
  | Render | `src/qmlmathplot/rhi.py` + `ggb_compute.py` | `CurveRhiItem`: one `QQuickRhiItem` draws every curve in one pass; interval-supported 1-D curves run the GGB path state machine in a compute shader |
551
506
  | Path spec | `src/qmlmathplot/ggb_path.py` | The GGB path state machine (topology in, segments out), shared by the CPU sampler and the tests; no interval arithmetic |
552
- | Reference | `src/qmlmathplot/ggb_reference.py` | CPU sampler (`mpmath.iv`) feeding the shared state machine; also used by the CPU fallback |
553
- | CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.cpuRenderer = true`: one QPainterPath per curve when RHI is unavailable |
507
+ | Reference | `src/qmlmathplot/ggb_reference.py` | test oracle: CPU sampler (`mpmath.iv`) feeding the shared state machine |
508
+ | CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.renderMode = "cpu"`: one QPainterPath per curve when RHI is unavailable |
554
509
  | Plot | `src/qmlmathplot/plot.py` | `Plot`: the camera, the curves and the styling the view binds to |
555
- | Facade | `src/qmlmathplot/figure.py` | `Figure` / `Axes`: Matplotlib-shaped names over one `Plot` (no Qt beyond the model) |
556
- | Facade host | `src/qmlmathplot/canvas.py` | `FigureCanvasQt`: a `MathPlotWidget` attached to a `Figure` (QtWidgets, lazy) |
557
- | Facade scripting | `src/qmlmathplot/pyplot.py` | `qmlmathplot.pyplot`: current figure, `plot`/`xlim`/`show`/`savefig` for scripts |
558
510
  | View | `src/qmlmathplot/qml/PlotView.qml` (+ `PlotCurves.qml`, `PlotFurniture.qml`, `PlotInput.qml`, `view.py`) | composition: model injection, camera mapping and keyboard focus; curves, furniture and pointer input live in the three child components (one `QQuickRhiItem` pass for all curves) |
559
511
  | Themes | `src/qmlmathplot/themes.py` + `themes/` | the vendored Matplotlib style sheets, resolved onto the `Plot`'s properties |
560
512
  | Export | `src/qmlmathplot/export.py` | `Plot.savefig()` / `to_image()`: the canvas region rendered offscreen at a pixel size (QtWidgets is imported lazily) |
@@ -562,7 +514,7 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
562
514
  | Bake | `src/qmlmathplot/qsb.py` | GLSL → `.qsb` (PySide6 ships `qsb.exe`), cached by source hash |
563
515
  | Widget | `src/qmlmathplot/widget.py` | `MathPlotWidget`: QQuickWidget bridge, drops straight into QtWidgets layouts |
564
516
  | Entry | `src/qmlmathplot/app.py` | command line / standalone window (the `qmlmathplot` script, `examples/minimal.py`) |
565
- | Examples | `examples/` | `minimal.py` (minimal), `explorer_qtwidgets.py` (input box + widget coexistence), `explorer_qtquick.py` (Qt Quick) |
517
+ | Examples | `examples/` | `minimal.py` (standalone window), `explorer_qtquick.py` (Qt Quick), `process_calculator.py` (the process split + `--check-isolation`), `widget_minimal.py` (QtWidgets) |
566
518
 
567
519
  ## Algorithm (GeoGebra's interval path on the GPU)
568
520
 
@@ -596,20 +548,15 @@ same state machine (topology in, screen-space segments out).
596
548
  the compute path from the expression). Expressions
597
549
  GGB's `IntervalFunctionSupport` rejects — `floor`, `sign`, `Min/Max`, `Piecewise`, and
598
550
  dependent quotients such as `x/sqrt(x**2)` (GGB's algebra simplifies it to `sgn`) — use
599
- the sampled fragment: arc-length marching, the join invariant, the envelope band and the
600
- boundary ray. Implicit curves always use their own level-set shader.
601
-
602
- Deliberate deviations: a wrapped pole (`exp(tan(x))`) becomes a connected `[0, +inf]`
603
- column. The rule is a Nyquist test against half the pole spacing (the distance between two
604
- poles, derived from the expression's period): below it the column is resolved, so the branch
605
- that approaches +∞ (the side with the larger neighbour high edge) is drawn and the other
606
- branch is left to the next column; at or above it the column width cannot guarantee one pole
607
- per column, and a non-negative, pole-only-domain expression fills the attained half-plane
608
- uniformly instead of leaving a moiré of spike tops. GGB's conservative connector would
609
- violate the "no line across a pole" invariant. The CPU sampler (`ggb_reference.py`,
610
- `mpmath.iv`) cannot express a gap, so its pole columns are path breaks; it feeds the same
611
- state machine. The deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus
612
- the whole-column rule, as before.
551
+ GGB's classic `CurvePlotter`, ported to `curveplotter.py`: adaptive sampling, continuity
552
+ checks and a world-space path, stroked by the RHI path pipeline (the CPU owns the path,
553
+ the GPU owns the stroke). Implicit curves always use their own level-set shader.
554
+
555
+ Deliberate deviation: `exp(tan(x))` follows GGB's own wrapped-pole behaviour; no special
556
+ fill compensates for it. The CPU sampler (`ggb_reference.py`, `mpmath.iv`) cannot express a
557
+ gap, so its pole columns are path breaks; it feeds the same `ggb_path` state machine. The
558
+ deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus the whole-column
559
+ rule.
613
560
 
614
561
 
615
562
  ## Tests
@@ -622,11 +569,11 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
622
569
  ```
623
570
 
624
571
  - `test_model.py`: GLSL generation (small integer powers as repeated
625
- multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0), shader
626
- injection (including the camera uniform and the mapping), and the camera math
572
+ multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0), domain analysis,
573
+ and the camera math
627
574
  (the home view at the first size, resize keeping the scale, an anchored zoom that
628
575
  is exactly reversible, the aspect expanding but never cropping, ticks).
629
- - `test_curve.py`: a failed expression keeps the last working shaders, the style
576
+ - `test_curve.py`: a failed expression keeps the last working expression, the style
630
577
  setters are idempotent, and the list model reports the right roles — plus the
631
578
  `Expr`/string round-trip accounting (a string is parsed once, an expression never).
632
579
  - `test_implicit.py`: the routing by free symbols (and the `ValueError` for a third symbol), the
@@ -641,8 +588,8 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
641
588
  matplotlib's default style, the limits are forwarded to the camera, and a theme applies
642
589
  (and re-applies as a reset) while an unknown name is rejected.
643
590
  - `test_themes.py`: the vendored style sheets resolve to this library's properties.
644
- - `test_overlay.py`: annotations (`annotate`/`text`), horizontal/vertical guides, legend and
645
- custom ticks, the `Axes`/`pyplot` forwarding, and the matplotlib hand-off mapping.
591
+ - `test_overlay.py`: annotations, horizontal/vertical guides, legend and custom ticks, and
592
+ the matplotlib hand-off mapping.
646
593
  - `test_qsb_cache.py`: the cache location/info/clear API and the age + LRU prune; unrelated
647
594
  files in the temp directory are not touched.
648
595
  - `test_shader_bake.py`: the stage of a baked `.qsb` must match its purpose, and
@@ -670,10 +617,6 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
670
617
  extra.
671
618
  - `test_async_bake.py`: a later expression assignment bakes on the thread pool, a stale bake
672
619
  is discarded, and a failed bake keeps the drawn curve.
673
- - `test_compat.py`: the Matplotlib-shaped facade — expressions (and the refusal of x/y arrays),
674
- limits/title/labels/grid/aspect, the single-axes rule, `pyplot` current-figure semantics,
675
- `FigureCanvasQt` rendering and export, and that importing `qmlmathplot` does not load
676
- QtWidgets.
677
620
  - `test_export.py`: `to_image()`/`savefig()` — the image is exactly the requested device size
678
621
  (`dpi` multiplies it), the no-argument form reproduces the live centre and scale, the three
679
622
  `adjustable` modes reconcile range and size as documented (a world 45-degree line stays at 45
@@ -750,7 +693,7 @@ Startup to the first frame is about **230 ms**, attributed as follows
750
693
  | Qt graphics device init (RHI/D3D11) | **~160 ms** | exactly what the first window's `show→expose` costs; a **bare window (no ShaderEffect) is the same**, so it is not the shader's fault |
751
694
  | Window creation + QML load + first frame | ~80 ms | from the second window on (device already created) this is the order of magnitude |
752
695
  | `qsb` bake | 0 / ~126 ms | cached by source hash: 0.15 ms on a hit; later `expression` assignments bake on Qt's thread pool, so only the first curve construction blocks (tests force the synchronous path) |
753
- | Expression analysis (sympy) | 0.6~3 ms | `unbounded_edges` only analyses **polynomial** boundaries (`tan(x)`'s `cos(x)=0` takes 94 ms via solve+limit, while tan diverges fast and sampling is naturally deep enough, so that ray is not needed) |
696
+ | Expression analysis (sympy) | 0.6~3 ms | domain/pole analysis for the interval and implicit renderers; unsupported 1-D expressions go to the CPU CurvePlotter |
754
697
 
755
698
  Per-frame grab cycles (900×600, `grabToImage` — frame + readback, so an upper bound on
756
699
  the GPU cost), steady state on the same window: `sin(1/x)` default **13.6 ms**, `tan(x)`
@@ -12,6 +12,24 @@ The state is a **camera on an infinite canvas**: a `Plot` holds a `Camera` (wher
12
12
  and a list of `Curve`s (what is drawn), and the QML component `PlotView` binds to it. Several
13
13
  panels are several `Plot`s in a Qt layout — there is no figure, no axes box and no page.
14
14
 
15
+ ## Breaking changes in 0.6
16
+
17
+ - **The model no longer parses expressions.** A calculator turns text into a `Program`
18
+ (`qmlmathplot.protocol`) and the model consumes it; the UI process does not import sympy.
19
+ The in-process default (`InlineCalculator`) needs the `calc` extra
20
+ (`pip install qmlmathplot[calc]`); a host that isolates the calculator injects
21
+ `ProcessCalculator` — see `examples/process_calculator.py`.
22
+ - `Curve` and `add_curve` are **asynchronous**: shaders arrive with the program. Wait for
23
+ `shadersChanged`, and read `curve.error` for a failing edit (the previous drawing stays).
24
+ `Curve.expr` is a deprecated, lazily imported sympy view.
25
+ - Routing is `Program.kind`: `intervals` (compute shader), `adaptive` (CPU `CurvePlotter`),
26
+ `implicit` (level set). `add_curve("a*x")` reports the parameter error on the curve
27
+ instead of raising.
28
+ - **Removed:** `Figure`, `Axes`, `FigureCanvasQt`, `qmlmathplot.pyplot`. `MathPlotWidget`
29
+ stays.
30
+ - **QML:** `import QmlMathPlot 2.0`; `cpuRenderer: true` became `renderMode: "cpu"`
31
+ (`"auto"` / `"gpu"` / `"cpu"`).
32
+
15
33
  ## Quick start
16
34
 
17
35
  One line in a QtWidgets layout (least effort):
@@ -33,16 +51,15 @@ An un-themed plot looks like matplotlib (white background, tab10 colours, grey g
33
51
  axes through the origin with the tick labels riding them) — the default style sheet is applied
34
52
  at construction, so `plot.theme = "default"` is the reset.
35
53
 
36
- `add_curve()` (and `Curve.expression`) accept the expression **either as a string or as a sympy
37
- expression**: `plot.add_curve(sp.sin(1 / sp.Symbol("x")))` is the same curve as
38
- `plot.add_curve("sin(1/x)")`. The stored and reported form is always the canonical string — QML,
39
- a project file and the text field want one — while the parsed expression is cached and exposed as
40
- `Curve.expr`, which is what re-baking and the matplotlib hand-off use. The object form exists so a
41
- curve can be composed without string interpolation:
54
+ `add_curve()` (and `Curve.expression`) take the expression as text; a sympy object is also
55
+ accepted and converted to its canonical string once. The model never parses it: a calculator
56
+ turns the text into a `Program` (`kind`, shader sources, `math`/`numpy` programs), so the UI
57
+ process can stay sympy-free. `Curve.expr` still exists as a deprecated, lazily imported sympy
58
+ view for the compatibility layer, and the matplotlib hand-off uses it.
42
59
 
43
60
  ```python
44
61
  curve = plot.add_curve("sin(x)")
45
- plot.add_curve(sp.diff(sp.sympify(curve.expression, locals={"x": sp.Symbol("x")})), label="f'")
62
+ plot.add_curve(sp.diff(sp.sympify(curve.expression)), label="f'") # needs the [calc] extra
46
63
  ```
47
64
 
48
65
  `MathPlotWidget` keeps one curve's worth of convenience (`expression`, `error`,
@@ -61,14 +78,14 @@ Standalone window / command line:
61
78
  ```
62
79
  uv run qmlmathplot "sin(1/x)" # equivalent to python examples/minimal.py …
63
80
  uv run qmlmathplot --backend d3d11 "sin(1/x)" # force a specific backend
64
- python examples/explorer_qtwidgets.py # function input box + sibling widgets
81
+ python examples/widget_minimal.py # QtWidgets host
82
+ python examples/process_calculator.py # calculator in its own process
65
83
  ```
66
84
 
67
85
  The examples live in `examples/`: `minimal.py` is the smallest window;
68
- `explorer_qtwidgets.py` deliberately puts the plot widget into a "grabby"
69
- environment (input box, `QScrollArea`, `QSplitter`, overlay) to verify the widgets
70
- do not fight each other; `explorer_qtquick.py` (plus `explorer_qtquick.qml`) is
71
- the pure Qt Quick demo.
86
+ `explorer_qtquick.py` (plus `explorer_qtquick.qml`) is the pure Qt Quick demo;
87
+ `process_calculator.py` runs the calculator in its own process and can prove the isolation
88
+ with `--check-isolation`; `widget_minimal.py` embeds `MathPlotWidget` in a QtWidgets layout.
72
89
 
73
90
  ## Embedding into an app
74
91
 
@@ -100,7 +117,7 @@ app.exec()
100
117
  ```
101
118
 
102
119
  ```qml
103
- import QmlMathPlot 1.0
120
+ import QmlMathPlot 2.0
104
121
 
105
122
  PlotView { // inject a Plot, or let the component create its own
106
123
  anchors.fill: parent
@@ -155,74 +172,8 @@ Matplotlib *vocabulary* is kept where it is vocabulary (limits, grid, title, tic
155
172
  *mechanism* is refused — no `Figure`/`Axes`/`subplots` in the core (one view per widget, no
156
173
  bounded page), no redraw loop (the scene is live), no toolbar and no legend (host chrome), and
157
174
  an export is a canvas region at a pixel size rather than a re-rendered page (§9, implemented —
158
- see *Export* below). A **thin compatibility facade** (`Figure`, `Axes`, `FigureCanvasQt`,
159
- `qmlmathplot.pyplot`) provides the familiar names for porting scripts; it maps onto the same
160
- model and refuses the mechanisms that do not fit (see *Matplotlib-style facade* below).
161
-
162
- ## Matplotlib-style facade
163
-
164
- For porting scripts and for code that expects Matplotlib's vocabulary, the package ships a
165
- **thin** compatibility layer. It maps onto the same `Plot`/`Camera` model and refuses the
166
- mechanisms that do not fit an infinite, live canvas.
167
-
168
- ```python
169
- from qmlmathplot import Figure, FigureCanvasQt
170
-
171
- fig = Figure(figsize=(6.4, 4.8)) # stored for the window size only; no page is implied
172
- ax = fig.gca() # exactly one axes per figure
173
- ax.plot("sin(1/x)", label="sin(1/x)")
174
- ax.set_xlim(-1, 1)
175
- ax.set_ylim(-2, 2)
176
- ax.set_title("sin(1/x)")
177
- ax.set_xlabel("x")
178
- ax.set_ylabel("y")
179
- ax.grid(True)
180
- ax.set_aspect("equal")
181
-
182
- canvas = FigureCanvasQt(fig) # a QWidget, like MathPlotWidget
183
- layout.addWidget(canvas)
184
- canvas.draw() # no-op: the Qt scene is already live
185
- ```
186
-
187
- The same model through a pyplot-shaped module (global current figure, for scripts and
188
- notebooks; applications should keep the object API):
189
-
190
- ```python
191
- import qmlmathplot.pyplot as plt
192
-
193
- plt.figure()
194
- plt.plot("sin(1/x)")
195
- plt.xlim(-1, 1)
196
- plt.title("sin(1/x)")
197
- plt.xlabel("x")
198
- plt.grid(True)
199
- plt.savefig("out.png") # Qt raster export
200
- plt.show() # blocks in a script; use block=False in a host
201
- ```
202
-
203
- What maps: one figure/one axes (`subplots()` and `subplot(111)`), `plot(expression)`,
204
- `xlim`/`ylim`, `title`/`xlabel`/`ylabel`, `grid(axis=...)`, `aspect`, `savefig`/`to_image`,
205
- themes, `legend`, `annotate`/`text`, `axhline`/`axvline`, `set_xticks`/`set_yticks` and
206
- `tick_params`. `plt.gcf()`/`gca()`/`sca()`/`close()`/`show()` follow Matplotlib's
207
- current-figure shape.
208
-
209
- What is deliberately different:
210
-
211
- - **One view per figure.** `subplots(2, 1)`/`add_subplot(211)` raise `NotImplementedError`;
212
- several panels are several `Figure` objects in a Qt layout, not a `GridSpec`.
213
- - **Expressions, not x/y arrays.** `ax.plot("sin(x)")` is native; `ax.plot(x, y)` is refused
214
- because the Qt renderer evaluates an expression per pixel. For sampled data use
215
- `Plot.to_matplotlib()` (the hand-off), or add a data-series artist.
216
- - **`draw()` / `draw_idle()` are no-ops.** The scene repaints from model signals; there is no
217
- deferred renderer or blitting.
218
- - **`savefig` is a region at a pixel size**, not a page at a dpi. `width`/`height` are device
219
- pixels and `dpi` is a multiplier; `backend="matplotlib"` is the vector route.
220
- - **Log axes and full annotation styling are not implemented.** `set_xscale()`/`set_yscale()`
221
- raise with the reason; annotations support text + a straight arrow + pixel offset, not
222
- Matplotlib's bbox/connectionstyle engine.
223
- - **Themes are per figure**, not `rcParams`; `plt` has no global style state beyond the current
224
- figure.
225
- - **`mpl_connect`/`blit` are absent.** Qt signals are the event system.
175
+ see *Export* below). 0.6 removed the Matplotlib-shaped facade entirely: the model is `Plot` /
176
+ `Camera` / `Curve`, and the optional matplotlib hand-off is `Plot.to_matplotlib()`.
226
177
 
227
178
  ## Implicit curves
228
179
 
@@ -525,20 +476,17 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
525
476
 
526
477
  | Layer | File | Responsibility |
527
478
  |---|---|---|
528
- | Model | `src/qmlmathplot/model.py` | expression → GLSL (no Qt dependency, unit-testable) |
479
+ | Calculator | `src/qmlmathplot/calc/` | text → `Program` (GLSL, `math` and `numpy` sources); the only code that imports sympy. Qt-free, runs in-process or as `python -m qmlmathplot.calc` |
529
480
  | Shaders | `src/qmlmathplot/shaders/` + `shaderbuild.py` | GLSL resources, `@NAME@` substitution, shared `@INCLUDE` blocks, validation (no template strings in Python) |
530
481
  | Camera | `src/qmlmathplot/camera.py` | `Camera`: centre / zoom / aspect, the derived ranges, pan/zoom/reset, the nice-number tick algorithm |
531
482
  | Input | `src/qmlmathplot/input.py` | `ViewInput`: keyboard switch, pan step, rebindable shortcuts, key matching |
532
483
  | Animation | `src/qmlmathplot/animation.py` | `CameraAnimator`: the one eased interpolator behind wheel/keyboard moves |
533
- | Curves | `src/qmlmathplot/curve.py` | `Curve` (expression → baked `.qsb`, style, error; later edits bake on the thread pool) and `CurveListModel` |
484
+ | Curves | `src/qmlmathplot/curve.py` | `Curve` (requests a `Program`, bakes its GLSL on the thread pool, keeps the last drawing on failure) and `CurveListModel` |
534
485
  | Render | `src/qmlmathplot/rhi.py` + `ggb_compute.py` | `CurveRhiItem`: one `QQuickRhiItem` draws every curve in one pass; interval-supported 1-D curves run the GGB path state machine in a compute shader |
535
486
  | Path spec | `src/qmlmathplot/ggb_path.py` | The GGB path state machine (topology in, segments out), shared by the CPU sampler and the tests; no interval arithmetic |
536
- | Reference | `src/qmlmathplot/ggb_reference.py` | CPU sampler (`mpmath.iv`) feeding the shared state machine; also used by the CPU fallback |
537
- | CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.cpuRenderer = true`: one QPainterPath per curve when RHI is unavailable |
487
+ | Reference | `src/qmlmathplot/ggb_reference.py` | test oracle: CPU sampler (`mpmath.iv`) feeding the shared state machine |
488
+ | CPU fallback | `src/qmlmathplot/qml/CpuCurves.qml` + `pathcurve.py` | `view.renderMode = "cpu"`: one QPainterPath per curve when RHI is unavailable |
538
489
  | Plot | `src/qmlmathplot/plot.py` | `Plot`: the camera, the curves and the styling the view binds to |
539
- | Facade | `src/qmlmathplot/figure.py` | `Figure` / `Axes`: Matplotlib-shaped names over one `Plot` (no Qt beyond the model) |
540
- | Facade host | `src/qmlmathplot/canvas.py` | `FigureCanvasQt`: a `MathPlotWidget` attached to a `Figure` (QtWidgets, lazy) |
541
- | Facade scripting | `src/qmlmathplot/pyplot.py` | `qmlmathplot.pyplot`: current figure, `plot`/`xlim`/`show`/`savefig` for scripts |
542
490
  | View | `src/qmlmathplot/qml/PlotView.qml` (+ `PlotCurves.qml`, `PlotFurniture.qml`, `PlotInput.qml`, `view.py`) | composition: model injection, camera mapping and keyboard focus; curves, furniture and pointer input live in the three child components (one `QQuickRhiItem` pass for all curves) |
543
491
  | Themes | `src/qmlmathplot/themes.py` + `themes/` | the vendored Matplotlib style sheets, resolved onto the `Plot`'s properties |
544
492
  | Export | `src/qmlmathplot/export.py` | `Plot.savefig()` / `to_image()`: the canvas region rendered offscreen at a pixel size (QtWidgets is imported lazily) |
@@ -546,7 +494,7 @@ three import QtWidgets lazily, so a pure-QML host never loads it.
546
494
  | Bake | `src/qmlmathplot/qsb.py` | GLSL → `.qsb` (PySide6 ships `qsb.exe`), cached by source hash |
547
495
  | Widget | `src/qmlmathplot/widget.py` | `MathPlotWidget`: QQuickWidget bridge, drops straight into QtWidgets layouts |
548
496
  | Entry | `src/qmlmathplot/app.py` | command line / standalone window (the `qmlmathplot` script, `examples/minimal.py`) |
549
- | Examples | `examples/` | `minimal.py` (minimal), `explorer_qtwidgets.py` (input box + widget coexistence), `explorer_qtquick.py` (Qt Quick) |
497
+ | Examples | `examples/` | `minimal.py` (standalone window), `explorer_qtquick.py` (Qt Quick), `process_calculator.py` (the process split + `--check-isolation`), `widget_minimal.py` (QtWidgets) |
550
498
 
551
499
  ## Algorithm (GeoGebra's interval path on the GPU)
552
500
 
@@ -580,20 +528,15 @@ same state machine (topology in, screen-space segments out).
580
528
  the compute path from the expression). Expressions
581
529
  GGB's `IntervalFunctionSupport` rejects — `floor`, `sign`, `Min/Max`, `Piecewise`, and
582
530
  dependent quotients such as `x/sqrt(x**2)` (GGB's algebra simplifies it to `sgn`) — use
583
- the sampled fragment: arc-length marching, the join invariant, the envelope band and the
584
- boundary ray. Implicit curves always use their own level-set shader.
585
-
586
- Deliberate deviations: a wrapped pole (`exp(tan(x))`) becomes a connected `[0, +inf]`
587
- column. The rule is a Nyquist test against half the pole spacing (the distance between two
588
- poles, derived from the expression's period): below it the column is resolved, so the branch
589
- that approaches +∞ (the side with the larger neighbour high edge) is drawn and the other
590
- branch is left to the next column; at or above it the column width cannot guarantee one pole
591
- per column, and a non-negative, pole-only-domain expression fills the attained half-plane
592
- uniformly instead of leaving a moiré of spike tops. GGB's conservative connector would
593
- violate the "no line across a pole" invariant. The CPU sampler (`ggb_reference.py`,
594
- `mpmath.iv`) cannot express a gap, so its pole columns are path breaks; it feeds the same
595
- state machine. The deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus
596
- the whole-column rule, as before.
531
+ GGB's classic `CurvePlotter`, ported to `curveplotter.py`: adaptive sampling, continuity
532
+ checks and a world-space path, stroked by the RHI path pipeline (the CPU owns the path,
533
+ the GPU owns the stroke). Implicit curves always use their own level-set shader.
534
+
535
+ Deliberate deviation: `exp(tan(x))` follows GGB's own wrapped-pole behaviour; no special
536
+ fill compensates for it. The CPU sampler (`ggb_reference.py`, `mpmath.iv`) cannot express a
537
+ gap, so its pole columns are path breaks; it feeds the same `ggb_path` state machine. The
538
+ deep-`log` view is handled by `drawNormalInfinity`'s on-screen test plus the whole-column
539
+ rule.
597
540
 
598
541
 
599
542
  ## Tests
@@ -606,11 +549,11 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
606
549
  ```
607
550
 
608
551
  - `test_model.py`: GLSL generation (small integer powers as repeated
609
- multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0), shader
610
- injection (including the camera uniform and the mapping), and the camera math
552
+ multiplication, avoiding `pow(x, y)`'s undefined behaviour for x<0), domain analysis,
553
+ and the camera math
611
554
  (the home view at the first size, resize keeping the scale, an anchored zoom that
612
555
  is exactly reversible, the aspect expanding but never cropping, ticks).
613
- - `test_curve.py`: a failed expression keeps the last working shaders, the style
556
+ - `test_curve.py`: a failed expression keeps the last working expression, the style
614
557
  setters are idempotent, and the list model reports the right roles — plus the
615
558
  `Expr`/string round-trip accounting (a string is parsed once, an expression never).
616
559
  - `test_implicit.py`: the routing by free symbols (and the `ValueError` for a third symbol), the
@@ -625,8 +568,8 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
625
568
  matplotlib's default style, the limits are forwarded to the camera, and a theme applies
626
569
  (and re-applies as a reset) while an unknown name is rejected.
627
570
  - `test_themes.py`: the vendored style sheets resolve to this library's properties.
628
- - `test_overlay.py`: annotations (`annotate`/`text`), horizontal/vertical guides, legend and
629
- custom ticks, the `Axes`/`pyplot` forwarding, and the matplotlib hand-off mapping.
571
+ - `test_overlay.py`: annotations, horizontal/vertical guides, legend and custom ticks, and
572
+ the matplotlib hand-off mapping.
630
573
  - `test_qsb_cache.py`: the cache location/info/clear API and the age + LRU prune; unrelated
631
574
  files in the temp directory are not touched.
632
575
  - `test_shader_bake.py`: the stage of a baked `.qsb` must match its purpose, and
@@ -654,10 +597,6 @@ uv run ruff check src tests examples # bug-level lint (undefined names, unused
654
597
  extra.
655
598
  - `test_async_bake.py`: a later expression assignment bakes on the thread pool, a stale bake
656
599
  is discarded, and a failed bake keeps the drawn curve.
657
- - `test_compat.py`: the Matplotlib-shaped facade — expressions (and the refusal of x/y arrays),
658
- limits/title/labels/grid/aspect, the single-axes rule, `pyplot` current-figure semantics,
659
- `FigureCanvasQt` rendering and export, and that importing `qmlmathplot` does not load
660
- QtWidgets.
661
600
  - `test_export.py`: `to_image()`/`savefig()` — the image is exactly the requested device size
662
601
  (`dpi` multiplies it), the no-argument form reproduces the live centre and scale, the three
663
602
  `adjustable` modes reconcile range and size as documented (a world 45-degree line stays at 45
@@ -734,7 +673,7 @@ Startup to the first frame is about **230 ms**, attributed as follows
734
673
  | Qt graphics device init (RHI/D3D11) | **~160 ms** | exactly what the first window's `show→expose` costs; a **bare window (no ShaderEffect) is the same**, so it is not the shader's fault |
735
674
  | Window creation + QML load + first frame | ~80 ms | from the second window on (device already created) this is the order of magnitude |
736
675
  | `qsb` bake | 0 / ~126 ms | cached by source hash: 0.15 ms on a hit; later `expression` assignments bake on Qt's thread pool, so only the first curve construction blocks (tests force the synchronous path) |
737
- | Expression analysis (sympy) | 0.6~3 ms | `unbounded_edges` only analyses **polynomial** boundaries (`tan(x)`'s `cos(x)=0` takes 94 ms via solve+limit, while tan diverges fast and sampling is naturally deep enough, so that ray is not needed) |
676
+ | Expression analysis (sympy) | 0.6~3 ms | domain/pole analysis for the interval and implicit renderers; unsupported 1-D expressions go to the CPU CurvePlotter |
738
677
 
739
678
  Per-frame grab cycles (900×600, `grabToImage` — frame + readback, so an upper bound on
740
679
  the GPU cost), steady state on the same window: `sin(1/x)` default **13.6 ms**, `tan(x)`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qmlmathplot"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Embeddable function plotter for Qt Quick / QtWidgets (implicit per-pixel drawing, MVVM)"
5
5
  readme = "README.md"
6
6
  license = "GPL-3.0-only"
@@ -10,8 +10,8 @@ license-files = [
10
10
  ]
11
11
  requires-python = ">=3.11"
12
12
  dependencies = [
13
- "sympy>=1.14",
14
13
  "PySide6>=6.12",
14
+ "numpy>=1.26",
15
15
  ]
16
16
 
17
17
  [[project.authors]]
@@ -19,7 +19,9 @@ name = "I-AM-A-NOOB"
19
19
  email = "suzc08@outlook.com"
20
20
 
21
21
  [project.optional-dependencies]
22
+ calc = ["sympy>=1.14"]
22
23
  matplotlib = ["matplotlib>=3.8"]
24
+ mpl = ["matplotlib>=3.8"]
23
25
 
24
26
  [project.scripts]
25
27
  qmlmathplot = "qmlmathplot.app:main"
@@ -32,6 +34,7 @@ build-backend = "uv_build"
32
34
  dev = [
33
35
  "pytest>=9.1.1",
34
36
  "matplotlib>=3.8",
37
+ "sympy>=1.14",
35
38
  "ruff>=0.9",
36
39
  ]
37
40
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qmlmathplot"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Embeddable function plotter for Qt Quick / QtWidgets (implicit per-pixel drawing, MVVM)"
5
5
  readme = "README.md"
6
6
  license = "GPL-3.0-only"
@@ -10,14 +10,19 @@ authors = [
10
10
  ]
11
11
  requires-python = ">=3.11"
12
12
  dependencies = [
13
- "sympy>=1.14",
14
13
  "PySide6>=6.12",
14
+ "numpy>=1.26",
15
15
  ]
16
16
 
17
17
  [project.optional-dependencies]
18
+ # The calculator is the only part that needs sympy; install it for the in-process
19
+ # calculator and the worker (`python -m qmlmathplot.calc`). A host that isolates the
20
+ # calculator in another process does not need it in the UI environment.
21
+ calc = ["sympy>=1.14"]
18
22
  # The matplotlib hand-off (Plot.to_matplotlib / savefig(backend="matplotlib")) is optional:
19
23
  # nothing in the package imports matplotlib until that path is used.
20
24
  matplotlib = ["matplotlib>=3.8"]
25
+ mpl = ["matplotlib>=3.8"]
21
26
 
22
27
  [project.scripts]
23
28
  qmlmathplot = "qmlmathplot.app:main"
@@ -30,6 +35,7 @@ build-backend = "uv_build"
30
35
  dev = [
31
36
  "pytest>=9.1.1",
32
37
  "matplotlib>=3.8", # the hand-off tests need it (the feature itself stays optional)
38
+ "sympy>=1.14", # the calculator implementation and its oracles
33
39
  "ruff>=0.9",
34
40
  ]
35
41