Добавить общие графики, декодер KONOR и порт STM32 bxCAN
This commit is contained in:
159
python/protocan/plot.py
Normal file
159
python/protocan/plot.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""Plot interaction port. All numerical operations use templates' set_plot.c.
|
||||
|
||||
This module has no Qt dependency. The application supplies its SETProtocol CDLL.
|
||||
Units, clocks, acquisition, colours and rendering belong to the application.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Axis(IntEnum):
|
||||
X = 1
|
||||
Y = 2
|
||||
|
||||
|
||||
class Marker(IntEnum):
|
||||
A = 0
|
||||
B = 1
|
||||
C = 2
|
||||
D = 3
|
||||
E = 4
|
||||
F = 5
|
||||
G = 6
|
||||
H = 7
|
||||
|
||||
@property
|
||||
def horizontal(self) -> bool:
|
||||
return self in (Marker.E, Marker.F, Marker.G, Marker.H)
|
||||
|
||||
|
||||
class PlotMath:
|
||||
"""Typed operations over the versioned allocation-free C ABI."""
|
||||
def __init__(self, library: ctypes.CDLL) -> None:
|
||||
self.library = library
|
||||
library.set_plot_abi_version.restype = ctypes.c_uint32
|
||||
library.set_plot_abi_version.argtypes = []
|
||||
if library.set_plot_abi_version() != 1:
|
||||
raise RuntimeError("Unsupported plot ABI")
|
||||
library.set_plot_eval.argtypes = [ctypes.c_uint32, ctypes.POINTER(ctypes.c_double),
|
||||
ctypes.c_size_t, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t]
|
||||
library.set_plot_eval.restype = ctypes.c_size_t
|
||||
|
||||
def call(self, operation: int, *values: float) -> tuple:
|
||||
inputs = (ctypes.c_double * len(values))(*values)
|
||||
output = (ctypes.c_double * 4)()
|
||||
count = self.library.set_plot_eval(operation, inputs, len(values), output, 4)
|
||||
if not count:
|
||||
raise ValueError("Invalid plot operation %s" % operation)
|
||||
return tuple(output[:count])
|
||||
|
||||
def pinch_axis(self, dx: float, dy: float, slop: float) -> Optional[Axis]:
|
||||
value = int(self.call(1, dx, dy, slop)[0])
|
||||
return Axis(value) if value else None
|
||||
|
||||
def tick_step(self, span: float, pixels: float) -> float:
|
||||
return self.call(5, span, pixels)[0]
|
||||
|
||||
def delta(self, a: float, b: float, multiplier: float = 1) -> float:
|
||||
return self.call(6, a, b, multiplier)[0]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Viewport:
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
width: float = 1.0
|
||||
height: float = 1.0
|
||||
|
||||
def transform(self, core: PlotMath, zoom_x: float = 1, zoom_y: float = 1,
|
||||
pan_x: float = 0, pan_y: float = 0,
|
||||
focus_x: float = 0.5, focus_y: float = 0.5) -> "Viewport":
|
||||
return Viewport(*core.call(0, self.x, self.y, self.width, self.height,
|
||||
zoom_x, zoom_y, pan_x, pan_y, focus_x, focus_y))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Bounds:
|
||||
left: float
|
||||
right: float
|
||||
bottom: float
|
||||
top: float
|
||||
|
||||
def fraction(self, core: PlotMath, value: float, horizontal: bool) -> float:
|
||||
return core.call(2, value, self.bottom if horizontal else self.left,
|
||||
self.top if horizontal else self.right, int(horizontal))[0]
|
||||
|
||||
def value(self, core: PlotMath, fraction: float, horizontal: bool) -> float:
|
||||
return core.call(3, fraction, self.bottom if horizontal else self.left,
|
||||
self.top if horizontal else self.right, int(horizontal))[0]
|
||||
|
||||
def visible(self, core: PlotMath, viewport: Viewport) -> "Bounds":
|
||||
return Bounds(self.value(core, viewport.x, False),
|
||||
self.value(core, viewport.x + viewport.width, False),
|
||||
self.value(core, viewport.y + viewport.height, True),
|
||||
self.value(core, viewport.y, True))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Markers:
|
||||
x_enabled: bool = True
|
||||
y_enabled: bool = False
|
||||
selected: Marker = Marker.A
|
||||
a: Optional[float] = None
|
||||
b: Optional[float] = None
|
||||
c: Optional[float] = None
|
||||
d: Optional[float] = None
|
||||
e: Optional[float] = None
|
||||
f: Optional[float] = None
|
||||
g: Optional[float] = None
|
||||
h: Optional[float] = None
|
||||
|
||||
def value(self, marker: Marker) -> Optional[float]:
|
||||
return getattr(self, marker.name.lower())
|
||||
|
||||
def enabled(self, marker: Marker) -> bool:
|
||||
return self.y_enabled if marker.horizontal else self.x_enabled
|
||||
|
||||
def move(self, marker: Marker, value: float) -> "Markers":
|
||||
return replace(self, **{marker.name.lower(): value})
|
||||
|
||||
def positioned(self, core: PlotMath, bounds: Bounds) -> "Markers":
|
||||
result = self
|
||||
for marker, fraction in ((Marker.A, .2), (Marker.B, .4), (Marker.C, .6), (Marker.D, .8),
|
||||
(Marker.E, .2), (Marker.F, .4), (Marker.G, .6), (Marker.H, .8)):
|
||||
if result.value(marker) is None:
|
||||
result = result.move(marker, bounds.value(core, fraction, marker.horizontal))
|
||||
return result
|
||||
|
||||
def reset(self, core: PlotMath, bounds: Bounds) -> "Markers":
|
||||
return replace(self, a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None).positioned(core, bounds)
|
||||
|
||||
def fraction(self, core: PlotMath, marker: Marker, bounds: Bounds) -> float:
|
||||
value = self.value(marker)
|
||||
if value is None:
|
||||
raise ValueError("Marker has no position")
|
||||
return bounds.fraction(core, value, marker.horizontal)
|
||||
|
||||
def drag(self, core: PlotMath, marker: Marker, delta: float, length: float, bounds: Bounds) -> "Markers":
|
||||
value = core.call(4, self.value(marker), delta, length,
|
||||
bounds.bottom if marker.horizontal else bounds.left,
|
||||
bounds.top if marker.horizontal else bounds.right, int(marker.horizontal))[0]
|
||||
return replace(self.move(marker, value), selected=marker)
|
||||
|
||||
def hit(self, core: PlotMath, x: float, y: float, width: float, height: float,
|
||||
radius: float, bounds: Bounds) -> Optional[Marker]:
|
||||
if not (0 <= x <= width and 0 <= y <= height):
|
||||
return None
|
||||
candidates = []
|
||||
for marker in Marker:
|
||||
if not self.enabled(marker) or self.value(marker) is None:
|
||||
continue
|
||||
fraction = self.fraction(core, marker, bounds)
|
||||
distance = abs(y - fraction * height if marker.horizontal else x - fraction * width)
|
||||
if 0 <= fraction <= 1 and distance <= radius:
|
||||
candidates.append((distance, marker != self.selected, marker))
|
||||
return min(candidates)[2] if candidates else None
|
||||
Reference in New Issue
Block a user