diff --git a/examples/laser.fl b/examples/laser.fl index 337f2958..49d14f6b 100644 --- a/examples/laser.fl +++ b/examples/laser.fl @@ -5,7 +5,7 @@ func objects(nodes, count=10) for i in ..10 let period=2*beta(:beta)[i]+1 phase=uniform(:phase)[i] b=beat/period+phase let x=map(quad(b%1), beta(:x;i)[b], beta(:x;i)[b+1])-0.5 y=map(quad(b%1), beta(:y;i)[b], beta(:y;i)[b+1])-0.5 - let c=map(quad(b%1), hsv(uniform(:hue;i)[b];1;0.5), hsv(uniform(:hue;i)[b+1];1;1)) + let c=map(quad(b%1), hsv(uniform(:hue;i)[b];1;1), hsv(uniform(:hue;i)[b+1];1;1)) !group translate=x;y rotate=phase color=c if i % 2 !ellipse radius=0.05 diff --git a/src/flitter/model.pxd b/src/flitter/model.pxd index 038ef3fe..22211643 100644 --- a/src/flitter/model.pxd +++ b/src/flitter/model.pxd @@ -49,6 +49,7 @@ cdef class Vector: cdef int compare(self, Vector other) except -2 cdef Vector slice(self, Vector index) cdef Vector item(self, int i) + cdef Vector items(self, int start, int end) cpdef double squared_sum(self) noexcept cpdef Vector normalize(self) cpdef Vector dot(self, Vector other) diff --git a/src/flitter/model.pyx b/src/flitter/model.pyx index 21b579c9..f0be12c2 100644 --- a/src/flitter/model.pyx +++ b/src/flitter/model.pyx @@ -912,6 +912,34 @@ cdef class Vector: result.numbers[0] = self.numbers[i] return result + cdef Vector items(self, int start, int end): + cdef int i, j, n = self.length, m = end - start + if m <= 0 or end <= 0 or start >= n: + return null_ + cdef list objects = self.objects + cdef PyObject* objptr + cdef Vector result = Vector.__new__(Vector) + result.allocate_numbers(m) + if objects is not None: + j = 0 + for i in range(start, end): + objptr = PyList_GET_ITEM(objects, i) + if type(objptr) is float: + result.numbers[j] = PyFloat_AS_DOUBLE(objptr) + elif type(objptr) is int: + result.numbers[j] = PyLong_AsDouble(objptr) + else: + result.deallocate_numbers() + result.objects = list(self.objects) + break + j += 1 + else: + j = 0 + for i in range(start, end): + result.numbers[j] = self.numbers[i] + j += 1 + return result + @cython.cdivision(True) cpdef double squared_sum(self) noexcept: cdef int i, n = self.length @@ -1871,7 +1899,7 @@ cdef class Node: for i in range(result.allocate_numbers(n)): result.numbers[i] = value.numbers[0] return result - elif m == n: + elif m == n or n == 0: return value return default diff --git a/src/flitter/render/laser.pyx b/src/flitter/render/laser.pyx index d558837c..b69479ca 100644 --- a/src/flitter/render/laser.pyx +++ b/src/flitter/render/laser.pyx @@ -14,22 +14,24 @@ import numpy as np import usb.core from .. import name_patch -from .. cimport model +from ..model cimport Node, Vector, Matrix33, null_ logger = name_patch(logger, __name__) -cdef double TWO_PI = 2*np.pi +cdef double Tau = 2*np.pi +cdef Vector Zero2 = Vector((0, 0)) +cdef Vector Half2 = Vector((0.5, 0.5)) +cdef Vector Zero3 = Vector((0, 0, 0)) @cython.cdivision(True) @cython.boundscheck(False) @cython.wraparound(False) -cdef double[:] travel(double distance, double accelleration, double start_speed, double end_speed): +cdef Vector travel(double distance, double accelleration, double start_speed, double end_speed): cdef double d0, t0, s, t1, tt, tm, t, tp, d cdef int i, n - cdef double[:] ds d0 = abs(end_speed*end_speed - start_speed*start_speed) / (2*accelleration) t0 = abs(end_speed - start_speed) / accelleration s = max(start_speed, end_speed) @@ -37,19 +39,19 @@ cdef double[:] travel(double distance, double accelleration, double start_speed, tt = t0 + 2*t1 tm = t0 + t1 if end_speed > start_speed else t1 n = max((round(tt)), 1) - distances = np.zeros(n) - ds = distances + cdef Vector distances = Vector.__new__(Vector) + distances.allocate_numbers(n) for i in range(1, n+1): t = i * tt / n tp = min(tm, t) d = start_speed*t + tp*tp*accelleration/2 tp = max(0, t-tm) d += tp*tm*accelleration - tp*tp*accelleration/2 - ds[i-1] = d + distances.numbers[i-1] = d return distances -cdef double max_speed(double distance, double accelleration, double start_speed): - return sqrt(2*distance*accelleration + start_speed * start_speed) +cdef inline double max_speed(double distance, double accelleration, double start_speed): + return sqrt(2*distance*accelleration + start_speed*start_speed) cdef double distance(tuple p0, tuple p1): cdef double x0, y0, x1, y1, dx, dy @@ -65,7 +67,6 @@ cdef class LaserDriver: DEFAULT_ACCELLERATION = 10 DEFAULT_EPSILON = 0.0001 - cdef list _sample_bunches cdef int _sample_rate cdef double _accelleration, _epsilon @@ -74,7 +75,7 @@ cdef class LaserDriver: return self._sample_rate @sample_rate.setter - def sample_rate(self, value): + def sample_rate(self, int value): self._sample_rate = value @property @@ -82,7 +83,7 @@ cdef class LaserDriver: return self._accelleration @accelleration.setter - def accelleration(self, value): + def accelleration(self, double value): self._accelleration = value @property @@ -90,79 +91,113 @@ cdef class LaserDriver: return self._epsilon @epsilon.setter - def epsilon(self, value): + def epsilon(self, double value): self._epsilon = value - def __init__(self, sample_rate=None, accelleration=None, epsilon=None): - self._sample_bunches = [np.array([(0.5, 0.5, 0, 0, 0)])] - self._sample_rate = sample_rate if sample_rate is not None else self.DEFAULT_SAMPLE_RATE - self._accelleration = accelleration if accelleration is not None else self.DEFAULT_ACCELLERATION - self._epsilon = epsilon if epsilon is not None else self.DEFAULT_EPSILON + def __init__(self): + self._sample_rate = self.DEFAULT_SAMPLE_RATE + self._accelleration = self.DEFAULT_ACCELLERATION + self._epsilon = self.DEFAULT_EPSILON - def close(self): - pass + async def start(self): + raise NotImplementedError() + + async def stop(self): + raise NotImplementedError() + + async def start_update(self, Node node): + raise NotImplementedError() + + async def append_samples_bunch(self, Vector bunch): + raise NotImplementedError() - async def flush(self): + async def finish_update(self): raise NotImplementedError() @cython.cdivision(True) @cython.boundscheck(False) @cython.wraparound(False) - cpdef draw_path(self, points, tuple color): - cdef int i, j, n, m - cdef double a, s, dv, dx, dy, l, x, y - cdef double[:] accels, distances, speeds, lengths - cdef double[:, :] samples, coords, normals + cpdef Vector draw_path(self, Vector last, Vector points, Vector color, Matrix33 transform): + cdef int i, j, k, n, m + cdef double a, s, dv, dx, dy, l, x, y, last_x, last_y + cdef Vector accels, speeds, lengths, samples, normals, coords, bunch cdef double r, g, b - r = min(max(0, (color[0])), 1) - g = min(max(0, (color[1])), 1) - b = min(max(0, (color[2])), 1) + r, g, b = color.numbers[0], color.numbers[1], color.numbers[2] a = self._accelleration / self._sample_rate - coords = np.vstack((self._sample_bunches[len(self._sample_bunches)-1][-1:, :2], points)) - n = len(coords) - lengths = np.zeros(n-1) - normals = np.zeros((n-1, 2)) - accels = np.zeros(n-1) + coords = last.concat(points) + n = coords.length // 2 + lengths = Vector.__new__(Vector) + lengths.allocate_numbers(n) + normals = Vector.__new__(Vector) + normals.allocate_numbers(n*2) + accels = Vector.__new__(Vector) + accels.allocate_numbers(n) i = 0 + last_x, last_y = coords.numbers[0], coords.numbers[1] for j in range(1, n): - dx = coords[j, 0] - coords[i, 0] - dy = coords[j, 1] - coords[i, 1] + x, y = coords.numbers[j*2], coords.numbers[j*2+1] + dx, dy = x-last_x, y-last_y l = sqrt(dx*dx + dy*dy) if l < self._epsilon: n -= 1 continue - lengths[i] = l - normals[i, 0] = dx / l - normals[i, 1] = dy / l - accels[i] = a / (max(abs(dx), abs(dy)) / l) + lengths.numbers[i] = l + normals.numbers[i*2] = dx / l + normals.numbers[i*2+1] = dy / l + accels.numbers[i] = a / (max(abs(dx), abs(dy)) / l) i += 1 if i != j: - coords[i, 0] = coords[j, 0] - coords[i, 1] = coords[j, 1] + coords.numbers[i*2] = x + coords.numbers[i*2+1] = y + last_x, last_y = x, y if n == 1: return - speeds = np.zeros(n) + speeds = Vector.__new__(Vector) + speeds.allocate_numbers(n) + s = 0 + speeds.numbers[0] = s for i in range(1, n-1): - s = max_speed(lengths[i-1], accels[i-1], speeds[i-1]) - dv = max(abs(normals[i, 0] - normals[i-1, 0]), abs(normals[i, 1] - normals[i-1, 1])) - speeds[i] = min(s, a / 2 / dv) if dv else s + s = max_speed(lengths.numbers[i-1], accels.numbers[i-1], s) + dv = max(abs(normals.numbers[i*2] - normals.numbers[(i-1)*2]), abs(normals.numbers[i*2+1] - normals.numbers[(i-1)*+1])) + if dv: + s = min(s, a / 2 / dv) + speeds.numbers[i] = s for i in range(n-2, 0, -1): - speeds[i] = min(speeds[i], max_speed(lengths[i], accels[i], speeds[i+1])) + speeds.numbers[i] = min(speeds.numbers[i], max_speed(lengths.numbers[i], accels.numbers[i], speeds.numbers[i+1])) + cdef Vector distances for i in range(n-1): - distances = travel(lengths[i], accels[i], speeds[i], speeds[i+1]) - m = len(distances) - bunch = np.zeros((m, 5)) - samples = bunch + distances = travel(lengths.numbers[i], accels.numbers[i], speeds.numbers[i], speeds.numbers[i+1]) + m = distances.length + bunch = Vector.__new__(Vector) + bunch.allocate_numbers(m*5) for j in range(m): - x = normals[i, 0] * distances[j] + coords[i, 0] - y = normals[i, 1] * distances[j] + coords[i, 1] - samples[j, 0] = min(max(0, x), 1) - samples[j, 1] = min(max(0, y), 1) + x = normals.numbers[i*2] * distances.numbers[j] + coords.numbers[i*2] + y = normals.numbers[i*2+1] * distances.numbers[j] + coords.numbers[i*2+1] + k = j * 5 + bunch.numbers[k] = min(max(0, x), 1) + bunch.numbers[k+1] = min(max(0, y), 1) if i and 0 <= x <= 1 and 0 <= y <= 1: - samples[j, 2] = r - samples[j, 3] = g - samples[j, 4] = b - self._sample_bunches.append(bunch) + bunch.numbers[k+2] = r + bunch.numbers[k+3] = g + bunch.numbers[k+4] = b + return bunch + + +cdef class DummyLaserDriver(LaserDriver): + async def start(self): + logger.debug("Starting dummy laser driver") + + async def stop(self): + logger.debug("Stopping dummy laser driver") + + async def start_update(self, Node node): + pass + + async def append_samples_bunch(self, Vector bunch): + logger.trace("Samples bunch: {}", repr(bunch)) + + async def finish_update(self): + pass cdef class LaserCubeDriver(LaserDriver): @@ -189,31 +224,15 @@ cdef class LaserCubeDriver(LaserDriver): cdef _device cdef _control_out, _control_in, _data_out - cdef int _dac_min, _dac_range, _max_dac_rate + cdef object _samples_queue + cdef object _run_task cdef double _dac_lag - def __init__(self, id, dac_lag=None, **kwargs): - super().__init__(**kwargs) - for device in usb.core.find(idVendor=self.VENDOR_ID, idProduct=self.PRODUCT_ID, find_all=True): - if id is None or device.serial_number == id: - break - else: - raise ValueError("No device found") - self._device = device - self._device.set_configuration() - self._device.set_interface_altsetting(1, 1) - configuration = self._device.get_active_configuration() - self._control_out, self._control_in = configuration[0, 0].endpoints() - self._data_out, = configuration[1, 1].endpoints() - self._dac_min = self._execute(self.GET_DAC_MINIMUM_VALUE, 'I') - self._dac_range = self._execute(self.GET_DAC_MAXIMUM_VALUE, 'I') - self._dac_min - self._max_dac_rate = self._execute(self.GET_MAXIMUM_DAC_RATE, 'I') - self._dac_lag = dac_lag if dac_lag is not None else self.DEFAULT_DAC_LAG - self._sample_rate = min(max(0, self._sample_rate), self._max_dac_rate) - self._execute(self.SET_DAC_RATE, None, 'I', self._sample_rate) - self._execute(self.SET_ENABLED, None, 'B', 1) - major, minor = self._execute(self.GET_MAJOR_VERSION, 'I'), self._execute(self.GET_MINOR_VERSION, 'I') - logger.info("Configured USB LaserCube '{} {}' (firmware {}.{})", self._device.manufacturer, self._device.product, major, minor) + def __init__(self, id): + super().__init__() + self._device = None + self._dac_lag = self.DEFAULT_DAC_LAG + self._samples_queue = None @property def sample_rate(self): @@ -222,11 +241,106 @@ cdef class LaserCubeDriver(LaserDriver): @sample_rate.setter def sample_rate(self, value): self._sample_rate = min(max(0, value), self._max_dac_rate) - self._execute(self.SET_DAC_RATE, None, 'I', self._sample_rate) - def close(self): - self._execute(self.SET_ENABLED, None, 'B', 0) - self._device = self._control_in = self._control_out = self._data_out = None + async def start(self): + self._run_task = asyncio.create_task(self._run()) + + async def stop(self): + self._run_task.cancel() + await self._run_task + self._run_task = None + + async def start_update(self, Node node): + if self._run_task.done(): + await self._run_task + self._dac_lag = node.get('dac_lag', 1, float, self.DEFAULT_DAC_LAG) + + async def append_samples_bunch(self, Vector bunch): + if self._samples_queue is not None: + await self._samples_queue.put(bunch) + else: + logger.trace("LaserCube driver not ready, dumped {} sample bunch", bunch.length // 5) + + async def finish_update(self): + pass + + async def _run(self): + cdef int i, j, n, lag + cdef Vector samples, bunch + cdef unsigned short[:, :] output + cdef int sample_rate, current_sample_rate + cdef int dac_min, dac_range, max_dac_rate + cdef bint enabled = False + while True: + device = usb.core.find(idVendor=self.VENDOR_ID, idProduct=self.PRODUCT_ID) + if device is None: + await asyncio.sleep(1) + continue + try: + self._device = device + self._device.set_configuration() + self._device.set_interface_altsetting(1, 1) + configuration = self._device.get_active_configuration() + self._control_out, self._control_in = configuration[0, 0].endpoints() + self._data_out, = configuration[1, 1].endpoints() + self._execute(self.SET_ENABLED, None, 'B', 0) + enabled = False + dac_min = self._execute(self.GET_DAC_MINIMUM_VALUE, 'I') + dac_range = self._execute(self.GET_DAC_MAXIMUM_VALUE, 'I') - self._dac_min + max_dac_rate = self._execute(self.GET_MAXIMUM_DAC_RATE, 'I') + major, minor = self._execute(self.GET_MAJOR_VERSION, 'I'), self._execute(self.GET_MINOR_VERSION, 'I') + current_sample_rate = min(max(0, self.DEFAULT_SAMPLE_RATE), max_dac_rate) + self._execute(self.SET_DAC_RATE, None, 'I', current_sample_rate) + enabled = False + logger.info("Configured USB LaserCube '{} {}' (firmware {}.{})", self._device.manufacturer, self._device.product, major, minor) + samples = None + self._samples_queue = asyncio.Queue(maxsize=10) + while True: + try: + bunch = await asyncio.wait_for(self._samples_queue.get(), timeout=1) + except asyncio.TimeoutError: + if enabled: + self._execute(self.SET_ENABLED, None, 'B', 0) + enabled = False + continue + if samples is None: + samples = bunch + else: + samples = samples.concat(bunch) + sample_rate = min(max(0, self._sample_rate), max_dac_rate) + if sample_rate != current_sample_rate: + self._execute(self.SET_DAC_RATE, None, 'I', sample_rate) + current_sample_rate = sample_rate + if not enabled: + self._execute(self.SET_ENABLED, None, 'B', 1) + enabled = True + lag = max(0, (round(self._sample_rate * self._dac_lag))) + if len(samples) <= lag: + continue + n = len(samples) - lag + output_array = np.zeros((n, 4), dtype='uint16') + output = output_array + lag *= 5 + for i in range(n): + j = i * 5 + output[i, 0] = (round(samples.numbers[j+2] * 255)) + ((round(samples.numbers[j+3] * 255)) << 8) + output[i, 1] = (round(samples.numbers[j+4] * 255)) + output[i, 2] = (round((1 - samples.numbers[j+lag]) * self._dac_range)) + self._dac_min + output[i, 3] = (round((1 - samples.numbers[j+1+lag]) * self._dac_range)) + self._dac_min + logger.trace("Writing {} sample frame", len(output)) + await asyncio.to_thread(self._data_out.write, output_array.tobytes()) + if lag: + samples = samples.items(samples.length-lag, samples.length) + else: + samples = None + except asyncio.CancelledError: + if enabled: + self._execute(self.SET_ENABLED, None, 'B', 0) + self._device = self._control_in = self._control_out = self._data_out = None + self._samples_queue = None + except Exception as exc: + logger.error("Unexpected exception in LaserCube run task: {}", exc) + raise def _execute(self, int command, str results_format, str values_format='', *values): self._control_out.write(struct.pack(' 1 else results[0] - @cython.boundscheck(False) - async def flush(self): - cdef int i, n, lag = max(1, (round(self._sample_rate * self._dac_lag))) - samples_array = np.vstack(self._sample_bunches) - cdef double[:, :] samples = samples_array - cdef unsigned short[:, :] output - if len(samples) > lag: - n = len(samples) - lag - output_array = np.zeros((n, 4), dtype='uint16') - output = output_array - for i in range(n): - output[i, 0] = (round(samples[i, 2] * 255)) + ((round(samples[i, 3] * 255)) << 8) - output[i, 1] = (round(samples[i, 4] * 255)) - output[i, 2] = (round((1 - samples[i+lag, 0]) * self._dac_range)) + self._dac_min - output[i, 3] = (round((1 - samples[i+lag, 1]) * self._dac_range)) + self._dac_min - logger.trace("Writing {} sample frame", len(output)) - await asyncio.to_thread(self._data_out.write, output_array.tobytes()) - self._sample_bunches = [samples_array[-lag:]] - else: - self._sample_bunches = [samples_array] - - -cdef class AffineTransform: - cdef double v00, v01, v02 - cdef double v10, v11, v12 - cdef double v20, v21, v22 - - @classmethod - def identity(cls): - cdef AffineTransform a = cls() - a.v00 = a.v11 = a.v22 = 1 - return a - - @classmethod - def translate(cls, double x, double y): - cdef AffineTransform a = cls() - a.v00 = a.v11 = a.v22 = 1 - a.v02 = x - a.v12 = y - return a - - @classmethod - def scale(cls, double sx, double sy): - cdef AffineTransform a = cls() - a.v00 = sx - a.v11 = sy - a.v22 = 1 - return a - - @classmethod - def rotate(cls, double th): - cdef AffineTransform a = cls() - cdef double sx = cos(th), sy = sin(th) - a.v00 = sx - a.v01 = -sy - a.v10 = sy - a.v11 = sx - a.v22 = 1 - return a - - cpdef tuple t(self, double x, double y): - return (x*self.v00 + y*self.v01 + self.v02, x*self.v10 + y*self.v11 + self.v12) - - def __matmul__(AffineTransform a, AffineTransform b): - cdef AffineTransform c = AffineTransform() - c.v00 = a.v00*b.v00 + a.v01*b.v10 + a.v02*b.v20 - c.v01 = a.v00*b.v01 + a.v01*b.v11 + a.v02*b.v21 - c.v02 = a.v00*b.v02 + a.v01*b.v12 + a.v02*b.v22 - c.v10 = a.v10*b.v00 + a.v11*b.v10 + a.v12*b.v20 - c.v11 = a.v10*b.v01 + a.v11*b.v11 + a.v12*b.v21 - c.v12 = a.v10*b.v02 + a.v11*b.v12 + a.v12*b.v22 - c.v20 = a.v20*b.v00 + a.v21*b.v10 + a.v22*b.v20 - c.v21 = a.v20*b.v01 + a.v21*b.v11 + a.v22*b.v21 - c.v22 = a.v20*b.v02 + a.v21*b.v12 + a.v22*b.v22 - return c - cdef class Laser: cdef LaserDriver driver + cdef Vector last_point def __init__(self, **kwargs): self.driver = None @@ -328,109 +367,127 @@ cdef class Laser: self.driver.close() self.driver = None - async def update(self, engine, model.Node node, **kwargs): + async def update(self, engine, Node node, **kwargs): driver = node.get('driver', 1, str, '').lower() - cls = {'lasercube': LaserCubeDriver}.get(driver) + cls = {'lasercube': LaserCubeDriver, 'dummy': DummyLaserDriver}.get(driver) if cls is not None: id = node.get('id', 1, str) + if not isinstance(self.driver, cls): + if self.driver is not None: + await self.driver.stop() + self.driver = cls(id) + await self.driver.start() + self.last_point = Half2 sample_rate = node.get('sample_rate', 1, int, cls.DEFAULT_SAMPLE_RATE) accelleration = node.get('accelleration', 1, float, cls.DEFAULT_ACCELLERATION) epsilon = node.get('epsilon', 1, float, cls.DEFAULT_EPSILON) - if not isinstance(self.driver, cls): - if self.driver is not None: - self.driver.close() - self.driver = cls(id, sample_rate=sample_rate, accelleration=accelleration, epsilon=epsilon) - else: - if sample_rate != self.driver.sample_rate: - self.driver.sample_rate = sample_rate - if accelleration != self.driver.accelleration: - self.driver.accelleration = accelleration - if epsilon != self.driver.epsilon: - self.driver.epsilon = epsilon - color = (0., 0., 0.) - transform = AffineTransform.identity() + if sample_rate != self.driver.sample_rate: + self.driver.sample_rate = sample_rate + if accelleration != self.driver.accelleration: + self.driver.accelleration = accelleration + if epsilon != self.driver.epsilon: + self.driver.epsilon = epsilon + transform = Matrix33() paths = [] - self.collect_paths(node, paths, color, transform) + self.collect_paths(node, paths, Zero3, transform) if paths: - self.draw_paths(paths) - await self.driver.flush() + await self.driver.start_update(node) + await self.draw_paths(paths) + await self.driver.finish_update() elif self.driver is not None: - self.driver.close() + await self.driver.stop() self.driver = None - def draw_paths(self, list paths not None): - cdef list points - cdef tuple pair, color, point, last=(0.5, 0.5) + async def draw_paths(self, list paths): cdef int i, nearest_i cdef double d, nearest_d=0 + cdef Vector points, color, first + cdef Matrix33 transform + cdef Vector last = self.last_point while paths: for i in range(len(paths)): - pair = paths[i] - points = pair[0] - d = distance(last, points[0]) + points, color, transform = paths[i] + first = transform.vmul(points.items(0, 2)) + d = first.sub(last).squared_sum() if i == 0 or d < nearest_d: nearest_d = d nearest_i = i - pair = paths.pop(nearest_i) - points = pair[0] - color = pair[1] - self.driver.draw_path(points, color) - last = points[-1] + points, color, transform = paths.pop(nearest_i) + bunch = self.driver.draw_path(last, points, color, transform) + await self.driver.append_samples_bunch(bunch) + last = transform.vmul(points.items(points.length-2, points.length)) + self.last_point = last @cython.cdivision(True) - def collect_paths(self, model.Node node not None, list paths not None, tuple color not None, AffineTransform transform not None): + cpdef void collect_paths(self, Node node, list paths, Vector color, Matrix33 transform): cdef double x, y, sx, sy, th - cdef int i, n - cdef list path, points + cdef int i, j, m, n + cdef Vector size, point, points, path + cdef Matrix33 matrix + cdef bint close - c = node.get('color', 3, float) - if c is not None: - color = tuple(c) + color = node.get_fvec('color', 3, color) if node.kind in ('laser', 'group'): for key in node.keys(): if key == 'translate': - translate = node.get('translate', 2, float) - transform = transform @ AffineTransform.translate(*translate) + point = node.get_fvec('translate', 2, null_) + if (matrix := Matrix33._translate(point)) != None: + transform = transform.mmul(matrix) elif key == 'scale': - scale = node.get('scale', 2, float) - transform = transform @ AffineTransform.scale(*scale) + point = node.get_fvec('scale', 2, null_) + if (matrix := Matrix33._scale(point)) != None: + transform = transform.mmul(matrix) elif key == 'rotate': - rotate = node.get('rotate', 1, float) - transform = transform @ AffineTransform.rotate(TWO_PI * rotate) + th = node.get_float('rotate', 0) + if (matrix := Matrix33._rotate(th)) != None: + transform = transform.mmul(matrix) for child in node.children: self.collect_paths(child, paths, color, transform) elif node.kind == 'line': - points = node.get('points', 0, float) - n = len(points) - if n >= 2: - path = [] - for i in range(0, n, 2): - path.append(transform.t(points[i], points[i+1])) - if node.get('close', 1, bool, False): - path.append(path[0]) - paths.append((path, color)) + points = node.get_fvec('points', 0, null_) + if points.length >= 2: + m = n = points.length // 2 * 2 + if node.get_bool('close', False): + n += 2 + path = Vector.__new__(Vector) + path.allocate_numbers(n) + for i in range(0, n): + path.numbers[i] = points.numbers[i%m] + paths.append((path, color, transform)) elif node.kind == 'rect': - size = node.get('size', 2, float) - if size is not None: - sx, sy = size - x, y = node.get('point', 2, float, (0, 0)) - path = [transform.t(x, y), transform.t(x+sx, y), transform.t(x+sx, y+sy), transform.t(x, y+sy), transform.t(x, y)] - paths.append((path, color)) + size = node.get_fvec('size', 2, Zero2) + sx, sy = size.numbers[0], size.numbers[1] + if sx and sy: + point = node.get_fvec('point', 2, Zero2) + x, y = point.numbers[0], point.numbers[1] + path = Vector.__new__(Vector) + path.allocate_numbers(10) + path.numbers[0], path.numbers[1] = x, y + path.numbers[2], path.numbers[3] = x+sx, y + path.numbers[4], path.numbers[5] = x+sx, y+sy + path.numbers[6], path.numbers[7] = x, y+sy + path.numbers[8], path.numbers[9] = x, y + paths.append((path, color, transform)) elif node.kind == 'ellipse': - radius = node.get('radius', 2, float) - if radius is not None: - sx, sy = radius - x, y = node.get('point', 2, float, (0, 0)) - n = node.get('segments', 1, int, 60) - path = [] - for i in range(n+1): - th = TWO_PI * i/n - path.append(transform.t(x + sx*cos(th), y + sy*sin(th))) - paths.append((path, color)) + size = node.get_fvec('radius', 2, Zero2) + sx, sy = size.numbers[0], size.numbers[1] + if sx and sy: + point = node.get_fvec('point', 2, Zero2) + x, y = point.numbers[0], point.numbers[1] + n = node.get_int('segments', 60) + if n >= 3: + path = Vector.__new__(Vector) + path.allocate_numbers((n+1)*2) + for i in range(n+1): + th = Tau * i/n + j = i * 2 + path.numbers[j] = x + sx*cos(th) + path.numbers[j+1] = x + sx*sin(th) + paths.append((path, color, transform)) RENDERER_CLASS = Laser