tuhi/tuhi/drawing.py

170 lines
4.9 KiB
Python

#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
from gi.repository import GObject
import json
import logging
logger = logging.getLogger('tuhi.drawing')
class Point(GObject.Object):
def __init__(self, stroke):
GObject.Object.__init__(self)
self.stroke = stroke
self.position = None
self.pressure = None
def to_dict(self):
d = {}
for key in ['position', 'pressure']:
val = getattr(self, key, None)
if val is not None:
d[key] = val
return d
class Stroke(GObject.Object):
def __init__(self, drawing):
GObject.Object.__init__(self)
self.drawing = drawing
self.points = []
self._position = (0, 0)
self._pressure = 0
self._is_sealed = False
@GObject.Property
def sealed(self):
return self._is_sealed
def seal(self):
self._is_sealed = True
def new_rel(self, position=None, pressure=None):
assert not self._is_sealed
p = Point(self)
if position is not None:
x, y = self._position
self._position = (x + position[0], y + position[1])
p.position = self._position
if pressure is not None:
self._pressure += pressure
p.pressure = self._pressure
self.points.append(p)
def new_abs(self, position=None, pressure=None):
assert not self._is_sealed
p = Point(self)
if position is not None:
self._position = position
p.position = position
if pressure is not None:
self._pressure = pressure
p.pressure = pressure
self.points.append(p)
def to_dict(self):
d = {}
d['points'] = [p.to_dict() for p in self.points]
return d
class Drawing(GObject.Object):
'''
Abstracts a drawing. The drawing is composed Strokes, each of which has
Points.
'''
JSON_FILE_FORMAT_VERSION = 1
def __init__(self, name, dimensions, timestamp):
GObject.Object.__init__(self)
self.name = name
self.dimensions = dimensions
self.timestamp = timestamp # unix seconds
self.strokes = []
self._current_stroke = -1
self.session_id = 'unset'
def seal(self):
# Drop empty strokes
for s in self.strokes:
s.seal()
self.strokes = [s for s in self.strokes if s.points]
# The way we're building drawings, we don't need to change the current
# stroke at runtime, so this is read-ony
@GObject.Property
def current_stroke(self):
if self._current_stroke < 0:
return None
s = self.strokes[self._current_stroke]
return s if not s.sealed else None
def new_stroke(self):
'''
Create a new stroke and make it the current stroke
'''
if self.current_stroke is not None:
self.current_stroke.seal()
s = Stroke(self)
self.strokes.append(s)
self._current_stroke += 1
return s
def to_json(self):
json_data = {
'version': self.JSON_FILE_FORMAT_VERSION,
'devicename': self.name,
'sessionid': self.session_id,
'dimensions': list(self.dimensions),
'timestamp': self.timestamp,
'strokes': [s.to_dict() for s in self.strokes]
}
return json.dumps(json_data, indent=2)
@classmethod
def from_json(cls, path):
d = None
with open(path, 'r') as fp:
json_data = json.load(fp)
try:
if json_data['version'] != cls.JSON_FILE_FORMAT_VERSION:
logger.error(f'{path}: Invalid file format version')
return d
name = json_data['devicename']
dimensions = tuple(json_data['dimensions'])
timestamp = json_data['timestamp']
d = Drawing(name, dimensions, timestamp)
for s in json_data['strokes']:
stroke = d.new_stroke()
for p in s['points']:
position = p.get('position', None)
pressure = p.get('pressure', None)
stroke.new_abs(position, pressure)
except KeyError:
logger.error(f'{path}: failed to parse json file')
return d
def __repr__(self):
return f'Drawing from {self.name} at {self.timestamp}, {len(self.strokes)} strokes'