def main(): parser = argparse.ArgumentParser(description="Build Mada slanted fonts.") parser.add_argument("file", metavar="FILE", help="input font to process") parser.add_argument("outfile", metavar="FILE", help="output font to write") parser.add_argument("angle", metavar="FILE", help="slant angle", type=float) args = parser.parse_args() matrix = Identity.skew(math.radians(-args.angle)) font = Font(args.file) info = font.info if args.angle < 0: style = "Italic" else: style = "Slanted" info.styleName += " " + style info.italicAngle = info.postscriptSlantAngle = args.angle for glyph in font: for contour in glyph: for point in contour: point.x, point.y = matrix.transformPoint((point.x, point.y)) for anchor in glyph.anchors: anchor.x, anchor.y = matrix.transformPoint((anchor.x, anchor.y)) font.save(args.outfile)
def main(): parser = argparse.ArgumentParser(description="Build Mada slanted fonts.") parser.add_argument("file", metavar="FILE", help="input font to process") parser.add_argument("outfile", metavar="FILE", help="output font to write") parser.add_argument("angle", metavar="FILE", help="slant angle", type=float) args = parser.parse_args() matrix = Identity.skew(math.radians(-args.angle)) font = Font(args.file) info = font.info if args.angle < 0: style = "Italic" else: style = "Slanted" info.styleName += " " + style info.openTypeNamePreferredSubfamilyName += " " + style info.postscriptFullName += " " + style info.postscriptFontName += style info.italicAngle = info.postscriptSlantAngle = args.angle for glyph in font: for contour in glyph: for point in contour: point.x, point.y = matrix.transformPoint((point.x, point.y)) for anchor in glyph.anchors: anchor.x, anchor.y = matrix.transformPoint((anchor.x, anchor.y)) font.save(args.outfile)
def scale(self, pt, center=(0, 0)): dx, dy = center x, y = pt sT = Identity.translate(dx, dy) sT = sT.scale(x, y) sT = sT.translate(-dx, -dy) self.transform(sT)
def rotate(self, angle, offset=(0, 0)): dx, dy = offset radAngle = math.radians(angle) rT = Identity.translate(dx, dy) rT = rT.rotate(radAngle) rT = rT.translate(-dx, -dy) self.transform(rT)
def deepAppendGlyph(self, glyph, gToAppend, offset=(0,0)): if not gToAppend.components: glyph.appendGlyph(gToAppend, offset) else: for component in gToAppend.components: compGlyph = self.font[component.baseGlyph].copy() # handle component transformations componentTransformation = component.transformation # when undoing a paste anchor or a delete anchor action, RoboFont returns component.transformation as a list instead of a tuple if type(componentTransformation) is list: componentTransformation = tuple(componentTransformation) if componentTransformation != (1, 0, 0, 1, 0, 0): # if component is skewed and/or is shifted matrix = componentTransformation[0:4] if matrix != (1, 0, 0, 1): # if component is skewed transformObj = Identity.transform(matrix + (0, 0)) # ignore the original component's shifting values compGlyph.transform(transformObj) glyph.appendGlyph(compGlyph, map(sum, zip(component.offset, offset))) # add the two tuples of offset for contour in gToAppend: glyph.appendContour(contour, offset) # if the assembled glyph still has components, recursively remove and replace them 1-by-1 by the glyphs they reference if glyph.components: nestedComponent = glyph.components[-1] # start from the end glyph.removeComponent(nestedComponent) glyph = self.deepAppendGlyph(glyph, self.font[nestedComponent.baseGlyph], nestedComponent.offset) return glyph
def _deepAppendGlyph(self, glyph, gToAppend, font, offset=(0, 0)): if not gToAppend.components: glyph.appendGlyph(gToAppend, offset) else: for component in gToAppend.components: if component.baseGlyph not in font.keys(): # avoid traceback in the case where the selected glyph # is referencing a component whose glyph is not in the font continue compGlyph = font[component.baseGlyph].copy() if component.transformation != (1, 0, 0, 1, 0, 0): # if component is skewed and/or is shifted: matrix = component.transformation[0:4] if matrix != (1, 0, 0, 1): # if component is skewed transformObj = Identity.transform(matrix + (0, 0)) # ignore the original component's shifting values compGlyph.transform(transformObj) # add the two tuples of offset: totalOffset = tuple(map(sum, zip(component.offset, offset))) glyph.appendGlyph(compGlyph, totalOffset) for contour in gToAppend: glyph.appendContour(contour, offset) # if the assembled glyph still has components, recursively # remove and replace them 1-by-1 by the glyphs they reference: if glyph.components: nestedComponent = glyph.components[-1] glyph.removeComponent(nestedComponent) glyph = self._deepAppendGlyph(glyph, font[nestedComponent.baseGlyph], font, nestedComponent.offset) return glyph
def write_glyph(dir, font, glyph, master_id): if glyph not in font.glyphs: print("Unknown glyph %s" % glyph) return print(glyph) if master_id is None: master_id = 0 master = font.masters[master_id] layer = font.glyphs[glyph].layers[master.id] svg = etree.Element( "svg", width=str(layer.width), height=str(master.ascender - master.descender), xmlns="http://www.w3.org/2000/svg", ) et = etree.ElementTree(svg) t = Identity.scale(1, -1).translate(0, -master.ascender) commands = add_paths(layer, t) # Components for c in layer.components: comp_layer = font.glyphs[c.name].layers[master.id] commands += add_paths(comp_layer, t, c.transform) svg_path = etree.fromstring(f'<path d="{commands}" />') svg.append(svg_path) et.write("%s/%s.svg" % (dir, glyph), pretty_print=True)
def skew(self, angle, offset=(0, 0)): dx, dy = offset x, y = angle x, y = math.radians(x), math.radians(y) sT = Identity.translate(dx, dy) sT = sT.skew(x, y) sT = sT.translate(-dx, -dy) self.transform(sT)
def _alignment_transformation(segment): # Returns a transformation which aligns a segment horizontally at the # origin. Apply this transformation to curves and root-find to find # intersections with the segment. start = segment[0] end = segment[-1] angle = math.atan2(end[1] - start[1], end[0] - start[0]) return Identity.rotate(-angle).translate(-start[0], -start[1])
def trianglePath(x, y, size, angle): thirdSize = size / 3 pen = QtPen({}) tPen = TransformPen(pen, Identity.rotate(angle)) tPen.moveTo((-thirdSize, size)) tPen.lineTo((-thirdSize, -size)) tPen.lineTo((2 * thirdSize, 0)) tPen.closePath() return pen.path.translated(x, y)
def trianglePath(x, y, size, angle): thirdSize = size / 3 pen = QtPen({}) tPen = TransformPen( pen, Identity.rotate(angle)) tPen.moveTo((-thirdSize, size)) tPen.lineTo((-thirdSize, -size)) tPen.lineTo((2 * thirdSize, 0)) tPen.closePath() return pen.path.translated(x, y)
def deepAppendGlyph(self, glyph, gToAppend, offset=(0, 0)): if not gToAppend.components: glyph.appendGlyph(gToAppend, offset) else: for component in gToAppend.components: # avoid traceback in the case where the selected glyph is # referencing a component whose glyph is not in the font if component.baseGlyph not in self.font.keys(): print("WARNING: %s is referencing a glyph named %s, which " "does not exist in the font." % (self.font.selection[0], component.baseGlyph)) continue compGlyph = self.font[component.baseGlyph].copy() # handle component transformations componentTransformation = component.transformation # when undoing a paste anchor or a delete anchor action, # RoboFont returns component.transformation as a list instead # of a tuple if type(componentTransformation) is list: componentTransformation = tuple(componentTransformation) # if component is skewed and/or is shifted if componentTransformation != (1, 0, 0, 1, 0, 0): matrix = componentTransformation[0:4] if matrix != (1, 0, 0, 1): # if component is skewed # ignore the original component's shifting values if self.rf3: compGlyph.transform(matrix + (0, 0)) else: transformObj = Identity.transform(matrix + (0, 0)) compGlyph.transform(transformObj) # add the two tuples of offset glyph.appendGlyph( compGlyph, tuple(map(sum, zip(component.offset, offset)))) for contour in gToAppend: glyph.appendContour(contour, offset) # if the assembled glyph still has components, recursively # remove and replace them 1-by-1 by the glyphs they reference if glyph.components: nestedComponent = glyph.components[-1] # start from the end glyph.removeComponent(nestedComponent) glyph = self.deepAppendGlyph(glyph, self.font[nestedComponent.baseGlyph], nestedComponent.offset) return glyph
def _decompose_to_cubic_curves(self): if self.center_point is None and not self._parametrize(): return point_transform = Identity.rotate(self.angle).scale(self.rx, self.ry) # Some results of atan2 on some platform implementations are not exact # enough. So that we get more cubic curves than expected here. Adding 0.001f # reduces the count of sgements to the correct count. num_segments = int(ceil(fabs(self.theta_arc / (PI_OVER_TWO + 0.001)))) for i in range(num_segments): start_theta = self.theta1 + i * self.theta_arc / num_segments end_theta = self.theta1 + (i + 1) * self.theta_arc / num_segments t = (4 / 3) * tan(0.25 * (end_theta - start_theta)) if not isfinite(t): return sin_start_theta = sin(start_theta) cos_start_theta = cos(start_theta) sin_end_theta = sin(end_theta) cos_end_theta = cos(end_theta) point1 = complex( cos_start_theta - t * sin_start_theta, sin_start_theta + t * cos_start_theta, ) point1 += self.center_point target_point = complex(cos_end_theta, sin_end_theta) target_point += self.center_point point2 = target_point point2 += complex(t * sin_end_theta, -t * cos_end_theta) point1 = _map_point(point_transform, point1) point2 = _map_point(point_transform, point2) target_point = _map_point(point_transform, target_point) yield point1, point2, target_point
def main(args=None): parser = argparse.ArgumentParser( description="Transform X anchor positions in VOLT/VTP files.") parser.add_argument("input", metavar="INPUT", help="input font/VTP file to process") parser.add_argument("output", metavar="OUTPUT", help="output font/VTP file") parser.add_argument("-a", "--angle", type=float, required=True, help="the slant angle (in degrees)") options = parser.parse_args(args) font = None try: font = TTFont(options.input) if "TSIV" in font: indata = font["TSIV"].data.decode("utf-8") else: log.error('"TSIV" table is missing, font was not saved from VOLT?') return 1 except TTLibError: with open(options.input) as f: indata = f.read() transform = Identity.skew(options.angle * math.pi / 180) outdata = anchor_re.sub(lambda m: replace(m, transform), indata) if font is not None: font["TSIV"].data = outdata.encode("utf-8") font.save(options.output) else: with open(options.output, "w") as f: f.write(outdata)
def _deepAppendGlyph(self, glyph, gToAppend, font, offset=(0, 0)): if not gToAppend.components: glyph.appendGlyph(gToAppend, offset) else: for component in gToAppend.components: if component.baseGlyph not in font.keys(): # avoid traceback in the case where the selected glyph # is referencing a component whose glyph is not in the font continue compGlyph = font[component.baseGlyph].copy() if component.transformation != (1, 0, 0, 1, 0, 0): # if component is skewed and/or is shifted: matrix = component.transformation[0:4] if matrix != (1, 0, 0, 1): # if component is skewed transformObj = Identity.transform(matrix + (0, 0)) # ignore the original component's shifting values compGlyph.transform(transformObj) # add the two tuples of offset: totalOffset = map(sum, zip(component.offset, offset)) glyph.appendGlyph(compGlyph, totalOffset) for contour in gToAppend: glyph.appendContour(contour, offset) # if the assembled glyph still has components, recursively # remove and replace them 1-by-1 by the glyphs they reference: if glyph.components: nestedComponent = glyph.components[-1] glyph.removeComponent(nestedComponent) glyph = self._deepAppendGlyph(glyph, font[nestedComponent.baseGlyph], font, nestedComponent.offset) return glyph
def drawGlyphPoints( painter, glyph, scale, drawStartPoints=True, drawOnCurves=True, drawOffCurves=True, drawCoordinates=False, drawBluesMarkers=True, onCurveColor=None, onCurveSmoothColor=None, offCurveColor=None, otherColor=None, backgroundColor=None): """ Draws a Glyph_ *glyph*’s points. .. _Glyph: http://ts-defcon.readthedocs.org/en/ufo3/objects/glyph.html """ if onCurveColor is None: onCurveColor = defaultColor("glyphOnCurvePoints") if onCurveSmoothColor is None: onCurveSmoothColor = defaultColor("glyphOnCurveSmoothPoints") if offCurveColor is None: offCurveColor = defaultColor("glyphOffCurvePoints") if otherColor is None: otherColor = defaultColor("glyphOtherPoints") if backgroundColor is None: backgroundColor = defaultColor("background") bluesMarkerColor = defaultColor("glyphBluesMarker") notchColor = defaultColor("glyphContourStroke").lighter(200) # get the outline data outlineData = glyph.getRepresentation("defconQt.OutlineInformation") points = [] # blue zones markers if drawBluesMarkers and drawOnCurves: font = glyph.font blues = [] if font.info.postscriptBlueValues: blues += font.info.postscriptBlueValues if font.info.postscriptOtherBlues: blues += font.info.postscriptOtherBlues if blues: blues_ = set(blues) size = 13 * scale snapSize = 17 * scale painter.save() pen = painter.pen() pen.setColor(QColor(255, 255, 255, 125)) pen.setWidth(0) painter.setPen(pen) for point in outlineData["onCurvePoints"]: x, y = point["point"] # TODO: we could add a non-overlapping interval tree special # cased for borders for yMin, yMax in zip(blues[::2], blues[1::2]): if not (y >= yMin and y <= yMax): continue # if yMin > 0 and y == yMin or yMin <= 0 and y == yMax: if y in blues_: path = lozengePath(x, y, snapSize) else: path = ellipsePath(x, y, size) painter.fillPath(path, bluesMarkerColor) painter.drawPath(path) painter.restore() # handles if drawOffCurves and outlineData["offCurvePoints"]: painter.save() painter.setPen(otherColor) for x1, y1, x2, y2 in outlineData["bezierHandles"]: drawLine(painter, x1, y1, x2, y2) painter.restore() # on curve if drawOnCurves and outlineData["onCurvePoints"]: size = 6.5 * scale smoothSize = 8 * scale startSize = 7 * scale loneStartSize = 12 * scale painter.save() notchPath = QPainterPath() path = QPainterPath() smoothPath = QPainterPath() for point in outlineData["onCurvePoints"]: x, y = point["point"] points.append((x, y)) # notch if "smoothAngle" in point: angle = point["smoothAngle"] t = Identity.rotate(angle) x1, y1 = t.transformPoint((-1.35 * scale, 0)) x2, y2 = -x1, -y1 notchPath.moveTo(x1 + x, y1 + y) notchPath.lineTo(x2 + x, y2 + y) # points if drawStartPoints and "startPointAngle" in point: angle = point["startPointAngle"] if angle is not None: pointPath = trianglePath(x, y, startSize, angle) else: pointPath = ellipsePath(x, y, loneStartSize) elif point["smooth"]: pointPath = ellipsePath(x, y, smoothSize) else: pointPath = rectanglePath(x, y, size) # store the path if point["smooth"]: smoothPath.addPath(pointPath) else: path.addPath(pointPath) # stroke pen = QPen(onCurveColor) pen.setWidthF(1.2 * scale) painter.setPen(pen) painter.drawPath(path) pen.setColor(onCurveSmoothColor) painter.setPen(pen) painter.drawPath(smoothPath) # notch pen.setColor(notchColor) pen.setWidth(0) painter.setPen(pen) painter.drawPath(notchPath) painter.restore() # off curve if drawOffCurves and outlineData["offCurvePoints"]: # points offSize = 4.25 * scale path = QPainterPath() for point in outlineData["offCurvePoints"]: x, y = point["point"] pointPath = ellipsePath(x, y, offSize) path.addPath(pointPath) pen = QPen(offCurveColor) pen.setWidthF(2.5 * scale) painter.save() painter.setPen(pen) painter.drawPath(path) painter.fillPath(path, QBrush(backgroundColor)) painter.restore() # coordinates if drawCoordinates: painter.save() painter.setPen(otherColor) font = painter.font() font.setPointSize(7) painter.setFont(font) for x, y in points: posX = x # TODO: We use + here because we align on top. Consider abstracting # yOffset. posY = y + 6 * scale x = round(x, 1) if int(x) == x: x = int(x) y = round(y, 1) if int(y) == y: y = int(y) text = "%d %d" % (x, y) drawTextAtPoint(painter, text, posX, posY, scale, xAlign="center", yAlign="top") painter.restore()
def _parametrize(self): # convert from endopoint to center parametrization: # https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter # If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a # "lineto") joining the endpoints. # http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters rx = fabs(self.rx) ry = fabs(self.ry) if not (rx and ry): return False # If the current point and target point for the arc are identical, it should # be treated as a zero length path. This ensures continuity in animations. if self.target_point == self.current_point: return False mid_point_distance = (self.current_point - self.target_point) * 0.5 point_transform = Identity.rotate(-self.angle) transformed_mid_point = _map_point(point_transform, mid_point_distance) square_rx = rx * rx square_ry = ry * ry square_x = transformed_mid_point.real * transformed_mid_point.real square_y = transformed_mid_point.imag * transformed_mid_point.imag # Check if the radii are big enough to draw the arc, scale radii if not. # http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii radii_scale = square_x / square_rx + square_y / square_ry if radii_scale > 1: rx *= sqrt(radii_scale) ry *= sqrt(radii_scale) self.rx, self.ry = rx, ry point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle) point1 = _map_point(point_transform, self.current_point) point2 = _map_point(point_transform, self.target_point) delta = point2 - point1 d = delta.real * delta.real + delta.imag * delta.imag scale_factor_squared = max(1 / d - 0.25, 0.0) scale_factor = sqrt(scale_factor_squared) if self.sweep == self.large: scale_factor = -scale_factor delta *= scale_factor center_point = (point1 + point2) * 0.5 center_point += complex(-delta.imag, delta.real) point1 -= center_point point2 -= center_point theta1 = atan2(point1.imag, point1.real) theta2 = atan2(point2.imag, point2.real) theta_arc = theta2 - theta1 if theta_arc < 0 and self.sweep: theta_arc += TWO_PI elif theta_arc > 0 and not self.sweep: theta_arc -= TWO_PI self.theta1 = theta1 self.theta2 = theta1 + theta_arc self.theta_arc = theta_arc self.center_point = center_point return True
def skew(self, angle, offset=(0, 0)): xRad = math.radians(angle[0]) yRad = math.radians(angle[1]) rT = Identity.translate(offset[0], offset[1]) rT = rT.skew(xRad, yRad) self.transform(rT)
def drawGlyphPoints(painter, glyph, scale, drawStartPoints=True, drawOnCurves=True, drawOffCurves=True, drawCoordinates=False, drawSelection=True, drawBluesMarkers=True, onCurveColor=None, onCurveSmoothColor=None, offCurveColor=None, otherColor=None, backgroundColor=None): if onCurveColor is None: onCurveColor = defaultColor("glyphOnCurvePoints") if onCurveSmoothColor is None: onCurveSmoothColor = defaultColor("glyphOnCurveSmoothPoints") if offCurveColor is None: offCurveColor = defaultColor("glyphOffCurvePoints") if otherColor is None: otherColor = defaultColor("glyphOtherPoints") if backgroundColor is None: backgroundColor = defaultColor("background") bluesMarkerColor = defaultColor("glyphBluesMarker") notchColor = defaultColor("glyphContourStroke").lighter(200) # get the outline data outlineData = glyph.getRepresentation("defconQt.OutlineInformation") points = [] # blue zones markers if drawBluesMarkers and drawOnCurves: font = glyph.font blues = [] if font.info.postscriptBlueValues: blues += font.info.postscriptBlueValues if font.info.postscriptOtherBlues: blues += font.info.postscriptOtherBlues if blues: blues_ = set(blues) size = 13 * scale selectedSize = 15 * scale snapSize = 17 * scale selectedSnapSize = 20 * scale painter.save() pen = painter.pen() pen.setColor(QColor(255, 255, 255, 125)) pen.setWidth(0) painter.setPen(pen) for point in outlineData["onCurvePoints"]: x, y = point["point"] # TODO: we could add a non-overlapping interval tree special # cased for borders selected = drawSelection and point.get("selected", False) if selected: size_ = selectedSize snapSize_ = selectedSnapSize else: size_ = size snapSize_ = snapSize for yMin, yMax in zip(blues[::2], blues[1::2]): if not (y >= yMin and y <= yMax): continue # if yMin > 0 and y == yMin or yMin <= 0 and y == yMax: if y in blues_: path = lozengePath(x, y, snapSize_) else: path = ellipsePath(x, y, size_) painter.fillPath(path, bluesMarkerColor) painter.drawPath(path) painter.restore() # handles if drawOffCurves and outlineData["offCurvePoints"]: painter.save() painter.setPen(otherColor) for x1, y1, x2, y2 in outlineData["bezierHandles"]: drawLine(painter, x1, y1, x2, y2) painter.restore() # on curve if drawOnCurves and outlineData["onCurvePoints"]: size = 6.5 * scale selectedSize = 8.5 * scale smoothSize = 8 * scale selectedSmoothSize = 10 * scale startSize = 7 * scale selectedStartSize = 9 * scale loneStartSize = 12 * scale selectedLoneStartSize = 14 * scale painter.save() notchPath = QPainterPath() paths = (QPainterPath(), QPainterPath()) smoothPaths = (QPainterPath(), QPainterPath()) for point in outlineData["onCurvePoints"]: x, y = point["point"] points.append((x, y)) # notch if "smoothAngle" in point: angle = point["smoothAngle"] t = Identity.rotate(angle) x1, y1 = t.transformPoint((-1.35 * scale, 0)) x2, y2 = -x1, -y1 x1 += x y1 += y x2 += x y2 += y notchPath.moveTo(x1, y1) notchPath.lineTo(x2, y2) # points selected = drawSelection and point.get("selected", False) if selected: size_ = selectedSize smoothSize_ = selectedSmoothSize startSize_ = selectedStartSize loneStartSize_ = selectedLoneStartSize else: size_ = size smoothSize_ = smoothSize startSize_ = startSize loneStartSize_ = loneStartSize if drawStartPoints and "startPointAngle" in point: angle = point["startPointAngle"] if angle is not None: pointPath = trianglePath(x, y, startSize_, angle) else: pointPath = ellipsePath(x, y, loneStartSize_) elif point["smooth"]: pointPath = ellipsePath(x, y, smoothSize_) else: pointPath = rectanglePath(x, y, size_) # store the path if point["smooth"]: smoothPaths[selected].addPath(pointPath) else: paths[selected].addPath(pointPath) path, selectedPath = paths smoothPath, selectedSmoothPath = smoothPaths # fill selectedPath.setFillRule(Qt.WindingFill) selectedSmoothPath.setFillRule(Qt.WindingFill) painter.fillPath(selectedPath, onCurveColor) painter.fillPath(selectedSmoothPath, onCurveSmoothColor) # stroke pen = QPen(onCurveColor) pen.setWidthF(1.2 * scale) painter.setPen(pen) painter.drawPath(path) pen.setColor(onCurveSmoothColor) painter.setPen(pen) painter.drawPath(smoothPath) # notch pen.setColor(notchColor) pen.setWidth(0) painter.setPen(pen) painter.drawPath(notchPath) painter.restore() # off curve if drawOffCurves and outlineData["offCurvePoints"]: # points offSize = 4.25 * scale selectedOffSize = 6.75 * scale path = QPainterPath() selectedPath = QPainterPath() selectedPath.setFillRule(Qt.WindingFill) for point in outlineData["offCurvePoints"]: x, y = point["point"] selected = drawSelection and point.get("selected", False) if selected: offSize_ = selectedOffSize else: offSize_ = offSize pointPath = ellipsePath(x, y, offSize_) if selected: selectedPath.addPath(pointPath) else: path.addPath(pointPath) pen = QPen(offCurveColor) pen.setWidthF(2.5 * scale) painter.save() painter.setPen(pen) painter.drawPath(path) painter.fillPath(path, QBrush(backgroundColor)) painter.fillPath(selectedPath, QBrush(offCurveColor.lighter(135))) painter.restore() # coordinates if drawCoordinates: painter.save() painter.setPen(otherColor) font = painter.font() font.setPointSize(7) painter.setFont(font) for x, y in points: posX = x # TODO: We use + here because we align on top. Consider abstracting # yOffset. posY = y + 6 * scale x = round(x, 1) if int(x) == x: x = int(x) y = round(y, 1) if int(y) == y: y = int(y) text = "%d %d" % (x, y) drawTextAtPoint(painter, text, posX, posY, scale, xAlign="center", yAlign="top") painter.restore()
def rotate(self, angle, offset=(0, 0)): radAngle = math.radians(angle) rT = Identity.translate(offset[0], offset[1]) rT = rT.rotate(radAngle) rT = rT.translate(-offset[0], -offset[1]) self.transform(rT)