Skip to content

TexnoMagic Drawing

TexnoMagicDrawing

TexnoMagic Drawing is a set of 2D curves defined by points.

Specifically, Drawing is list of lists of 2D points (a list of separated curves) as generated by pointing devices such as mouse, touch, and tablets.

This abstraction is roughly equivalent to using a pen & paper. Individual drawing points indicate the trajectory of a pen/pencil/marker/brush/chalk/etc when it was writing.

Drawing is a usually a representation of a Symbol.

Drawings are stored as CSV files with individual curves separated by empty lines (,).

self.path is a path of Drawing data CSV file.

This class provides convenient utilities for working with Drawings, see individual methods.

Source code in texnomagic/drawing.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
class TexnoMagicDrawing:
    """TexnoMagic Drawing is a set of 2D curves defined by points.

    Specifically, Drawing is list of lists of 2D points (a list of separated
    curves) as generated by pointing devices such as mouse, touch, and tablets.

    This abstraction is roughly equivalent to using a pen & paper.
    Individual drawing points indicate the trajectory of a
    pen/pencil/marker/brush/chalk/etc when it was writing.

    Drawing is a usually a representation of a
    [Symbol][texnomagic.symbol.TexnoMagicSymbol].

    Drawings are stored as CSV files with individual curves separated
    by empty lines (`,`).

    `self.path` is a path of Drawing data CSV file.

    This class provides convenient utilities for working with Drawings,
    see individual methods.
    """

    def __init__(self, path=None, curves=None, points_range=1000.0):
        self.path = path
        self.points_range = points_range
        self._curves = None
        self._points = None
        self._file_size = None
        if curves:
            self.set_curves(curves)

    @property
    def curves(self) -> list[np.array]:
        """Individual curves (lists of points).

        Lazy loaded on-demand."""
        if self._curves is None:
            self.load_curves()
        return self._curves

    @property
    def points(self) -> np.array:
        """All points from all curves.

        Lazy loaded on-demand."""
        if self._points is None:
            self.load_curves()
        return self._points

    @property
    def name(self) -> str | None:
        """Drawing file name.

        Derived from self.path."""
        if self.path:
            return self.path.name
        return None

    @property
    def file_size(self) -> int:
        """Drawing file size.

        Lazy loaded on-demand."""
        if self._file_size is None:
            self._file_size = self.path.stat().st_size
        return self._file_size

    def set_curves(self, curves):
        """Assign curves.

        Converts to a single numpy.array points with curves being views
        into the array for fast processing."""
        # keep all points in single continuous numpy array
        self._points = np.array(list(itertools.chain(*curves)), dtype=np.float64)
        self._curves = []
        i = 0
        for curve in curves:
            n = len(curve)
            # curves are numpy views into main points array
            cview = self._points[i:i+n]
            self._curves.append(cview)
            i += n

    def load(self, path=None):
        # this is only kept for consistence with symbol and abc
        if path:
            self.path = path
        return self

    def load_curves(self):
        """Load Drawing curves from file."""
        curves = []
        curve = []
        with self.path.open('r') as f:
            reader = csv.reader(f)
            for row in reader:
                if not row or '' in row:
                    # empty rows separate individual curves
                    curves.append(curve)
                    curve = []
                    continue
                point = list(map(float, row[:2]))
                curve.append(point)
        curves.append(curve)
        self.set_curves(curves)

    def save(self):
        """Save drawing to CSV file specified by self.path."""
        self.path.parent.mkdir(parents=True, exist_ok=True)
        with self.path.open('w', newline='') as f:
            writer = csv.writer(f)
            first = True
            for curve in self.curves:
                if first:
                    first = False
                else:
                    # curves separator
                    writer.writerow([None, None])
                writer.writerows(curve.tolist())

    def normalize(self):
        """
        Normalize drawing points in-place into <0, self.points_range> range.

        See also [curves_fit_area][texnomagic.drawing.TexnoMagicDrawing.curves_fit_area].
        """
        if len(self.points) == 0:
            return

        # move to [0,0]
        self._points -= np.min(self.points, axis=0)
        # normalize
        k = self.points_range / np.max((np.max(np.max(self._points, axis=0)), 0.2))
        self._points *= k
        # center
        offset = (self.points_range - np.max(self._points, axis=0)) / 2
        self._points += offset

    def curves_fit_area(self, pos : tuple[float, float] | npt.ArrayLike, size : tuple[float, float] | npt.ArrayLike) -> list[npt.ArrayLike]:
        """
        Return curves scaled to fit area.

        Useful for drawing curves in UI

        See also [normalize][texnomagic.drawing.TexnoMagicDrawing.normalize].

        Args:
          pos: position / offset (x, y) - relative <0, 1>
          size: desired size (width, height) - relative <0, 1>

        Returns:
          Curves scaled to fit desired area.
        """
        pos = np.array(pos)
        size = np.array(size)

        k = np.min(size) / self.points_range
        max_range = self.points_range * k

        offset = pos + (size - max_range) / 2

        scurves = []
        for curve in self.curves:
            if len(curve) > 0:
                scurve = curve * k + offset
            else:
                scurve = curve
            scurves.append(scurve)
        return scurves

    def flip_y_axis(self):
        """
        Flip Drawing along Y axis in-place.

        Useful for compatibility with systems that use different Y axis sign.
        """
        self._points[:,1] = self.points_range - self._points[:,1]

    def delete(self):
        """Delete the Drawing file."""
        if not self.path or not self.path.exists():
            return
        self.path.unlink()

    def pretty(self, size=True) -> str:
        """Pretty Drawing string with colors in rich formatting."""
        n_curves = len(self.curves)
        n_points = len(self.points)
        s = f"[white bold]{self.path.name}[/]: "
        s += f"{n_points} points, {n_curves} curves"
        if size:
            fsize = math.ceil(self.file_size / 1024.0)
            s += f", {fsize} kB"
        return s

    def __str__(self) -> str:
        if self._curves is None:
            info = "curves not loaded"
        else:
            info = f"{len(self._points)} points, {len(self._curves)} curves"
        return f"{self.name}: {info}"

    def __repr__(self) -> str:
        return '<TexnoMagicSymbol %s>' % self.__str__()

