def __init__( self, parent, mirror_x: bool = True, width: int = 800, height: int = 600, style: int = wx.NO_BORDER, ): """ Args: parent: wx parent. mirror_x: Flip image on its x axis. width: width of this panel. height: height of this panel. style: a style. """ wx.Panel.__init__(self, parent, size=(width, height), style=style) self.width = None self.height = None self.buffer = wx.NullBitmap self._mirror_x = mirror_x self._matrix = wx.AffineMatrix2D() self._buffered_dc = wx.BufferedDC(wx.ClientDC(self), wx.NullBitmap, wx.BUFFER_VIRTUAL_AREA)
def roundrect_pts(cx, cy, w, h, r, rot, tm): """Compute the scaled/translated/rotated endpoints for a rounded rectangle.""" # Build a rounded rectangle from two overlapping rectangles with # four circles to cover the corners. # Overlap two rectangles to create a larger rectangle with the corners cut out. rects = [] rects.append(FootprintPainter.rect_pts(cx, cy, w, h - 2 * r, rot, tm)) rects.append(FootprintPainter.rect_pts(cx, cy, w - 2 * r, h, rot, tm)) # Add four circles to fill-in corners. circles = [] # Matrix for rotating circle centers and translating by pad center. tmp_tm = wx.AffineMatrix2D() tmp_tm.Translate(cx, cy) tmp_tm.Rotate(rot * math.pi / 180) # Circle at NE corner cutout. ccx, ccy = tmp_tm.TransformPoint(w / 2 - r, h / 2 - r) circles.append(FootprintPainter.circle_pts(ccx, ccy, r, tm)) # Circle at NW corner cutout. ccx, ccy = tmp_tm.TransformPoint(-w / 2 + r, h / 2 - r) circles.append(FootprintPainter.circle_pts(ccx, ccy, r, tm)) # Circle at SE corner cutout. ccx, ccy = tmp_tm.TransformPoint(w / 2 - r, -h / 2 + r) circles.append(FootprintPainter.circle_pts(ccx, ccy, r, tm)) # Circle at SW corner cutout. ccx, ccy = tmp_tm.TransformPoint(-w / 2 + r, -h / 2 + r) circles.append(FootprintPainter.circle_pts(ccx, ccy, r, tm)) return rects, circles
def test_affinematrix2d1(self): m = wx.Matrix2D() am = wx.AffineMatrix2D() am.Set(m, (23, 25)) values = am.Get() self.assertTrue(len(values) == 2) self.assertTrue(isinstance(values[0], wx.Matrix2D)) self.assertTrue(isinstance(values[1], wx.Point2D))
def rect_pts(cx, cy, w, h, rot, tm): """Compute the scaled/translated/rotated endpoints for a rectangle.""" cx, cy, w, h = [m * PRESCALE for m in [cx, cy, w, h]] h2, w2 = h / 2, w / 2 pts = ((-w2, -h2), (-w2, h2), (w2, h2), (w2, -h2)) tmp_tm = wx.AffineMatrix2D(tm) tmp_tm.Translate(cx, cy) tmp_tm.Rotate(rot * math.pi / 180) return [list(tmp_tm.TransformPoint(pt)) for pt in pts]
def on_mouse_wheel(self, event): if event.GetWheelRotation() > 0: zoom_delta = 1.03 else: zoom_delta = 0.97 # If we just change the zoom, the design will appear to move on the # screen. We have to adjust the pan to compensate. We want to keep # the part of the design under the mouse pointer in the same spot # after we zoom, so that we appar to be zooming centered on the # mouse pointer. # This will create a matrix that takes a point in the design and # converts it to screen coordinates: matrix = wx.AffineMatrix2D() matrix.Translate(*self.pan) matrix.Scale(self.zoom, self.zoom) # First, figure out where the mouse pointer is in the coordinate system # of the design: pos = event.GetPosition() inverse_matrix = wx.AffineMatrix2D() inverse_matrix.Set(*matrix.Get()) inverse_matrix.Invert() pos = inverse_matrix.TransformPoint(*pos) # Next, see how that point changes position on screen before and after # we apply the zoom change: x_old, y_old = matrix.TransformPoint(*pos) matrix.Scale(zoom_delta, zoom_delta) x_new, y_new = matrix.TransformPoint(*pos) x_delta = x_new - x_old y_delta = y_new - y_old # Finally, compensate for that change in position: self.pan = (self.pan[0] - x_delta, self.pan[1] - y_delta) self.zoom *= zoom_delta self.Refresh()
def trapezoid_pts(cx, cy, w, h, dw, dh, rot, tm): """Compute the scaled/translated/rotated endpoints for a trapezoid.""" cx, cy, w, h, dw, dh = [m * PRESCALE for m in [cx, cy, w, h, dw, dh]] h2, w2, dw2, dh2 = h / 2, w / 2, dw / 2, dh / 2 pts = ( (-w2 + dw2, -h2 - dh2), (-w2 - dw2, h2 + dh2), (w2 + dw2, h2 - dh2), (w2 - dw2, -h2 + dh2), ) tmp_tm = wx.AffineMatrix2D(tm) tmp_tm.Translate(cx, cy) tmp_tm.Rotate(rot * math.pi / 180) return [list(tmp_tm.TransformPoint(pt)) for pt in pts]
def paint(self, dc, paint_actual_size=False): """Paint the footprint into the device context.""" bbox = self.bbox if bbox: # The bbox for the footprint exists, so set the # scaling and translation for painting it within the DC. panel_w, panel_h = dc.GetSize() if paint_actual_size: scale_x = dc.GetPPI().GetWidth() * 1.0 / (25.4 * PRESCALE) scale_y = dc.GetPPI().GetHeight() * 1.0 / (25.4 * PRESCALE) else: panel_w = panel_w or 100 # Prevent division by zero. panel_h = panel_h or 100 # Prevent division by zero. scale_x = float(panel_w) / (bbox.x1 - bbox.x0) scale_y = float(panel_h) / (bbox.y1 - bbox.y0) scale = min(scale_x, scale_y) # Compute translation to place footprint center at center of panel. bbox_cx, bbox_cy = (bbox.x1 + bbox.x0) / 2, (bbox.y1 + bbox.y0) / 2 panel_cx, panel_cy = (panel_w / 2) / scale, (panel_h / 2) / scale tx, ty = panel_cx - bbox_cx, panel_cy - bbox_cy # Transformation matrices (TMs) operate opposite of expected: # the first scale/trans operation you add is the last operation # that's applied to a point. tm = wx.AffineMatrix2D() # Start with identity matrix. tm.Scale(scale, scale) # Apply scaling from real to screen dimensions. tm.Translate(tx, ty) else: # No bbox, so this must be first time the footprint is being painted. # Therefore, paint it without scaling to determine the physical dimensions. # These will be used later to scale it to the panel display. tm = wx.AffineMatrix2D() # Identity matrix so no scaling. # Initialize device context for drawing. dc.SetBackground(wx.Brush(FP_BCK_COLOUR)) dc.Clear() # Create layer dictionary for storing graphics on each layer. layers = defaultdict(Layer) # Draw the footprint's pad primitives. for pad in self.footprint.pads: attr = pad.attributes # Process the pad. w, h = (attr["size"] + [0])[0:2] cx, cy, rot = (attr["at"] + [0])[0:3] rot = -rot # Apply the offset of the pad from the drill. try: offset_x, offset_y = attr["drill"].attributes["offset"] except (AttributeError, KeyError, TypeError): pass # Either no drill or no pad offset w.r.t. the drill. else: # The pad is offset w.r.t. the drill, so it goes in the opposite dir. offset_x, offset_y = -offset_x, -offset_y # Rotate the offset as needed. rot_tm = wx.AffineMatrix2D() rot_tm.Rotate(rot * math.pi / 180) offset_x, offset_y = rot_tm.TransformPoint(offset_x, offset_y) # Apply the offset to the center of the pad. cx -= offset_x cy -= offset_y # Paint the appropriate pad shape. if attr["shape"] == "rect": pts = self.rect_pts(cx, cy, w, h, rot, tm) for lyr_nm in attr["layers"]: layers[lyr_nm].add_pad_polygon(pts) elif attr["shape"] == "circle": r = w / 2 pts = self.circle_pts(cx, cy, r, tm) for lyr_nm in attr["layers"]: layers[lyr_nm].add_pad_circle(pts) elif attr["shape"] == "roundrect": r_ratio = attr["roundrect_rratio"] r = r_ratio * min(w, h) pts_sets = self.roundrect_pts(cx, cy, w, h, r, rot, tm) for lyr_nm in attr["layers"]: for rect_pts in pts_sets[0]: layers[lyr_nm].add_pad_polygon(rect_pts) for circle_pts in pts_sets[1]: layers[lyr_nm].add_pad_circle(circle_pts) elif attr["shape"] == "oval": r_ratio = 0.5 r = r_ratio * min(w, h) pts_sets = self.roundrect_pts(cx, cy, w, h, r, rot, tm) for lyr_nm in attr["layers"]: for rect_pts in pts_sets[0]: layers[lyr_nm].add_pad_polygon(rect_pts) for circle_pts in pts_sets[1]: layers[lyr_nm].add_pad_circle(circle_pts) elif attr["shape"] == "trapezoid": try: dh, dw = attr["rect_delta"] except TypeError: dh, dw = 0, 0 pts = self.trapezoid_pts(cx, cy, w, h, dw, dh, rot, tm) for lyr_nm in attr["layers"]: layers[lyr_nm].add_pad_polygon(pts) else: raise NotImplementedError # Process any drill in the pad. cx, cy, rot = (attr["at"] + [0])[0:3] try: # See if there's a drill in this pad. drill_attr = attr["drill"].attributes except (KeyError, AttributeError): pass # No drill for this pad. else: # Yes, there is a drill. lyr_nm = "Drill" size = drill_attr["size"] try: # See if this pad is oval (i.e., has w, h). w, h = size r_ratio = 0.5 r = r_ratio * min(w, h) pts_sets = self.roundrect_pts(cx, cy, w, h, r, rot, tm) for rect_pts in pts_sets[0]: layers[lyr_nm].add_pad_polygon(rect_pts) for circle_pts in pts_sets[1]: layers[lyr_nm].add_pad_circle(circle_pts) except TypeError: # No, pad is circular. r = size / 2 pts = self.circle_pts(cx, cy, r, tm) layers[lyr_nm].add_pad_circle(pts) # Draw the footprint's line primitives. for line in self.footprint.lines: attr = line.attributes w = attr["width"] x0, y0 = attr["start"] x1, y1 = attr["end"] pts = self.line_pts(w, x0, y0, x1, y1, tm) lyr_nm = attr["layer"] layers[lyr_nm].add_line(pts) # Draw the footprint's circle primitives. for circle in self.footprint.circles: attr = circle.attributes w = attr["width"] cx, cy = attr["center"] x1, y1 = attr["end"] # Use line_pts routine to calc coords of center and endpoint and line width. w, cx, cy, x1, y1 = self.line_pts(w, cx, cy, x1, y1, tm) r = math.hypot(cx - x1, cy - y1) lyr_nm = attr["layer"] layers[lyr_nm].add_circle((w, cx, cy, r)) # Draw each layer, starting from the bottom layers that will be overwritten by the top layers. for lyr_nm in ( "B.SilkS", "B.Fab", "B.CrtYd", "B.Cu", "F&B.Cu", "F.Cu", "*.Cu", "F&B.CrtYd", "F.CrtYd", "*.CrtYd", "F&B.Fab", "F.Fab", "*.Fab", "F&B.SilkS", "F.SilkS", "*.SilkS", "Drill", ): layers[lyr_nm].set_fill( *layer_style[lyr_nm]) # Set layer color and fill. layers[lyr_nm].paint(dc) # Draw items for that layer. # layers['F.Mask'].set_fill(FMASK_COLOUR, wx.BRUSHSTYLE_CROSS_HATCH) # bmp = self.mk_bmp(4,4,(0,255,0), 1.0/16) # layers['F.Paste'].set_fill(bmp) if not self.bbox: # Calculate the bounding box if it hasn't been set, yet. # In this case, the transformation matrix will be an identity matrix, # so the bbox will contain the physical size of the footprint. # This will be scaled for the given device context when this function # is called again. self.bbox = BBox(*dc.GetBoundingBox()) # make the bbox a little bigger to put some margin along each side. delta = 0.025 * max(self.bbox.x1 - self.bbox.x0, self.bbox.y1 - self.bbox.y0) # Correct point-sized bounding boxes to prevent divide-by-zero errors during scaling. if delta < MIN_SIZE: delta = MIN_SIZE self.bbox = BBox( self.bbox.x0 - delta, self.bbox.y0 - delta, self.bbox.x1 + delta, self.bbox.y1 + delta, )