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 add_continuity_correction(self, offset_path, blade_path, point): """ Adds if the upcoming angle and previous angle are not the same we need to correct for that difference by "arcing back" about the current blade point with a radius equal to the offset. """ # Current blade position cur = blade_path.currentPosition() # Determine direction of next move sp = QPainterPath() sp.moveTo(cur) sp.lineTo(point) next_angle = sp.angleAtPercent(1) # Direction of last move angle = blade_path.angleAtPercent(1) # If not continuous it needs corrected with an arc if isnan(angle) or isnan(next_angle): return if abs(angle - next_angle) > self.config.cutoff: r = self.config.offset a = radians(next_angle) dx, dy = r*cos(a), -r*sin(a) po = QPointF(cur.x()+dx, cur.y()+dy) c = offset_path.currentPosition() dx, dy = po.x()-cur.x()+c.x()-cur.x(), po.y()-cur.y()+c.y()-cur.y() c1 = QPointF(cur.x()+dx, cur.y()+dy) offset_path.quadTo(c1, po)
def move_path(self): """ Returns the path the head moves when not cutting """ # Compute the negative path = QPainterPath() for i in range(self.model.elementCount()): e = self.model.elementAt(i) if e.isMoveTo(): path.lineTo(e.x, e.y) else: path.moveTo(e.x, e.y) return path
def split_painter_path(path): """ Split a QPainterPath into subpaths. """ if not isinstance(path, QPainterPath): raise TypeError("path must be a QPainterPath, got: {}".format(path)) # Element types MoveToElement = QPainterPath.MoveToElement LineToElement = QPainterPath.LineToElement CurveToElement = QPainterPath.CurveToElement CurveToDataElement = QPainterPath.CurveToDataElement subpaths = [] params = [] e = None def finish_curve(p, params): if len(params) == 2: p.quadTo(*params) elif len(params) == 3: p.cubicTo(*params) else: raise ValueError("Invalid curve parameters: {}".format(params)) for i in range(path.elementCount()): e = path.elementAt(i) # Finish the previous curve (if there was one) if params and e.type != CurveToDataElement: finish_curve(p, params) params = [] # Reconstruct the path if e.type == MoveToElement: p = QPainterPath() p.moveTo(e.x, e.y) subpaths.append(p) elif e.type == LineToElement: p.lineTo(e.x, e.y) elif e.type == CurveToElement: params = [QPointF(e.x, e.y)] elif e.type == CurveToDataElement: params.append(QPointF(e.x, e.y)) # Finish the previous curve (if there was one) if params and e and e.type != CurveToDataElement: finish_curve(p, params) return subpaths
def apply_blade_offset(self, poly, offset): """ Apply blade offset to the given polygon by appending a quadratic bezier to each point . """ # Use a QPainterPath to track the distance in c++ path = QPainterPath() cutoff = cos(radians(self.config.cutoff)) # Forget last = None n = len(poly) for i, p in enumerate(poly): if i == 0: path.moveTo(p) last_path = QPainterPath() last_path.moveTo(p) last = p continue # Move to the point path.lineTo(p) if i+1 == n: # Done break # Get next point next = poly.at(i+1) # Make our paths last_path.lineTo(p) next_path = QPainterPath() next_path.moveTo(p) next_path.lineTo(next) # Get angle between the two components u, v = QVector2D(last-p), QVector2D(next-p) cos_theta = QVector2D.dotProduct(u.normalized(), v.normalized()) # If the angle is large enough to need compensation if (cos_theta < cutoff and last_path.length() > offset and next_path.length() > offset): # Calculate the extended point t = last_path.percentAtLength(offset) c1 = p+(last_path.pointAtPercent(t)-last) c2 = p t = next_path.percentAtLength(offset) ep = next_path.pointAtPercent(t) if offset > 2: # Can smooth it for larger offsets path.cubicTo(c1, c2, ep) else: # This works for small offsets < 0.5 mm path.lineTo(c1) path.lineTo(ep) # Update last last_path = next_path last = p return path.toSubpathPolygons(IDENITY_MATRIX)
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 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)