curves: list[np.array] property

Individual curves (lists of points).

Lazy loaded on-demand.

file_size: int property

Drawing file size.

Lazy loaded on-demand.

name: str | None property

Drawing file name.

Derived from self.path.

points: np.array property

All points from all curves.

Lazy loaded on-demand.

curves_fit_area(pos, size)

Return curves scaled to fit area.

Useful for drawing curves in UI

See also normalize.

Parameters:

Name Type Description Default
pos tuple[float, float] | ArrayLike

position / offset (x, y) - relative <0, 1>

required
size tuple[float, float] | ArrayLike

desired size (width, height) - relative <0, 1>

required

Returns:

Type Description
list[ArrayLike]

Curves scaled to fit desired area.

Source code in texnomagic/drawing.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def curves_fit_area(self, pos : tuple[float, float] | npt.ArrayLike, size : tuple[float, float] | npt.ArrayLike) -> list[npt.ArrayLike]:
    """
    Return curves scaled to fit area.

    Useful for drawing curves in UI

    See also [normalize][texnomagic.drawing.TexnoMagicDrawing.normalize].

    Args:
      pos: position / offset (x, y) - relative <0, 1>
      size: desired size (width, height) - relative <0, 1>

    Returns:
      Curves scaled to fit desired area.
    """
    pos = np.array(pos)
    size = np.array(size)

    k = np.min(size) / self.points_range
    max_range = self.points_range * k

    offset = pos + (size - max_range) / 2

    scurves = []
    for curve in self.curves:
        if len(curve) > 0:
            scurve = curve * k + offset
        else:
            scurve = curve
        scurves.append(scurve)
    return scurves

delete()

Delete the Drawing file.

Source code in texnomagic/drawing.py
186
187
188
189
190
def delete(self):
    """Delete the Drawing file."""
    if not self.path or not self.path.exists():
        return
    self.path.unlink()

flip_y_axis()

Flip Drawing along Y axis in-place.

Useful for compatibility with systems that use different Y axis sign.

Source code in texnomagic/drawing.py
178
179
180
181
182
183
184
def flip_y_axis(self):
    """
    Flip Drawing along Y axis in-place.

    Useful for compatibility with systems that use different Y axis sign.
    """
    self._points[:,1] = self.points_range - self._points[:,1]

load_curves()

Load Drawing curves from file.

Source code in texnomagic/drawing.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def load_curves(self):
    """Load Drawing curves from file."""
    curves = []
    curve = []
    with self.path.open('r') as f:
        reader = csv.reader(f)
        for row in reader:
            if not row or '' in row:
                # empty rows separate individual curves
                curves.append(curve)
                curve = []
                continue
            point = list(map(float, row[:2]))
            curve.append(point)
    curves.append(curve)
    self.set_curves(curves)

normalize()

Normalize drawing points in-place into <0, self.points_range> range.

See also curves_fit_area.

Source code in texnomagic/drawing.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def normalize(self):
    """
    Normalize drawing points in-place into <0, self.points_range> range.

    See also [curves_fit_area][texnomagic.drawing.TexnoMagicDrawing.curves_fit_area].
    """
    if len(self.points) == 0:
        return

    # move to [0,0]
    self._points -= np.min(self.points, axis=0)
    # normalize
    k = self.points_range / np.max((np.max(np.max(self._points, axis=0)), 0.2))
    self._points *= k
    # center
    offset = (self.points_range - np.max(self._points, axis=0)) / 2
    self._points += offset

pretty(size=True)

Pretty Drawing string with colors in rich formatting.

Source code in texnomagic/drawing.py
192
193
194
195
196
197
198
199
200
201
def pretty(self, size=True) -> str:
    """Pretty Drawing string with colors in rich formatting."""
    n_curves = len(self.curves)
    n_points = len(self.points)
    s = f"[white bold]{self.path.name}[/]: "
    s += f"{n_points} points, {n_curves} curves"
    if size:
        fsize = math.ceil(self.file_size / 1024.0)
        s += f", {fsize} kB"
    return s

save()

Save drawing to CSV file specified by self.path.

Source code in texnomagic/drawing.py
114
115
116
117
118
119
120
121
122
123
124
125
126
def save(self):
    """Save drawing to CSV file specified by self.path."""
    self.path.parent.mkdir(parents=True, exist_ok=True)
    with self.path.open('w', newline='') as f:
        writer = csv.writer(f)
        first = True
        for curve in self.curves:
            if first:
                first = False
            else:
                # curves separator
                writer.writerow([None, None])
            writer.writerows(curve.tolist())

set_curves(curves)

Assign curves.

Converts to a single numpy.array points with curves being views into the array for fast processing.

Source code in texnomagic/drawing.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def set_curves(self, curves):
    """Assign curves.

    Converts to a single numpy.array points with curves being views
    into the array for fast processing."""
    # keep all points in single continuous numpy array
    self._points = np.array(list(itertools.chain(*curves)), dtype=np.float64)
    self._curves = []
    i = 0
    for curve in curves:
        n = len(curve)
        # curves are numpy views into main points array
        cview = self._points[i:i+n]
        self._curves.append(cview)
        i += n