Add native QCustomPlot renderer and shared plot adapters
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Isolated native QCustomPlot renderer; no Qt ABI mixing with PySide."""
|
||||
from array import array
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
from threading import Lock
|
||||
try:
|
||||
from PySide6.QtCore import QObject, QRunnable, Signal
|
||||
except ImportError:
|
||||
from PySide2.QtCore import QObject, QRunnable, Signal
|
||||
|
||||
class NativePlotSignals(QObject):
|
||||
ready = Signal(str, object, object)
|
||||
|
||||
class NativePlotEngine:
|
||||
def __init__(self, executable):
|
||||
self.executable = Path(executable)
|
||||
self.process = None
|
||||
self.closed = False
|
||||
self.lock = Lock()
|
||||
self.directory = tempfile.TemporaryDirectory(prefix='setgui-qcp-')
|
||||
self.records = {}
|
||||
self.serial = 0
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
if self.process is not None:
|
||||
self.process.kill()
|
||||
if self.process.stdin is not None:
|
||||
self.process.stdin.close()
|
||||
# Files are owned by this TemporaryDirectory and released with the engine.
|
||||
|
||||
def render(self, times, values, bounds, width, height, color):
|
||||
with self.lock:
|
||||
if self.closed:
|
||||
raise RuntimeError("Plot closed")
|
||||
if self.process is None or self.process.poll() is not None:
|
||||
env = os.environ.copy()
|
||||
env['QT_QPA_PLATFORM'] = 'offscreen'
|
||||
env.pop('QT_PLUGIN_PATH', None)
|
||||
env['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(self.executable.parent / 'platforms')
|
||||
self.process = subprocess.Popen([str(self.executable)], stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env, bufsize=0,
|
||||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0), cwd=self.executable.parent)
|
||||
key = (id(times), id(values))
|
||||
record = self.records.get(key)
|
||||
if record is None:
|
||||
self.serial += 1
|
||||
path = Path(self.directory.name) / f'{self.serial}.bin'
|
||||
with path.open('wb') as stream:
|
||||
array('d', times).tofile(stream)
|
||||
array('d', values).tofile(stream)
|
||||
record = (path, times, values)
|
||||
self.records[key] = record
|
||||
if len(self.records) > 4:
|
||||
self.records.pop(next(iter(self.records)))
|
||||
request = dict(file=str(record[0]), count=len(times), width=width, height=height,
|
||||
left=bounds.left, right=bounds.right, bottom=bounds.bottom, top=bounds.top, color=color)
|
||||
self.process.stdin.write((json.dumps(request)+'\n').encode())
|
||||
self.process.stdin.flush()
|
||||
size = int(self.process.stdout.readline())
|
||||
if not 0 < size < 100_000_000:
|
||||
raise RuntimeError('Invalid QCustomPlot frame')
|
||||
chunks = bytearray()
|
||||
while len(chunks) < size:
|
||||
chunk = self.process.stdout.read(size-len(chunks))
|
||||
if not chunk:
|
||||
raise RuntimeError('QCustomPlot renderer stopped')
|
||||
chunks.extend(chunk)
|
||||
return bytes(chunks)
|
||||
|
||||
class NativePlotJob(QRunnable):
|
||||
def __init__(self, engine, key, geometry, times, values, bounds, width, height, color, signals):
|
||||
super().__init__()
|
||||
self.engine, self.key, self.geometry, self.signals = engine, key, geometry, signals
|
||||
self.args = times, values, bounds, width, height, color
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
png = self.engine.render(*self.args)
|
||||
except Exception:
|
||||
png = None
|
||||
try:
|
||||
self.signals.ready.emit(self.key, self.geometry, png)
|
||||
except RuntimeError:
|
||||
pass
|
||||
Reference in New Issue
Block a user