def process_cubic(self, offset_path, blade_path, params, quality): """ Add offset correction to a cubic bezier. """ r = self.config.offset p0 = blade_path.currentPosition() p1, p2, p3 = params self.add_continuity_correction(offset_path, blade_path, p1) curve = QPainterPath() curve.moveTo(p0) curve.cubicTo(*params) p = QPainterPath() p.moveTo(p0) if quality == 1: polygon = curve.toSubpathPolygons(IDENITY_MATRIX)[0] else: m = QTransform.fromScale(quality, quality) m_inv = QTransform.fromScale(1/quality, 1/quality) polygon = m_inv.map(curve.toSubpathPolygons(m)[0]) for point in polygon: p.lineTo(point) t = curve.percentAtLength(p.length()) angle = curve.angleAtPercent(t) a = radians(angle) dx, dy = r*cos(a), -r*sin(a) offset_path.lineTo(point.x()+dx, point.y()+dy) blade_path.cubicTo(*params)
def parseTransform(self, e): t = QTransform() # transforms don't apply to the root svg element, but we do need to # take into account the viewBox there if self.isParentSvg: viewBox = e.attrib.get('viewBox', None) if viewBox is not None: (x, y, innerWidth, innerHeight) = map(self.parseUnit, re.split("[ ,]+", viewBox)) if x != 0 or y != 0: raise ValueError( "viewBox '%s' needs to be translated " "because is not at the origin. " "See https://github.com/codelv/inkcut/issues/69" % viewBox) outerWidth, outerHeight = map(self.parseUnit, (e.attrib.get( 'width', None), e.attrib.get('height', None))) if outerWidth is not None and outerHeight is not None: t.scale(outerWidth / innerWidth, outerHeight / innerHeight) else: x, y = map(self.parseUnit, (e.attrib.get('x', 0), e.attrib.get('y', 0))) t.translate(x, y) return t
def create(self, swap_xy=False, scale=None): """ Create a path model that is rotated and scaled """ model = QPainterPath() if not self.path: return path = self._create_copy() # Update size bbox = path.boundingRect() self.size = [bbox.width(), bbox.height()] # Create copies c = 0 points = self._copy_positions_iter(path) if self.auto_copies: self.stack_size = self._compute_stack_sizes(path) if self.stack_size[0]: copies_left = self.copies % self.stack_size[0] if copies_left: # not a full stack with self.events_suppressed(): self.copies = self._desired_copies self.add_stack() while c < self.copies: x, y = next(points) model.addPath(path * QTransform.fromTranslate(x, -y)) c += 1 # Create weedline if self.plot_weedline: self._add_weedline(model, self.plot_weedline_padding) # Determine padding bbox = model.boundingRect() if self.align_center[0]: px = (self.material.width() - bbox.width()) / 2.0 else: px = self.material.padding_left if self.align_center[1]: py = -(self.material.height() - bbox.height()) / 2.0 else: py = -self.material.padding_bottom # Scale and rotate if scale: model *= QTransform.fromScale(*scale) px, py = px * abs(scale[0]), py * abs(scale[1]) if swap_xy: t = QTransform() t.rotate(90) model *= t # Move to 0,0 bbox = model.boundingRect() p = bbox.bottomLeft() tx, ty = -p.x(), -p.y() # If swapped, make sure padding is still correct if swap_xy: px, py = -py, -px tx += px ty += py model = model * QTransform.fromTranslate(tx, ty) end_point = (QPointF(0, -self.feed_after + model.boundingRect().top()) if self.feed_to_end else QPointF(0, 0)) model.moveTo(end_point) return model
def _create_copy(self): """ Creates a copy of the original graphic applying the given transforms """ optimized_path = self.optimized_path bbox = optimized_path.boundingRect() # Create the base copy t = QTransform() t.scale( self.scale[0] * (self.mirror[0] and -1 or 1), self.scale[1] * (self.mirror[1] and -1 or 1), ) # Rotate about center if self.rotation != 0: c = bbox.center() t.translate(-c.x(), -c.y()) t.rotate(self.rotation) t.translate(c.x(), c.y()) # Apply transform path = optimized_path * t # Add weedline to copy if self.copy_weedline: self._add_weedline(path, self.copy_weedline_padding) # If it's too big we have to scale it w, h = path.boundingRect().width(), path.boundingRect().height() available_area = self.material.available_area #: This screws stuff up! if w > available_area.width() or h > available_area.height(): # If it's too big an auto scale is enabled, resize it to fit if self.auto_scale: sx, sy = 1, 1 if w > available_area.width(): sx = available_area.width() / w if h > available_area.height(): sy = available_area.height() / h s = min(sx, sy) # Fit to the smaller of the two path = optimized_path * QTransform.fromScale(s, s) # Move to bottom left p = path.boundingRect().bottomRight() path = path * QTransform.fromTranslate(-p.x(), -p.y()) return path
def arc(self, x1, y1, rx, ry, phi, large_arc_flag, sweep_flag, x2o, y2o): # handle rotated arcs as normal arcs that are transformed as a rotation if phi != 0: x2 = x1 + (x2o - x1) * cos(radians(phi)) + (y2o - y1) * sin( radians(phi)) y2 = y1 - (x2o - x1) * sin(radians(phi)) + (y2o - y1) * cos( radians(phi)) else: x2, y2 = x2o, y2o # https://www.w3.org/TR/SVG/implnote.html F.6.6 rx = abs(rx) ry = abs(ry) # https://www.w3.org/TR/SVG/implnote.html F.6.5 x1prime = (x1 - x2) / 2 y1prime = (y1 - y2) / 2 # https://www.w3.org/TR/SVG/implnote.html F.6.6 lamb = (x1prime * x1prime) / (rx * rx) + (y1prime * y1prime) / (ry * ry) if lamb >= 1: ry = sqrt(lamb) * ry rx = sqrt(lamb) * rx # Back to https://www.w3.org/TR/SVG/implnote.html F.6.5 radicand = (rx * rx * ry * ry - rx * rx * y1prime * y1prime - ry * ry * x1prime * x1prime) radicand /= (rx * rx * y1prime * y1prime + ry * ry * x1prime * x1prime) if radicand < 0: radicand = 0 factor = (-1 if large_arc_flag == sweep_flag else 1) * sqrt(radicand) cxprime = factor * rx * y1prime / ry cyprime = -factor * ry * x1prime / rx cx = cxprime + (x1 + x2) / 2 cy = cyprime + (y1 + y2) / 2 start_theta = -atan2((y1 - cy) * rx, (x1 - cx) * ry) start_phi = -atan2((y1 - cy) / ry, (x1 - cx) / rx) end_phi = -atan2((y2 - cy) / ry, (x2 - cx) / rx) sweep_length = end_phi - start_phi if sweep_length < 0 and not sweep_flag: sweep_length += 2 * pi elif sweep_length > 0 and sweep_flag: sweep_length -= 2 * pi if phi != 0: rotarc = QPainterPath() rotarc.moveTo(x1, y1) rotarc.arcTo(cx - rx, cy - ry, rx * 2, ry * 2, start_theta * 360 / 2 / pi, sweep_length * 360 / 2 / pi) t = QTransform() t.translate(x1, y1) t.rotate(phi) t.translate(-x1, -y1) tmp = rotarc * t rotarc -= rotarc rotarc += tmp self.addPath(rotarc) else: self.arcTo(cx - rx, cy - ry, rx * 2, ry * 2, start_theta * 360 / 2 / pi, sweep_length * 360 / 2 / pi)
def parseTransform(self, e): """ Based on simpletrasnform.py by from Jean-Francois Barraud, [email protected] """ t = QTransform() if isinstance(e, EtreeElement): trans = e.attrib.get('transform', '').strip() else: trans = e # e is a string of the previous transform if not trans: return t m = re.match( "(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?", trans) if m is None: return t name, args = m.group(1), m.group(2).replace(',', ' ').split() if name == "translate": # The translate(<x> [<y>]) transform function moves the object # by x and y. If y is not provided, it is assumed to be 0. dx = float(args[0]) dy = float(args[1]) if len(args) == 2 else 0 t.translate(dx, dy) elif name == "scale": # The scale(<x> [<y>]) transform function specifies a scale # operation by x and y. If y is not provided, it is assumed to # be equal to x. sx = float(args[0]) sy = float(args[1]) if len(args) == 2 else sx t.scale(sx, sy) elif name == "rotate": # The rotate(<a> [<x> <y>]) transform function specifies a # rotation by a degrees about a given point. If optional # parameters x and y are not supplied, the rotation is about the # origin of the current user coordinate system. If optional # parameters x and y are supplied, the rotation is about the # point (x, y). if len(args) == 1: cx, cy = (0, 0) else: cx, cy = map(float, args[1:]) t.translate(cx, cy) t.rotate(float(args[0])) t.translate(-cx, -cy) elif name == "skewX": # The skewX(<a>) transform function specifies a skew transformation # along the x axis by a degrees. t.shear(math.tan(float(args[0]) * math.pi / 180.0), 0) elif name == "skewY": # The skewY(<a>) transform function specifies a skew transformation # along the y axis by a degrees. t.shear(0, math.tan(float(args[0]) * math.pi / 180.0)) elif name == "matrix": t = t * QTransform(*map(float, args)) if m.end() < len(trans): t = self.parseTransform(trans[m.end():]) * t return t
from atom.api import Float, Enum, Instance from enaml.qt.QtCore import QPointF from enaml.qt.QtGui import QPainterPath, QTransform, QVector2D from inkcut.device.plugin import DeviceFilter, Model from inkcut.core.utils import unit_conversions, log # Element types MoveToElement = QPainterPath.MoveToElement LineToElement = QPainterPath.LineToElement CurveToElement = QPainterPath.CurveToElement CurveToDataElement = QPainterPath.CurveToDataElement IDENITY_MATRIX = QTransform.fromScale(1, 1) def fp(point): return "({}, {})".format(round(point.x(), 3), round(point.y(), 3)) class BladeOffsetConfig(Model): #: BladeOffset in user units offset = Float(strict=False).tag(config=True) #: Units for display offset_units = Enum(*unit_conversions.keys()).tag(config=True) #: If the angle is less than this, consider it continuous cutoff = Float(5.0, strict=False).tag(config=True)
Distributed under the terms of the GPL v3 License. The full license is in the file LICENSE, distributed with this software. Created on Dec 14, 2018 @author: jrm """ from math import sqrt, cos, radians, isinf from atom.api import Float, Enum, Instance from inkcut.device.plugin import DeviceFilter, Model from inkcut.core.utils import unit_conversions from enaml.qt.QtGui import QPainterPath, QTransform, QVector2D IDENITY_MATRIX = QTransform.fromScale(1, 1) class BladeOffsetConfig(Model): #: BladeOffset in user units offset = Float(strict=False).tag(config=True) #: Units for display offset_units = Enum(*unit_conversions.keys()).tag(config=True) #: Cutoff angle cutoff = Float(20.0, strict=False).tag(config=True) def _default_offset_units(self): return 'mm'