qmlmathplot 0.4.0__tar.gz → 0.4.1__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.
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/PKG-INFO +19 -8
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/README.md +18 -7
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/pyproject.toml +1 -1
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/pyproject.toml.orig +1 -1
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/curve.py +83 -40
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/input.py +22 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/plot.py +6 -4
- qmlmathplot-0.4.1/src/qmlmathplot/qml/PlotView.qml +275 -0
- qmlmathplot-0.4.0/src/qmlmathplot/qml/PlotView.qml +0 -134
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/__init__.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/__main__.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/animation.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/app.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/camera.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/canvas.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/export.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/figure.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/interval.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/model.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/mpl.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/pyplot.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/qml/PlotCurves.qml +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/qml/PlotFurniture.qml +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/qml/PlotInput.qml +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/qsb.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/LICENSE.matplotlib +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/README.md +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/Solarize_Light2.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/bmh.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/classic.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/dark_background.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/default.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/fast.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/fivethirtyeight.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/ggplot.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/grayscale.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/petroff10.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-bright.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-colorblind.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-dark-palette.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-dark.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-darkgrid.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-deep.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-muted.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-notebook.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-paper.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-pastel.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-poster.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-talk.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-ticks.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-white.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-whitegrid.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/tableau-colorblind10.mplstyle +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/view.py +0 -0
- {qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/widget.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: qmlmathplot
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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>
|
|
@@ -145,7 +145,7 @@ The model:
|
|
|
145
145
|
|---|---|
|
|
146
146
|
| `Plot` | `camera`, `curves`, `xlim`/`ylim`/`aspect` (forwarded), `add_curve()` / `add_implicit()` / `remove_curve()`, `available_themes`, and the style properties: `background`, `grid`, `grid_color`, `grid_width`, `grid_alpha`, `grid_style`, `axis_color`, `axis_width`, `axes_position`, `color_cycle`, `line_width`, `text_color`, `tick_color`, `tick_length`, `font_size`, `tick_font_size`, `title`, `xlabel`, `ylabel`, `title_color`, `title_font_size`, `title_bold`, `ticks_visible`, `theme` |
|
|
147
147
|
| `Camera` | `centre`, `zoom` (world units per logical pixel), `aspect`, `xlim`/`ylim` (derived), `ticks_x`/`ticks_y`, `zoomStep` / `panEnabled` / `zoomEnabled`, `animationDuration`, `input` (a `ViewInput`), slots `set_viewport(w, h)` / `zoom_by(delta, u, v, w, h)` / `zoom_at(factor, u, v, w, h)` / `pan_pixels(dx, dy)` / `animate_zoom(...)` / `animate_pan(dx, dy, ms)` / `stop_animation()` / `reset()` |
|
|
148
|
-
| `ViewInput` | `keyboardEnabled`, `keyPanStep`, `panLeftShortcut` / `panRightShortcut` / `panUpShortcut` / `panDownShortcut` / `zoomInShortcut` / `zoomOutShortcut`, `matches(key, modifiers, shortcut)` |
|
|
148
|
+
| `ViewInput` | `keyboardEnabled`, `keyPanStep`, `keyPanRepeatInterval`, `panLeftShortcut` / `panRightShortcut` / `panUpShortcut` / `panDownShortcut` / `zoomInShortcut` / `zoomOutShortcut`, `matches(key, modifiers, shortcut)` |
|
|
149
149
|
| `Curve` | `expression` (string or sympy expression), `expr` (the parsed expression, read-only), `implicit`, `color`, `lineWidth`, `visible`, `label`, `error` (read-only), the baked `vertexShader` / `fragmentShader` |
|
|
150
150
|
| `CurveListModel` | the curves as a model (`curve`, `expression`, `color`, `lineWidth`, `visible`, `vertexShader`, `fragmentShader` roles) so QML puts one `ShaderEffect` behind each |
|
|
151
151
|
|
|
@@ -239,9 +239,16 @@ and `add_implicit` asks for the level-set artist explicitly — also for a funct
|
|
|
239
239
|
which is a pair of vertical lines. Only a *third* free symbol is refused: substitute parameters
|
|
240
240
|
first.
|
|
241
241
|
|
|
242
|
+
A sympy `Eq(lhs, rhs)` is always a level set: it is normalized to `lhs - rhs = 0`, and an
|
|
243
|
+
equation with only `x` still means the vertical lines where it holds (`Eq(x**2, 1)`), not
|
|
244
|
+
`y = x**2 - 1`. The canonical `expression` keeps the `Eq(...)` text and `expr` is the normalized
|
|
245
|
+
difference.
|
|
246
|
+
|
|
242
247
|
```python
|
|
248
|
+
x, y = sp.symbols("x y")
|
|
243
249
|
plot.add_curve("x**2 + y**2 - 1") # a circle: {x, y} routes to the level-set artist
|
|
244
|
-
plot.
|
|
250
|
+
plot.add_curve(sp.Eq(x**2 + y**2, 1)) # the same circle in equation form
|
|
251
|
+
plot.add_implicit(sp.Eq(x, sp.sin(y))) # asked for by name, equation form
|
|
245
252
|
plot.curves.at(0).expression = "x**2 + y**2 - 4" # re-routes the curve that is already there
|
|
246
253
|
plot.add_curve("a*sin(x)") # ValueError: substitute `a` first
|
|
247
254
|
```
|
|
@@ -306,9 +313,12 @@ plot.plot.camera.zoom_by(120, 0.5, 0.5, w, h) # one wheel notch, anchored at
|
|
|
306
313
|
## Keyboard, touch and animation
|
|
307
314
|
|
|
308
315
|
Clicking the plot gives it focus; while it has focus the arrow keys pan (10% of the viewport
|
|
309
|
-
per press by default), `+`/`=` zooms in and `-`/`_` zooms out.
|
|
310
|
-
|
|
311
|
-
|
|
316
|
+
per press by default), `+`/`=` zooms in and `-`/`_` zooms out. Held keys advance continuously:
|
|
317
|
+
one `keyPanStep` per `keyPanRepeatInterval` (100 ms by default), integrated every frame, so
|
|
318
|
+
there is no OS key-repeat delay or granular stepping. Several arrows combine, so `Left+Up`
|
|
319
|
+
moves diagonally at the normal speed. Every command is a property on the camera's `input`
|
|
320
|
+
object, so a host can rebind it — the value is a `QKeySequence` string, and `|` separates
|
|
321
|
+
alternatives (empty disables the command). `input.keyPanStep`, `input.keyPanRepeatInterval`,
|
|
312
322
|
`input.keyboardEnabled`, `panEnabled` and `zoomEnabled` shape the behaviour the same way.
|
|
313
323
|
|
|
314
324
|
```python
|
|
@@ -318,8 +328,9 @@ input.panLeftShortcut = "A" # rebind one arrow
|
|
|
318
328
|
input.panRightShortcut = "D"
|
|
319
329
|
input.zoomInShortcut = "Ctrl+="
|
|
320
330
|
input.zoomOutShortcut = "Ctrl+-"
|
|
321
|
-
input.keyPanStep = 0.2 # 20% of the viewport per
|
|
322
|
-
|
|
331
|
+
input.keyPanStep = 0.2 # 20% of the viewport per step
|
|
332
|
+
input.keyPanRepeatInterval = 80 # ms per step; smaller = faster pan/zoom
|
|
333
|
+
camera.animationDuration = 220 # ms; tap easing
|
|
323
334
|
```
|
|
324
335
|
|
|
325
336
|
Wheel and keyboard moves are **eased** (scale interpolates geometrically, so a zoom feels like
|
|
@@ -132,7 +132,7 @@ The model:
|
|
|
132
132
|
|---|---|
|
|
133
133
|
| `Plot` | `camera`, `curves`, `xlim`/`ylim`/`aspect` (forwarded), `add_curve()` / `add_implicit()` / `remove_curve()`, `available_themes`, and the style properties: `background`, `grid`, `grid_color`, `grid_width`, `grid_alpha`, `grid_style`, `axis_color`, `axis_width`, `axes_position`, `color_cycle`, `line_width`, `text_color`, `tick_color`, `tick_length`, `font_size`, `tick_font_size`, `title`, `xlabel`, `ylabel`, `title_color`, `title_font_size`, `title_bold`, `ticks_visible`, `theme` |
|
|
134
134
|
| `Camera` | `centre`, `zoom` (world units per logical pixel), `aspect`, `xlim`/`ylim` (derived), `ticks_x`/`ticks_y`, `zoomStep` / `panEnabled` / `zoomEnabled`, `animationDuration`, `input` (a `ViewInput`), slots `set_viewport(w, h)` / `zoom_by(delta, u, v, w, h)` / `zoom_at(factor, u, v, w, h)` / `pan_pixels(dx, dy)` / `animate_zoom(...)` / `animate_pan(dx, dy, ms)` / `stop_animation()` / `reset()` |
|
|
135
|
-
| `ViewInput` | `keyboardEnabled`, `keyPanStep`, `panLeftShortcut` / `panRightShortcut` / `panUpShortcut` / `panDownShortcut` / `zoomInShortcut` / `zoomOutShortcut`, `matches(key, modifiers, shortcut)` |
|
|
135
|
+
| `ViewInput` | `keyboardEnabled`, `keyPanStep`, `keyPanRepeatInterval`, `panLeftShortcut` / `panRightShortcut` / `panUpShortcut` / `panDownShortcut` / `zoomInShortcut` / `zoomOutShortcut`, `matches(key, modifiers, shortcut)` |
|
|
136
136
|
| `Curve` | `expression` (string or sympy expression), `expr` (the parsed expression, read-only), `implicit`, `color`, `lineWidth`, `visible`, `label`, `error` (read-only), the baked `vertexShader` / `fragmentShader` |
|
|
137
137
|
| `CurveListModel` | the curves as a model (`curve`, `expression`, `color`, `lineWidth`, `visible`, `vertexShader`, `fragmentShader` roles) so QML puts one `ShaderEffect` behind each |
|
|
138
138
|
|
|
@@ -226,9 +226,16 @@ and `add_implicit` asks for the level-set artist explicitly — also for a funct
|
|
|
226
226
|
which is a pair of vertical lines. Only a *third* free symbol is refused: substitute parameters
|
|
227
227
|
first.
|
|
228
228
|
|
|
229
|
+
A sympy `Eq(lhs, rhs)` is always a level set: it is normalized to `lhs - rhs = 0`, and an
|
|
230
|
+
equation with only `x` still means the vertical lines where it holds (`Eq(x**2, 1)`), not
|
|
231
|
+
`y = x**2 - 1`. The canonical `expression` keeps the `Eq(...)` text and `expr` is the normalized
|
|
232
|
+
difference.
|
|
233
|
+
|
|
229
234
|
```python
|
|
235
|
+
x, y = sp.symbols("x y")
|
|
230
236
|
plot.add_curve("x**2 + y**2 - 1") # a circle: {x, y} routes to the level-set artist
|
|
231
|
-
plot.
|
|
237
|
+
plot.add_curve(sp.Eq(x**2 + y**2, 1)) # the same circle in equation form
|
|
238
|
+
plot.add_implicit(sp.Eq(x, sp.sin(y))) # asked for by name, equation form
|
|
232
239
|
plot.curves.at(0).expression = "x**2 + y**2 - 4" # re-routes the curve that is already there
|
|
233
240
|
plot.add_curve("a*sin(x)") # ValueError: substitute `a` first
|
|
234
241
|
```
|
|
@@ -293,9 +300,12 @@ plot.plot.camera.zoom_by(120, 0.5, 0.5, w, h) # one wheel notch, anchored at
|
|
|
293
300
|
## Keyboard, touch and animation
|
|
294
301
|
|
|
295
302
|
Clicking the plot gives it focus; while it has focus the arrow keys pan (10% of the viewport
|
|
296
|
-
per press by default), `+`/`=` zooms in and `-`/`_` zooms out.
|
|
297
|
-
|
|
298
|
-
|
|
303
|
+
per press by default), `+`/`=` zooms in and `-`/`_` zooms out. Held keys advance continuously:
|
|
304
|
+
one `keyPanStep` per `keyPanRepeatInterval` (100 ms by default), integrated every frame, so
|
|
305
|
+
there is no OS key-repeat delay or granular stepping. Several arrows combine, so `Left+Up`
|
|
306
|
+
moves diagonally at the normal speed. Every command is a property on the camera's `input`
|
|
307
|
+
object, so a host can rebind it — the value is a `QKeySequence` string, and `|` separates
|
|
308
|
+
alternatives (empty disables the command). `input.keyPanStep`, `input.keyPanRepeatInterval`,
|
|
299
309
|
`input.keyboardEnabled`, `panEnabled` and `zoomEnabled` shape the behaviour the same way.
|
|
300
310
|
|
|
301
311
|
```python
|
|
@@ -305,8 +315,9 @@ input.panLeftShortcut = "A" # rebind one arrow
|
|
|
305
315
|
input.panRightShortcut = "D"
|
|
306
316
|
input.zoomInShortcut = "Ctrl+="
|
|
307
317
|
input.zoomOutShortcut = "Ctrl+-"
|
|
308
|
-
input.keyPanStep = 0.2 # 20% of the viewport per
|
|
309
|
-
|
|
318
|
+
input.keyPanStep = 0.2 # 20% of the viewport per step
|
|
319
|
+
input.keyPanRepeatInterval = 80 # ms per step; smaller = faster pan/zoom
|
|
320
|
+
camera.animationDuration = 220 # ms; tap easing
|
|
310
321
|
```
|
|
311
322
|
|
|
312
323
|
Wheel and keyboard moves are **eased** (scale interpolates geometrically, so a zoom feels like
|
|
@@ -80,19 +80,36 @@ def _color(value: object) -> QColor:
|
|
|
80
80
|
return QColor(value) if not isinstance(value, QColor) else QColor(value)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def
|
|
83
|
+
def _parse(value: str | sp.Basic) -> tuple[str, sp.Expr, bool]:
|
|
84
|
+
"""Canonical text, normalized expression and whether the input was an equation.
|
|
85
|
+
|
|
86
|
+
``Eq(lhs, rhs)`` becomes the level set ``lhs - rhs = 0`` and is flagged as implicit even
|
|
87
|
+
when only ``x`` is free (``Eq(x**2, 1)`` is two vertical lines, not ``y = x**2 - 1``).
|
|
88
|
+
"""
|
|
89
|
+
if isinstance(value, sp.Basic):
|
|
90
|
+
text = str(value)
|
|
91
|
+
expr = value
|
|
92
|
+
else:
|
|
93
|
+
text = str(value)
|
|
94
|
+
expr = sp.sympify(value, locals={"x": sp.Symbol("x"), "y": sp.Symbol("y")})
|
|
95
|
+
if isinstance(expr, sp.Eq):
|
|
96
|
+
return text, expr.lhs - expr.rhs, True
|
|
97
|
+
return text, expr, False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parsed(value: str | sp.Basic) -> tuple[str, sp.Expr]:
|
|
84
101
|
"""The canonical string and the parsed expression of a curve input.
|
|
85
102
|
|
|
86
103
|
A string is parsed here, once; a sympy expression is used as it is (no ``Expr -> str ->
|
|
87
|
-
Expr`` round trip).
|
|
104
|
+
Expr`` round trip). ``Eq(lhs, rhs)`` is normalized to ``lhs - rhs``. The canonical string
|
|
105
|
+
is what identifies the expression.
|
|
88
106
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return str(value), sp.sympify(value, locals={"x": sp.Symbol("x"), "y": sp.Symbol("y")})
|
|
107
|
+
text, expr, _ = _parse(value)
|
|
108
|
+
return text, expr
|
|
92
109
|
|
|
93
110
|
|
|
94
111
|
def curve_for(
|
|
95
|
-
expression: str | sp.
|
|
112
|
+
expression: str | sp.Basic,
|
|
96
113
|
*,
|
|
97
114
|
implicit: bool | None = None,
|
|
98
115
|
color: object = None,
|
|
@@ -106,17 +123,25 @@ def curve_for(
|
|
|
106
123
|
is an implicit curve (the level set ``F(x, y) = 0``), and anything else raises — a parameter
|
|
107
124
|
must be substituted before it is plotted, and silently evaluating it as a pixel coordinate is
|
|
108
125
|
exactly the trap this avoids. ``implicit=True`` picks the level-set artist regardless of the
|
|
109
|
-
symbols (``add_implicit``).
|
|
126
|
+
symbols (``add_implicit``). A sympy ``Eq`` is always a level set (``lhs - rhs = 0``), even
|
|
127
|
+
when only ``x`` is free; it keeps the ordinary ``Curve`` class so a later assignment can
|
|
128
|
+
re-route it.
|
|
110
129
|
"""
|
|
111
|
-
text, expr =
|
|
130
|
+
text, expr, is_equation = _parse(expression)
|
|
112
131
|
names = {symbol.name for symbol in expr.free_symbols}
|
|
113
132
|
if names - {"x", "y"}:
|
|
114
133
|
raise ValueError(
|
|
115
134
|
f"{sorted(names - {'x', 'y'})} must be substituted first: only x (or x and y) may "
|
|
116
135
|
f"be free, got {text!r}"
|
|
117
136
|
)
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
if implicit:
|
|
138
|
+
cls: type[Curve] = ImplicitCurve
|
|
139
|
+
elif is_equation:
|
|
140
|
+
cls = Curve
|
|
141
|
+
else:
|
|
142
|
+
cls = ImplicitCurve if "y" in names else Curve
|
|
143
|
+
source = expression if is_equation else expr
|
|
144
|
+
return cls(source, color=color, line_width=line_width, label=label, parent=parent)
|
|
120
145
|
|
|
121
146
|
|
|
122
147
|
class Curve(QObject):
|
|
@@ -125,7 +150,8 @@ class Curve(QObject):
|
|
|
125
150
|
The fragment shader follows the expression: ``y = f(x)`` while only ``x`` (or nothing) is
|
|
126
151
|
free, the level set ``F(x, y) = 0`` as soon as ``y`` appears — so ``add_curve`` and assigning
|
|
127
152
|
to ``expression`` both route themselves, and a text field can turn a function into a level
|
|
128
|
-
set. ``
|
|
153
|
+
set. A sympy ``Eq(lhs, rhs)`` is always a level set (``lhs - rhs = 0``), even when only ``x``
|
|
154
|
+
is free. ``ImplicitCurve`` forces the level-set template regardless of the symbols.
|
|
129
155
|
"""
|
|
130
156
|
|
|
131
157
|
#: Forced to the level-set template (see ``ImplicitCurve``).
|
|
@@ -145,7 +171,7 @@ class Curve(QObject):
|
|
|
145
171
|
|
|
146
172
|
def __init__(
|
|
147
173
|
self,
|
|
148
|
-
expression: str | sp.
|
|
174
|
+
expression: str | sp.Basic = "sin(x)",
|
|
149
175
|
color: str | QColor = DEFAULT_COLOR,
|
|
150
176
|
line_width: float = 1.5,
|
|
151
177
|
label: str = "",
|
|
@@ -164,6 +190,7 @@ class Curve(QObject):
|
|
|
164
190
|
self._fragment = QUrl()
|
|
165
191
|
self._generation = 0
|
|
166
192
|
self._pending_expr: sp.Expr | None = None
|
|
193
|
+
self._pending_implicit: bool | None = None
|
|
167
194
|
# Not parented: a job may outlive the curve; the signal object must stay valid for the
|
|
168
195
|
# queued connection (Qt disconnects the receiver when the curve is destroyed).
|
|
169
196
|
self._bake_signals = _BakeSignals()
|
|
@@ -174,51 +201,50 @@ class Curve(QObject):
|
|
|
174
201
|
def _get_expression(self) -> str:
|
|
175
202
|
return self._expression
|
|
176
203
|
|
|
177
|
-
def _set_expression(self, source: str | sp.
|
|
204
|
+
def _set_expression(self, source: str | sp.Basic, *, synchronous: bool = False) -> None:
|
|
178
205
|
"""Set the expression from a string (parsed here, once) or from a sympy expression.
|
|
179
206
|
|
|
180
207
|
The canonical **string** is what identifies the expression — ``sp.sin(x)`` and
|
|
181
208
|
``"sin(x)"`` are the same, and re-setting either is free — while the parsed
|
|
182
209
|
``sp.Expr`` is cached for the bake and for the matplotlib hand-off, so neither ever
|
|
183
|
-
stringifies and re-parses again.
|
|
210
|
+
stringifies and re-parses again. ``Eq(lhs, rhs)`` is normalized to ``lhs - rhs`` and
|
|
211
|
+
drawn as the level set, even when only ``x`` is free.
|
|
184
212
|
|
|
185
213
|
Parsing and source generation are synchronous (microseconds); the qsb subprocess runs
|
|
186
214
|
on the thread pool for later assignments. ``expr`` keeps the last successfully baked
|
|
187
215
|
expression until the new shaders arrive, so a failed or pending edit never draws an
|
|
188
216
|
incomplete curve.
|
|
189
217
|
"""
|
|
190
|
-
if isinstance(source, str)
|
|
191
|
-
canonical = source
|
|
192
|
-
else:
|
|
193
|
-
canonical = str(source)
|
|
218
|
+
canonical = source if isinstance(source, str) else str(source)
|
|
194
219
|
if canonical == self._expression:
|
|
195
220
|
return
|
|
196
221
|
self._generation += 1
|
|
197
222
|
generation = self._generation
|
|
198
223
|
self._pending_expr = None
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return
|
|
224
|
+
self._pending_implicit = None
|
|
225
|
+
try:
|
|
226
|
+
_, expr, is_equation = _parse(source)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
# Keep the last drawn expression: the shaders stay, `expression` is what was
|
|
229
|
+
# typed and `error` says why.
|
|
230
|
+
self._expression = str(canonical)
|
|
231
|
+
self._error = f"{type(exc).__name__}: {exc}"
|
|
232
|
+
self.expressionChanged.emit()
|
|
233
|
+
self.errorChanged.emit()
|
|
234
|
+
return
|
|
235
|
+
forced_implicit = type(self)._force_implicit or is_equation
|
|
212
236
|
self._expression = str(canonical)
|
|
213
237
|
self.expressionChanged.emit()
|
|
214
|
-
self._start_bake(
|
|
238
|
+
self._start_bake(
|
|
239
|
+
generation, expr, synchronous=synchronous, force_implicit=forced_implicit
|
|
240
|
+
)
|
|
215
241
|
|
|
216
242
|
# Annotate the value type (PySide6 idiom): Property is a descriptor, so without the
|
|
217
243
|
# annotation a type checker only sees the Property object and flags every read and write.
|
|
218
244
|
# Declared "QVariant" so an ``sp.Expr`` reaches the setter unconverted; the getter is always
|
|
219
245
|
# the canonical string (QML, a project file and the text field all want one).
|
|
220
|
-
expression: str | sp.
|
|
221
|
-
|
|
246
|
+
expression: str | sp.Basic = Property("QVariant", _get_expression, _set_expression,
|
|
247
|
+
notify=expressionChanged)
|
|
222
248
|
|
|
223
249
|
def _get_expr(self) -> sp.Expr:
|
|
224
250
|
return self._expr
|
|
@@ -302,7 +328,9 @@ class Curve(QObject):
|
|
|
302
328
|
"""
|
|
303
329
|
return self._force_implicit or "y" in {symbol.name for symbol in self._expr.free_symbols}
|
|
304
330
|
|
|
305
|
-
def _sources(
|
|
331
|
+
def _sources(
|
|
332
|
+
self, expr: sp.Expr, *, force_implicit: bool | None = None
|
|
333
|
+
) -> tuple[str, str]:
|
|
306
334
|
"""The shader sources for this expression; anything unrenderable raises ``ValueError``
|
|
307
335
|
(which ``_start_bake`` turns into ``error``, keeping the last working shaders)."""
|
|
308
336
|
names = {symbol.name for symbol in expr.free_symbols}
|
|
@@ -311,22 +339,33 @@ class Curve(QObject):
|
|
|
311
339
|
f"only x (or x and y) may be free, got {sorted(names)}: substitute parameters "
|
|
312
340
|
"first"
|
|
313
341
|
)
|
|
314
|
-
|
|
342
|
+
implicit = self._force_implicit if force_implicit is None else force_implicit
|
|
343
|
+
return implicit_shader_sources(expr) if (implicit or "y" in names) \
|
|
315
344
|
else shader_sources(expr)
|
|
316
345
|
|
|
317
|
-
def _start_bake(
|
|
346
|
+
def _start_bake(
|
|
347
|
+
self,
|
|
348
|
+
generation: int,
|
|
349
|
+
expr: sp.Expr,
|
|
350
|
+
*,
|
|
351
|
+
synchronous: bool,
|
|
352
|
+
force_implicit: bool | None = None,
|
|
353
|
+
) -> None:
|
|
318
354
|
"""Generate the GLSL now and bake it on the pool unless this curve is synchronous.
|
|
319
355
|
|
|
320
356
|
Source generation is cheap and stays on the caller's thread; only the qsb subprocess
|
|
321
357
|
(the ~126 ms cache miss) leaves it.
|
|
322
358
|
"""
|
|
323
359
|
try:
|
|
324
|
-
vertex_source, fragment_source = self._sources(
|
|
360
|
+
vertex_source, fragment_source = self._sources(
|
|
361
|
+
expr, force_implicit=force_implicit
|
|
362
|
+
)
|
|
325
363
|
except Exception as exc: # noqa: BLE001 - reported through error, previous shaders stay
|
|
326
364
|
self._error = f"{type(exc).__name__}: {exc}"
|
|
327
365
|
self.errorChanged.emit()
|
|
328
366
|
return
|
|
329
367
|
self._pending_expr = expr
|
|
368
|
+
self._pending_implicit = force_implicit
|
|
330
369
|
if synchronous or not self.bake_async or QCoreApplication.instance() is None:
|
|
331
370
|
self._run_bake(generation, vertex_source, fragment_source)
|
|
332
371
|
return
|
|
@@ -349,7 +388,9 @@ class Curve(QObject):
|
|
|
349
388
|
if generation != self._generation:
|
|
350
389
|
return
|
|
351
390
|
expr = self._pending_expr
|
|
391
|
+
implicit = self._pending_implicit
|
|
352
392
|
self._pending_expr = None
|
|
393
|
+
self._pending_implicit = None
|
|
353
394
|
if isinstance(result, Exception):
|
|
354
395
|
self._error = f"{type(result).__name__}: {result}"
|
|
355
396
|
self.errorChanged.emit()
|
|
@@ -358,6 +399,8 @@ class Curve(QObject):
|
|
|
358
399
|
return
|
|
359
400
|
vertex, fragment = result # type: ignore[misc]
|
|
360
401
|
self._expr = expr
|
|
402
|
+
if implicit is not None:
|
|
403
|
+
self._force_implicit = bool(implicit)
|
|
361
404
|
self._error = ""
|
|
362
405
|
self._vertex = QUrl.fromLocalFile(str(vertex))
|
|
363
406
|
self._fragment = QUrl.fromLocalFile(str(fragment))
|
|
@@ -475,7 +518,7 @@ class CurveListModel(QAbstractListModel):
|
|
|
475
518
|
@Slot("QVariant", result=QObject)
|
|
476
519
|
def add_curve(
|
|
477
520
|
self,
|
|
478
|
-
expression: str | sp.
|
|
521
|
+
expression: str | sp.Basic,
|
|
479
522
|
color: object = None,
|
|
480
523
|
line_width: object = None,
|
|
481
524
|
label: str = "",
|
|
@@ -491,7 +534,7 @@ class CurveListModel(QAbstractListModel):
|
|
|
491
534
|
@Slot("QVariant", result=QObject)
|
|
492
535
|
def add_implicit(
|
|
493
536
|
self,
|
|
494
|
-
expression: str | sp.
|
|
537
|
+
expression: str | sp.Basic,
|
|
495
538
|
color: object = None,
|
|
496
539
|
line_width: object = None,
|
|
497
540
|
label: str = "",
|
|
@@ -39,12 +39,14 @@ class ViewInput(QObject):
|
|
|
39
39
|
|
|
40
40
|
keyboardEnabledChanged = Signal()
|
|
41
41
|
keyPanStepChanged = Signal()
|
|
42
|
+
keyPanRepeatIntervalChanged = Signal()
|
|
42
43
|
shortcutsChanged = Signal()
|
|
43
44
|
|
|
44
45
|
def __init__(self, parent: QObject | None = None) -> None:
|
|
45
46
|
super().__init__(parent)
|
|
46
47
|
self._keyboard_enabled = True
|
|
47
48
|
self._key_pan_step = 0.1
|
|
49
|
+
self._key_pan_repeat_interval = 100
|
|
48
50
|
self._pan_left = "Left"
|
|
49
51
|
self._pan_right = "Right"
|
|
50
52
|
self._pan_up = "Up"
|
|
@@ -85,6 +87,26 @@ class ViewInput(QObject):
|
|
|
85
87
|
float, _get_key_pan_step, _set_key_pan_step, notify=keyPanStepChanged
|
|
86
88
|
)
|
|
87
89
|
|
|
90
|
+
def _get_key_pan_repeat_interval(self) -> int:
|
|
91
|
+
return self._key_pan_repeat_interval
|
|
92
|
+
|
|
93
|
+
def _set_key_pan_repeat_interval(self, value: int) -> None:
|
|
94
|
+
value = int(value)
|
|
95
|
+
if value <= 0:
|
|
96
|
+
raise ValueError(f"keyPanRepeatInterval must be positive, got {value!r}")
|
|
97
|
+
if value == self._key_pan_repeat_interval:
|
|
98
|
+
return
|
|
99
|
+
self._key_pan_repeat_interval = value
|
|
100
|
+
self.keyPanRepeatIntervalChanged.emit()
|
|
101
|
+
|
|
102
|
+
#: Milliseconds per held-key step (the frame timer integrates at dt / this value).
|
|
103
|
+
keyPanRepeatInterval: int = Property(
|
|
104
|
+
int,
|
|
105
|
+
_get_key_pan_repeat_interval,
|
|
106
|
+
_set_key_pan_repeat_interval,
|
|
107
|
+
notify=keyPanRepeatIntervalChanged,
|
|
108
|
+
)
|
|
109
|
+
|
|
88
110
|
#: Key sequences (QKeySequence text) for the six view commands. ``|`` separates
|
|
89
111
|
#: alternatives, so the default zoom-in is ``"+|="`` (Shift+= and the unshifted key).
|
|
90
112
|
panLeftShortcut: str = _shortcut_property("_pan_left", shortcutsChanged)
|
|
@@ -152,7 +152,7 @@ class Plot(QObject):
|
|
|
152
152
|
@Slot("QVariant", result=QObject)
|
|
153
153
|
def add_curve(
|
|
154
154
|
self,
|
|
155
|
-
expression: "str | sp.
|
|
155
|
+
expression: "str | sp.Basic",
|
|
156
156
|
color: object = None,
|
|
157
157
|
line_width: object = None,
|
|
158
158
|
label: str = "",
|
|
@@ -162,7 +162,8 @@ class Plot(QObject):
|
|
|
162
162
|
The artist follows the expression's free symbols: ``x`` (or none) is a 1-D curve
|
|
163
163
|
``y = f(x)``, ``x`` **and** ``y`` is an implicit curve — the level set ``F(x, y) = 0``,
|
|
164
164
|
so ``add_curve("x**2 + y**2 - 1")`` is a circle — and any other free symbol raises
|
|
165
|
-
``ValueError`` (substitute parameters first).
|
|
165
|
+
``ValueError`` (substitute parameters first). A sympy ``Eq(lhs, rhs)`` is normalized to
|
|
166
|
+
``lhs - rhs`` and treated as a level set even when only ``x`` is free.
|
|
166
167
|
"""
|
|
167
168
|
return self._curves.add_curve(expression, color, line_width, label)
|
|
168
169
|
|
|
@@ -170,13 +171,14 @@ class Plot(QObject):
|
|
|
170
171
|
@Slot("QVariant", result=QObject)
|
|
171
172
|
def add_implicit(
|
|
172
173
|
self,
|
|
173
|
-
expression: str | sp.
|
|
174
|
+
expression: str | sp.Basic,
|
|
174
175
|
color: object = None,
|
|
175
176
|
line_width: object = None,
|
|
176
177
|
label: str = "",
|
|
177
178
|
) -> Curve:
|
|
178
179
|
"""Add an implicit curve — the level set ``F(x, y) = 0`` — explicitly, whatever the
|
|
179
|
-
expression's free symbols (``x`` alone gives vertical lines)
|
|
180
|
+
expression's free symbols (``x`` alone gives vertical lines; ``Eq(lhs, rhs)`` is
|
|
181
|
+
normalized to ``lhs - rhs``).
|
|
180
182
|
|
|
181
183
|
Drawn per pixel like the other artists, with no sampling: ``|F|`` divided by ``|grad F|``
|
|
182
184
|
in pixels is the distance to the level set. Smooth, single-sheet level sets only for now
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// View layer composition: background, one shader pass per curve, screen-space furniture and
|
|
2
|
+
// pointer/keyboard input. The reusable pieces live in PlotCurves.qml, PlotFurniture.qml and
|
|
3
|
+
// PlotInput.qml; this file owns the model injection, the camera mapping and keyboard focus.
|
|
4
|
+
//
|
|
5
|
+
// Usage (the host creates the model and injects it; MVVM):
|
|
6
|
+
// PlotView { plot: myPlot }
|
|
7
|
+
// It also works standalone: the component brings its own empty Plot (`defaultExpression` adds
|
|
8
|
+
// a curve). The view owns no state: everything it draws is read from the Plot.
|
|
9
|
+
import QtQuick
|
|
10
|
+
import QmlMathPlot 1.0
|
|
11
|
+
|
|
12
|
+
Item {
|
|
13
|
+
id: root
|
|
14
|
+
|
|
15
|
+
implicitWidth: 800
|
|
16
|
+
implicitHeight: 600
|
|
17
|
+
|
|
18
|
+
// Keyboard navigation is opt-in by focus: a click (or a host focusing the QQuickWidget)
|
|
19
|
+
// makes this the active item, and the keys are handled only then, so a host's shortcuts
|
|
20
|
+
// keep working while the plot is idle. `focus: true` is the default *within* this scene,
|
|
21
|
+
// not a grab: the QQuickWidget is still ClickFocus and does not enter the Tab chain.
|
|
22
|
+
focus: true
|
|
23
|
+
Keys.onPressed: (event) => {
|
|
24
|
+
if (root._handleKeyPress(event))
|
|
25
|
+
event.accepted = true;
|
|
26
|
+
}
|
|
27
|
+
Keys.onReleased: (event) => {
|
|
28
|
+
if (root._handleKeyRelease(event))
|
|
29
|
+
event.accepted = true;
|
|
30
|
+
}
|
|
31
|
+
onActiveFocusChanged: {
|
|
32
|
+
if (!activeFocus)
|
|
33
|
+
root._clearHeld();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The model (injected by the host). Without injection the component brings its own plot;
|
|
37
|
+
// it adds no curve by itself. A standalone user opts into one with `defaultExpression`
|
|
38
|
+
// (``PlotView { defaultExpression: "sin(x)" }``) — no hidden default, and a host that
|
|
39
|
+
// injects a model or adds its own curves never races this handler.
|
|
40
|
+
property Plot plot: defaultPlot
|
|
41
|
+
property string defaultExpression: ""
|
|
42
|
+
Plot {
|
|
43
|
+
id: defaultPlot
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Forwarders, so a host can drive the component without knowing where the knob lives:
|
|
47
|
+
// `view.camera.aspect = 1.0`, `view.curves.add_curve("tan(x)")`. These are plain
|
|
48
|
+
// properties rather than QML aliases simply because an alias cannot reach through two
|
|
49
|
+
// objects (the plot is an injected property, not an id).
|
|
50
|
+
property QtObject camera: root.plot.camera
|
|
51
|
+
property QtObject curves: root.plot.curves
|
|
52
|
+
|
|
53
|
+
// World <-> screen. The camera maps the centre to the middle of the item, so screen x grows
|
|
54
|
+
// right and screen y grows down (vUV is top-down in the shader too).
|
|
55
|
+
function mapToScreen(x, y) {
|
|
56
|
+
const camera = root.plot.camera;
|
|
57
|
+
return Qt.point((x - camera.centre.x) / camera.scale_x + width / 2,
|
|
58
|
+
(camera.centre.y - y) / camera.scale_y + height / 2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mapFromScreen(point) {
|
|
62
|
+
const camera = root.plot.camera;
|
|
63
|
+
return Qt.point(camera.centre.x + (point.x - width / 2) * camera.scale_x,
|
|
64
|
+
camera.centre.y - (point.y - height / 2) * camera.scale_y);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Keyboard: arrow keys move the view (the camera moves, so the content follows), the
|
|
68
|
+
// zoom shortcuts anchor at the viewport centre. Every mapping is on camera.input, so a
|
|
69
|
+
// host can rebind it; an empty string disables a command.
|
|
70
|
+
//
|
|
71
|
+
// Held keys are tracked here rather than relying on the OS auto-repeat (which begins with
|
|
72
|
+
// a ~0.5 s delay and then repeats in bursts). One frame-rate Timer advances both pan and
|
|
73
|
+
// zoom by `dt / keyPanRepeatInterval` of a step, so movement is continuous and several
|
|
74
|
+
// keys combine (diagonal movement is normalised). The press itself still animates one
|
|
75
|
+
// step, so a tap keeps the one-step semantics.
|
|
76
|
+
property var _heldKeys: ({})
|
|
77
|
+
property real _lastTickMs: 0
|
|
78
|
+
|
|
79
|
+
Timer {
|
|
80
|
+
id: keyTimer
|
|
81
|
+
interval: 16
|
|
82
|
+
repeat: true
|
|
83
|
+
onTriggered: root._keyTick()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Connections {
|
|
87
|
+
target: root.plot.camera
|
|
88
|
+
function onPanEnabledChanged() {
|
|
89
|
+
if (!root.plot.camera.panEnabled) {
|
|
90
|
+
root._dropHeld("left");
|
|
91
|
+
root._dropHeld("right");
|
|
92
|
+
root._dropHeld("up");
|
|
93
|
+
root._dropHeld("down");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function onZoomEnabledChanged() {
|
|
97
|
+
if (!root.plot.camera.zoomEnabled) {
|
|
98
|
+
root._dropHeld("zoomIn");
|
|
99
|
+
root._dropHeld("zoomOut");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
Connections {
|
|
104
|
+
target: root.plot.camera.input
|
|
105
|
+
function onKeyboardEnabledChanged() {
|
|
106
|
+
if (!root.plot.camera.input.keyboardEnabled)
|
|
107
|
+
root._clearHeld();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// One command name for this event, or "" when it is not bound.
|
|
112
|
+
function _commandFor(event, requireEnabled) {
|
|
113
|
+
const camera = root.plot.camera;
|
|
114
|
+
const input = camera.input;
|
|
115
|
+
if (!input.keyboardEnabled)
|
|
116
|
+
return "";
|
|
117
|
+
if (input.matches(event.key, event.modifiers, input.panLeftShortcut))
|
|
118
|
+
return (!requireEnabled || camera.panEnabled) ? "left" : "";
|
|
119
|
+
if (input.matches(event.key, event.modifiers, input.panRightShortcut))
|
|
120
|
+
return (!requireEnabled || camera.panEnabled) ? "right" : "";
|
|
121
|
+
if (input.matches(event.key, event.modifiers, input.panUpShortcut))
|
|
122
|
+
return (!requireEnabled || camera.panEnabled) ? "up" : "";
|
|
123
|
+
if (input.matches(event.key, event.modifiers, input.panDownShortcut))
|
|
124
|
+
return (!requireEnabled || camera.panEnabled) ? "down" : "";
|
|
125
|
+
if (camera.zoomEnabled) {
|
|
126
|
+
if (input.matches(event.key, event.modifiers, input.zoomInShortcut))
|
|
127
|
+
return "zoomIn";
|
|
128
|
+
if (input.matches(event.key, event.modifiers, input.zoomOutShortcut))
|
|
129
|
+
return "zoomOut";
|
|
130
|
+
}
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _isPanCommand(command) {
|
|
135
|
+
return command === "left" || command === "right"
|
|
136
|
+
|| command === "up" || command === "down";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _clearHeld() {
|
|
140
|
+
root._heldKeys = ({});
|
|
141
|
+
keyTimer.stop();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _dropHeld(command) {
|
|
145
|
+
delete root._heldKeys[command];
|
|
146
|
+
if (Object.keys(root._heldKeys).length === 0)
|
|
147
|
+
keyTimer.stop();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _ensureTimer() {
|
|
151
|
+
if (!keyTimer.running) {
|
|
152
|
+
root._lastTickMs = Date.now();
|
|
153
|
+
keyTimer.start();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Frame-rate integration: a step per keyPanRepeatInterval, scaled by the real elapsed
|
|
158
|
+
// time (a delayed frame moves farther instead of slowing the pan down).
|
|
159
|
+
function _keyTick() {
|
|
160
|
+
const camera = root.plot.camera;
|
|
161
|
+
const input = camera.input;
|
|
162
|
+
if (!input.keyboardEnabled) {
|
|
163
|
+
root._clearHeld();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const dt = Math.min(100, Math.max(0, now - root._lastTickMs));
|
|
168
|
+
root._lastTickMs = now;
|
|
169
|
+
if (dt <= 0)
|
|
170
|
+
return;
|
|
171
|
+
const fraction = dt / Math.max(1, input.keyPanRepeatInterval);
|
|
172
|
+
|
|
173
|
+
let dx = 0;
|
|
174
|
+
let dy = 0;
|
|
175
|
+
if (root._heldKeys.left)
|
|
176
|
+
dx += 1;
|
|
177
|
+
if (root._heldKeys.right)
|
|
178
|
+
dx -= 1;
|
|
179
|
+
if (root._heldKeys.up)
|
|
180
|
+
dy += 1;
|
|
181
|
+
if (root._heldKeys.down)
|
|
182
|
+
dy -= 1;
|
|
183
|
+
if ((dx !== 0 || dy !== 0) && camera.panEnabled) {
|
|
184
|
+
if (dx !== 0 && dy !== 0) {
|
|
185
|
+
dx *= Math.SQRT1_2;
|
|
186
|
+
dy *= Math.SQRT1_2;
|
|
187
|
+
}
|
|
188
|
+
camera.pan_pixels(input.keyPanStep * root.width * dx * fraction,
|
|
189
|
+
input.keyPanStep * root.height * dy * fraction);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let zoom = 0;
|
|
193
|
+
if (root._heldKeys.zoomIn)
|
|
194
|
+
zoom += 1;
|
|
195
|
+
if (root._heldKeys.zoomOut)
|
|
196
|
+
zoom -= 1;
|
|
197
|
+
if (zoom !== 0 && camera.zoomEnabled) {
|
|
198
|
+
camera.zoom_by(120 * zoom * fraction, 0.5, 0.5, root.width, root.height);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _handleKeyPress(event) {
|
|
203
|
+
const command = root._commandFor(event, true);
|
|
204
|
+
if (command === "")
|
|
205
|
+
return false;
|
|
206
|
+
if (event.isAutoRepeat) // the Timer owns repeats
|
|
207
|
+
return true;
|
|
208
|
+
const camera = root.plot.camera;
|
|
209
|
+
const input = camera.input;
|
|
210
|
+
root._heldKeys[command] = true;
|
|
211
|
+
root._ensureTimer();
|
|
212
|
+
if (root._isPanCommand(command)) {
|
|
213
|
+
// One eased step for a tap; the frame timer cancels it on the first held frame
|
|
214
|
+
// and takes over continuously.
|
|
215
|
+
const step = input.keyPanStep;
|
|
216
|
+
if (command === "left")
|
|
217
|
+
camera.animate_pan(step * root.width, 0, camera.animationDuration);
|
|
218
|
+
else if (command === "right")
|
|
219
|
+
camera.animate_pan(-step * root.width, 0, camera.animationDuration);
|
|
220
|
+
else if (command === "up")
|
|
221
|
+
camera.animate_pan(0, step * root.height, camera.animationDuration);
|
|
222
|
+
else
|
|
223
|
+
camera.animate_pan(0, -step * root.height, camera.animationDuration);
|
|
224
|
+
} else if (command === "zoomIn") {
|
|
225
|
+
camera.animate_zoom(120, 0.5, 0.5, root.width, root.height,
|
|
226
|
+
camera.animationDuration);
|
|
227
|
+
} else if (command === "zoomOut") {
|
|
228
|
+
camera.animate_zoom(-120, 0.5, 0.5, root.width, root.height,
|
|
229
|
+
camera.animationDuration);
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _handleKeyRelease(event) {
|
|
235
|
+
const command = root._commandFor(event, false);
|
|
236
|
+
if (command === "")
|
|
237
|
+
return false;
|
|
238
|
+
if (event.isAutoRepeat)
|
|
239
|
+
return true;
|
|
240
|
+
root._dropHeld(command);
|
|
241
|
+
if (Object.keys(root._heldKeys).length > 0)
|
|
242
|
+
root._ensureTimer();
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The first report resolves the home view for the real size; later ones keep the scales, so
|
|
247
|
+
// nothing zooms while a window or a splitter is dragged.
|
|
248
|
+
Component.onCompleted: {
|
|
249
|
+
if (defaultExpression !== "" && curves.rowCount() === 0)
|
|
250
|
+
add_curve(defaultExpression);
|
|
251
|
+
root.plot.camera.set_viewport(width, height);
|
|
252
|
+
}
|
|
253
|
+
onWidthChanged: root.plot.camera.set_viewport(width, height)
|
|
254
|
+
onHeightChanged: root.plot.camera.set_viewport(width, height)
|
|
255
|
+
|
|
256
|
+
Rectangle {
|
|
257
|
+
anchors.fill: parent
|
|
258
|
+
color: root.plot.background
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
PlotCurves {
|
|
262
|
+
anchors.fill: parent
|
|
263
|
+
view: root
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
PlotFurniture {
|
|
267
|
+
anchors.fill: parent
|
|
268
|
+
view: root
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
PlotInput {
|
|
272
|
+
anchors.fill: parent
|
|
273
|
+
view: root
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
// View layer composition: background, one shader pass per curve, screen-space furniture and
|
|
2
|
-
// pointer/keyboard input. The reusable pieces live in PlotCurves.qml, PlotFurniture.qml and
|
|
3
|
-
// PlotInput.qml; this file owns the model injection, the camera mapping and keyboard focus.
|
|
4
|
-
//
|
|
5
|
-
// Usage (the host creates the model and injects it; MVVM):
|
|
6
|
-
// PlotView { plot: myPlot }
|
|
7
|
-
// It also works standalone: the component brings its own empty Plot (`defaultExpression` adds
|
|
8
|
-
// a curve). The view owns no state: everything it draws is read from the Plot.
|
|
9
|
-
import QtQuick
|
|
10
|
-
import QmlMathPlot 1.0
|
|
11
|
-
|
|
12
|
-
Item {
|
|
13
|
-
id: root
|
|
14
|
-
|
|
15
|
-
implicitWidth: 800
|
|
16
|
-
implicitHeight: 600
|
|
17
|
-
|
|
18
|
-
// Keyboard navigation is opt-in by focus: a click (or a host focusing the QQuickWidget)
|
|
19
|
-
// makes this the active item, and the keys are handled only then, so a host's shortcuts
|
|
20
|
-
// keep working while the plot is idle. `focus: true` is the default *within* this scene,
|
|
21
|
-
// not a grab: the QQuickWidget is still ClickFocus and does not enter the Tab chain.
|
|
22
|
-
focus: true
|
|
23
|
-
Keys.onPressed: (event) => {
|
|
24
|
-
if (root._handleKey(event))
|
|
25
|
-
event.accepted = true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// The model (injected by the host). Without injection the component brings its own plot;
|
|
29
|
-
// it adds no curve by itself. A standalone user opts into one with `defaultExpression`
|
|
30
|
-
// (``PlotView { defaultExpression: "sin(x)" }``) — no hidden default, and a host that
|
|
31
|
-
// injects a model or adds its own curves never races this handler.
|
|
32
|
-
property Plot plot: defaultPlot
|
|
33
|
-
property string defaultExpression: ""
|
|
34
|
-
Plot {
|
|
35
|
-
id: defaultPlot
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Forwarders, so a host can drive the component without knowing where the knob lives:
|
|
39
|
-
// `view.camera.aspect = 1.0`, `view.curves.add_curve("tan(x)")`. These are plain
|
|
40
|
-
// properties rather than QML aliases simply because an alias cannot reach through two
|
|
41
|
-
// objects (the plot is an injected property, not an id).
|
|
42
|
-
property QtObject camera: root.plot.camera
|
|
43
|
-
property QtObject curves: root.plot.curves
|
|
44
|
-
|
|
45
|
-
// World <-> screen. The camera maps the centre to the middle of the item, so screen x grows
|
|
46
|
-
// right and screen y grows down (vUV is top-down in the shader too).
|
|
47
|
-
function mapToScreen(x, y) {
|
|
48
|
-
const camera = root.plot.camera;
|
|
49
|
-
return Qt.point((x - camera.centre.x) / camera.scale_x + width / 2,
|
|
50
|
-
(camera.centre.y - y) / camera.scale_y + height / 2);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function mapFromScreen(point) {
|
|
54
|
-
const camera = root.plot.camera;
|
|
55
|
-
return Qt.point(camera.centre.x + (point.x - width / 2) * camera.scale_x,
|
|
56
|
-
camera.centre.y - (point.y - height / 2) * camera.scale_y);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Keyboard: arrow keys move the view (the camera moves, so the content follows), the
|
|
60
|
-
// zoom shortcuts anchor at the viewport centre. Every mapping is on camera.input, so a
|
|
61
|
-
// host can rebind it; an empty string disables a command.
|
|
62
|
-
function _matches(event, shortcut) {
|
|
63
|
-
return root.plot.camera.input.matches(event.key, event.modifiers, shortcut);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function _handleKey(event) {
|
|
67
|
-
const camera = root.plot.camera;
|
|
68
|
-
const input = camera.input;
|
|
69
|
-
if (!input.keyboardEnabled)
|
|
70
|
-
return false;
|
|
71
|
-
const dx = input.keyPanStep * root.width;
|
|
72
|
-
const dy = input.keyPanStep * root.height;
|
|
73
|
-
const duration = camera.animationDuration;
|
|
74
|
-
if (camera.panEnabled) {
|
|
75
|
-
if (_matches(event, input.panLeftShortcut)) {
|
|
76
|
-
camera.animate_pan(dx, 0, duration);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
if (_matches(event, input.panRightShortcut)) {
|
|
80
|
-
camera.animate_pan(-dx, 0, duration);
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
if (_matches(event, input.panUpShortcut)) {
|
|
84
|
-
camera.animate_pan(0, dy, duration);
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
if (_matches(event, input.panDownShortcut)) {
|
|
88
|
-
camera.animate_pan(0, -dy, duration);
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (camera.zoomEnabled) {
|
|
93
|
-
if (_matches(event, input.zoomInShortcut)) {
|
|
94
|
-
camera.animate_zoom(120, 0.5, 0.5, root.width, root.height, duration);
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
if (_matches(event, input.zoomOutShortcut)) {
|
|
98
|
-
camera.animate_zoom(-120, 0.5, 0.5, root.width, root.height, duration);
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// The first report resolves the home view for the real size; later ones keep the scales, so
|
|
106
|
-
// nothing zooms while a window or a splitter is dragged.
|
|
107
|
-
Component.onCompleted: {
|
|
108
|
-
if (defaultExpression !== "" && curves.rowCount() === 0)
|
|
109
|
-
add_curve(defaultExpression);
|
|
110
|
-
root.plot.camera.set_viewport(width, height);
|
|
111
|
-
}
|
|
112
|
-
onWidthChanged: root.plot.camera.set_viewport(width, height)
|
|
113
|
-
onHeightChanged: root.plot.camera.set_viewport(width, height)
|
|
114
|
-
|
|
115
|
-
Rectangle {
|
|
116
|
-
anchors.fill: parent
|
|
117
|
-
color: root.plot.background
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
PlotCurves {
|
|
121
|
-
anchors.fill: parent
|
|
122
|
-
view: root
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
PlotFurniture {
|
|
126
|
-
anchors.fill: parent
|
|
127
|
-
view: root
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
PlotInput {
|
|
131
|
-
anchors.fill: parent
|
|
132
|
-
view: root
|
|
133
|
-
}
|
|
134
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-colorblind.mplstyle
RENAMED
|
File without changes
|
{qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-dark-palette.mplstyle
RENAMED
|
File without changes
|
|
File without changes
|
{qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-darkgrid.mplstyle
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-notebook.mplstyle
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/seaborn-v0_8-whitegrid.mplstyle
RENAMED
|
File without changes
|
|
File without changes
|
{qmlmathplot-0.4.0 → qmlmathplot-0.4.1}/src/qmlmathplot/themes/tableau-colorblind10.mplstyle
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|