def _make_line(self, p1, p2, aperture): x1, y1 = float(p1[0]), float(p1[1]) x2, y2 = float(p2[0]), float(p2[1]) aperture = float(aperture) dx = x2 - x1 dy = y2 - y1 length = math.sqrt(dx * dx + dy * dy) if length == 0.0: return [] result = [] line1 = Line((x1 - aperture * (y2 - y1) / length, y1 - aperture * (x1 - x2) / length), (x2 - aperture * (y2 - y1) / length, y2 - aperture * (x1 - x2) / length)) line2 = Line((x1 + aperture * (y2 - y1) / length, y1 + aperture * (x1 - x2) / length), (x2 + aperture * (y2 - y1) / length, y2 + aperture * (x1 - x2) / length)) result.append(line1) result.append(line2) def make_arc(p1, p2, p0): start_angle = math.atan2(p1.y - p0.y, p1.x - p0.x) / math.pi end_angle = math.atan2(p2.y - p0.y, p2.x - p0.x) / math.pi return Arc(p0.x, p0.y, start_angle, end_angle, aperture) result.append(make_arc(line1.p1, line2.p1, Point(p1))) result.append(make_arc(line2.p2, line1.p2, Point(p2))) return result
def parse_line(self, rect): """ Parse a line element """ return [Line((get_x(rect, 'x1', self.svg_mult), get_y(rect, 'y1', self.svg_mult)), (get_x(rect, 'x2', self.svg_mult), get_y(rect, 'y2', self.svg_mult)))]
def test_create_new_line(self): """ Test the creation of a new empty line. """ p1 = Point(0, 1) p2 = Point(2, 3) line = Line(p1, p2) assert line.p1.x == p1.x assert line.p1.y == p1.y assert line.p2.x == p2.x assert line.p2.y == p2.y
def parse_z(self, data, is_relative): """ Parse a Z or z (closepath) segment. """ self.shapes.append( Line(make_point(self.cur_point, self.svg_mult), make_point(self.start_point, self.svg_mult))) self.cur_point = self.start_point return data
def parse_shape(self, shape): """ Extract a shape. """ # pylint: disable=R0914 # pylint: disable=R0911 typ = shape.get('type') if 'rectangle' == typ: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) parsed_shape = Rectangle(x, y, width, height) elif 'rounded_rectangle' == typ: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) radius = int(shape.get('radius')) parsed_shape = RoundedRectangle(x, y, width, height, radius) elif 'arc' == typ: x = int(shape.get('x')) y = int(shape.get('y')) start_angle = float(shape.get('start_angle')) end_angle = float(shape.get('end_angle')) radius = int(shape.get('radius')) parsed_shape = Arc(x, y, start_angle, end_angle, radius) elif 'circle' == typ: x = int(shape.get('x')) y = int(shape.get('y')) radius = int(shape.get('radius')) parsed_shape = Circle(x, y, radius) elif 'label' == typ: x = int(shape.get('x')) y = int(shape.get('y')) rotation = float(shape.get('rotation')) text = shape.get('text') align = shape.get('align') parsed_shape = Label(x, y, text, align, rotation) elif 'line' == typ: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = Line(p1, p2) elif 'polygon' == typ: parsed_shape = Polygon() for point in shape.get('points'): parsed_shape.add_point(self.parse_point(point)) elif 'bezier' == typ: control1 = self.parse_point(shape.get('control1')) control2 = self.parse_point(shape.get('control2')) p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = BezierCurve(control1, control2, p1, p2) parsed_shape.styles = shape.get('styles') or {} parsed_shape.attributes = shape.get('attributes') or {} return parsed_shape
def parse_line(self, args): """ Returns a parsed line. """ numpts, _sep, pts = args.partition(' ') pts = [int(p) for p in pts.split()] numpts = int(numpts) # this next bit would be much easier if open polygons were # explicitly acceptable # TODO yuck, and callers need to special-case this return ('lines', [Line((pts[i], pts[i + 1]),(pts[i + 2], pts[i + 3])) for i in range(0, (numpts - 1) * 2, 2)])
def parse_p_line(self, parts): """ Parse a P (Polyline) line """ num_points = int(parts[1]) lines = [] last_point = None for i in xrange(num_points): point = int(parts[5 + 2 * i]), int(parts[6 + 2 * i]) if last_point is not None: lines.append(Line(last_point, point)) last_point = point return lines
def parse_l(self, data, is_relative): """ Parse an L or l (lineto) segment. """ points, data = self.parse_points(data) for point in points: point = self.get_path_point(point, is_relative) self.shapes.append( Line(make_point(self.cur_point, self.svg_mult), make_point(point, self.svg_mult))) self.cur_point = point return data
def parse_h(self, data, is_relative): """ Parse an H or h (horizontal line) segment. """ nums, data = self.parse_nums(data) for num in nums: point = (num, 0 if is_relative else self.cur_point[1]) point = self.get_path_point(point, is_relative) self.shapes.append( Line(make_point(self.cur_point, self.svg_mult), make_point(point, self.svg_mult))) self.cur_point = point return data
def parse_v(self, data, is_relative): """ Parse a V or v (vertical line) segment. """ nums, data = self.parse_nums(data) for num in nums: point = (0 if is_relative else self.cur_point[0], num) point = self.get_path_point(point, is_relative) self.shapes.append( Line(make_point(self.cur_point, self.svg_mult), make_point(point, self.svg_mult))) self.cur_point = point return data
def make_shape_for_wire(self, wire): """ Generate an openjson shape for an eaglexml wire. """ if wire.curve is None: return Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2))) curve, x1, y1, x2, y2 = map( float, (wire.curve, wire.x1, wire.y1, wire.x2, wire.y2)) if curve < 0: curve = -curve negative = True mult = -1.0 else: negative = False mult = 1.0 if curve > 180.0: major_arc = True curve = 360.0 - curve mult *= -1.0 else: major_arc = False chordlen = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2)) radius = chordlen / (2.0 * sin(radians(curve) / 2)) mx, my = (x1 + x2) / 2, (y1 + y2) / 2 # midpoint between arc points h = sqrt(pow(radius, 2) - pow(chordlen / 2, 2)) # height of isoceles # calculate center point cx = mx + mult * h * (y1 - y2) / chordlen cy = my + mult * h * (x2 - x1) / chordlen if negative: start_angle = atan2(y2 - cy, x2 - cx) end_angle = start_angle + radians(curve) - (pi if major_arc else 0.0) else: start_angle = atan2(y1 - cy, x1 - cx) end_angle = start_angle + radians(curve) + (pi if major_arc else 0.0) return Arc(self.make_length(cx), self.make_length(cy), round(start_angle / pi, 3) % 2.0, round(end_angle / pi, 3) % 2.0, self.make_length(radius))
def parse_polyline(self, poly): """ Parse a polyline element """ shapes = [] last_point = None for point in poly.get('points', '').split(): if point: x, y = point.split(',') point = (make_x(x, self.svg_mult), make_y(y, self.svg_mult)) if last_point is not None: shapes.append(Line(last_point, point)) last_point = point return shapes
def make_body_from_symbol(self, lib, symbol_name): """ Contruct an openjson Body from an eagle symbol in a library. """ body = Body() symbol = [ s for s in get_subattr(lib, 'symbols.symbol') if s.name == symbol_name ][0] for wire in symbol.wire: body.add_shape( Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2)))) return body
def parse_m(self, data, is_relative): """ Parse an M or m (moveto) segment. """ points, data = self.parse_points(data) for i, point in enumerate(points): point = self.get_path_point(point, is_relative) if i == 0: self.start_point = self.cur_point = point else: # subsequent moves are lineto's self.shapes.append( Line(make_point(self.cur_point, self.svg_mult), make_point(point, self.svg_mult))) self.cur_point = point return data
def make_body_from_symbol(self, lib, symbol_name): """ Construct an openjson Body from an eagle symbol in a library. """ body = Body() symbol = [ s for s in get_subattr(lib, 'symbols.symbol') if s.name == symbol_name ][0] for wire in symbol.wire: body.add_shape( Line((self.make_length(wire.x1), self.make_length(wire.y1)), (self.make_length(wire.x2), self.make_length(wire.y2)))) for rect in symbol.rectangle: x = self.make_length(rect.x1) y = self.make_length(rect.y1) width = self.make_length(rect.x2) - x height = self.make_length(rect.y2) - y body.add_shape(Rectangle(x, y + height, width, height)) pin_map = {} for pin in symbol.pin: connect_point = (self.make_length(pin.x), self.make_length(pin.y)) null_point = self.get_pin_null_point(connect_point, pin.length, pin.rot) label = self.get_pin_label(pin, null_point) pin_map[pin.name] = Pin(pin.name, null_point, connect_point, label) body.add_pin(pin_map[pin.name]) ann_map = {} for text in symbol.text: x = self.make_length(text.x) y = self.make_length(text.y) content = '' if text.valueOf_ is None else text.valueOf_ rotation = self.make_angle('0' if text.rot is None else text.rot) if content == '>NAME': ann_map['name'] = Annotation(content, x, y, rotation, 'true') elif content == '>VALUE': ann_map['value'] = Annotation(content, x, y, rotation, 'true') else: body.add_shape(Label(x, y, content, 'left', rotation)) return body, pin_map, ann_map
def test_line_max_point(self): '''Test Line.max_point() in different situations''' line = Line(Point(2, 3), Point(4, 5)) bottom_right = line.max_point() self.assertEqual(bottom_right.x, 4) self.assertEqual(bottom_right.y, 5) line = Line(Point(2, 3), Point(-1, 4)) bottom_right = line.max_point() self.assertEqual(bottom_right.x, 2) self.assertEqual(bottom_right.y, 4)
def test_line_min_point(self): '''Test Line.min_point() in different situations''' line = Line(Point(2, 3), Point(4, 5)) top_left = line.min_point() self.assertEqual(top_left.x, 2) self.assertEqual(top_left.y, 3) line = Line(Point(2, 3), Point(-1, 4)) top_left = line.min_point() self.assertEqual(top_left.x, -1) self.assertEqual(top_left.y, 3)
def _move(self, block): """ Draw a shape, or a segment of a trace or fill. """ start = tuple([self.status[k] for k in ('x', 'y')]) end = self._target_pos(block) ends = (Point(start), Point(end)) apertures = self.layer_buff.apertures if self.status['draw'] == 'ON': # generate segment if self.status['interpolation'] == 'LINEAR': seg = Line(start, end) else: ctr_offset = block[2:] seg = self._draw_arc(ends, ctr_offset) # append segment to fill if self.status['outline_fill']: self.fill_buff.append((ends, seg)) else: aperture = apertures[self.status['aperture']] if isinstance(aperture.shape, Rectangle): # construct a smear self._check_smear(seg, aperture.shape) self.img_buff.smears.append(Smear(seg, aperture.shape)) else: wid = aperture.shape.radius * 2 trace = self.trace_buff.get_trace(wid, seg) if trace is None: # FIXME(shamer): fill into segments not old Trace class #trace = Trace(wid) self.img_buff.traces.append(trace) trace.segments.append(seg) self.trace_buff.add_segment(seg, trace) elif self.status['draw'] == 'FLASH': aperture = apertures[self.status['aperture']] shape_inst = ShapeInstance(ends[1], aperture) self.img_buff.shape_instances.append(shape_inst) return {'x': end[0], 'y': end[1]}
def add_shape(self, shape, parent, parent_offset, offset): """ Add a shape to the image. (might be added as a smear or fill) """ # Copy the shape so it can be mutated without affecting other instances shapecpy = copy.deepcopy(shape) # XXX(shamer): a label needs to be rendered before in-place rotations are made so the bounding box for the shape # are known if isinstance(shapecpy, Label): self.face.set_char_size(int(shapecpy.font_size)) label_contours = [] x_offset = 0 y_offset = 0 label_text = self.resolve_text(shapecpy.text, parent.get_attribute) for i, c in enumerate(label_text): self.face.load_char(c, flags=freetype.ft_enums.FT_LOAD_NO_BITMAP) slot = self.face.glyph outline = slot.outline glyph_contours = [] start, end = 0, 0 # Iterate over each contour separately. Characters like 'g' have multiple contours as they cannot be # walked with a contiguous set of arcs. The contours list contains the index of the point that the # contour starts on. for contour_idx in range(len(outline.contours)): end = outline.contours[contour_idx] points = [Point(t[0], t[1]) for t in outline.points[start:end+1]] tags = outline.tags[start:end+1] # Close the contour by repeating the last point. points.append(copy.deepcopy(points[0])) tags.append(copy.deepcopy(tags[0])) segments = [[points[0], ], ] # Group points into segments. The tag identifies real vs control points. for point_idx in range(1, len(points) ): segments[-1].append(points[point_idx]) if tags[point_idx] & (1 << 0) and point_idx < (len(points)-1): segments.append([copy.deepcopy(points[point_idx]),]) # take the fist and last points of each segment (the non-control points). To approximate the curves # using straight lines. glyph_contours.append([[segment[0], segment[-1]] for segment in segments]) start = end+1 # update the segments in the glyph with the x_offset for contour_segments in glyph_contours: for segments in contour_segments: for point in segments: point.x += x_offset point.y += y_offset label_contours.extend(glyph_contours) x_offset += slot.advance.x # adjust amount to advance with kerning offset if i + 1 < len(label_text): next_c = label_text[i + 1] x_offset += self.face.get_kerning(c, next_c).x # Update the segments for pre-render shifts, rotates, alignment for contour_segments in label_contours: for segments in contour_segments: if shapecpy.align == 'center': segments[0].shift(-(x_offset / 2), 0) segments[1].shift(-(x_offset / 2), 0) if shapecpy.rotation: segments[0].rotate(shapecpy.rotation) segments[1].rotate(shapecpy.rotation) if shapecpy.flip_horizontal: segments[0].flip(shapecpy.flip_horizontal) segments[1].flip(shapecpy.flip_horizontal) shapecpy._segments.append(segments) # Calculate the bounding fox the label from the segments min_point = [2**100, 2**100] max_point = [-2**100, -2**100] for contour_segments in label_contours: for segments in contour_segments: min_point[0] = min(segments[0].x, segments[1].x, min_point[0]) max_point[0] = max(segments[0].x, segments[1].x, max_point[0]) min_point[1] = min(segments[0].y, segments[1].y, min_point[1]) max_point[1] = max(segments[0].y, segments[1].y, max_point[1]) shapecpy._min_point = Point(min_point[0], min_point[1]) shapecpy._max_point = Point(max_point[0], max_point[1]) shapecpy.shift(offset.x, offset.y) if parent_offset.rotation != 0: shapecpy.rotate(parent_offset.rotation) if parent_offset.flip_horizontal: shapecpy.flip(parent_offset.flip_horizontal) shapecpy.shift(parent_offset.x, parent_offset.y) if offset.rotation != 0: if parent_offset.flip_horizontal: shapecpy.rotate(-offset.rotation, in_place=True) else: shapecpy.rotate(offset.rotation, in_place=True) if offset.flip_horizontal: shapecpy.flip(offset.flip_horizontal) if isinstance(shapecpy, Line): # FIXME(shamer): line doesn't have an explicit width. Gets used for outlines. Defaulted to 0.15mm self.smears.append(Smear(shapecpy, Circle(0, 0, 0.15 * 1000000))) elif isinstance(shapecpy, Circle): self.shape_instances.append(ShapeInstance(Point(shapecpy.x, shapecpy.y), Aperture(None, shapecpy, None))) elif isinstance(shapecpy, Rectangle): #shapecpy.x += shapecpy.width #shapecpy.y -= shapecpy.height / 2 shapecpy.width = abs(shapecpy.width) shapecpy.height = abs(shapecpy.height) if shapecpy.rotation != 0: instance_name = 'Rect-W{width}-H{height}-RO{rotation}'.format(height=shapecpy.height, width=shapecpy.width, rotation=shapecpy.rotation) # XXX(shamer): additional copy is made so the x, y can be reset for use as a ComplexInstance shapecpycpy = copy.deepcopy(shapecpy) shapecpycpy.x = 0 shapecpycpy.y = 0 shapecpycpy.is_centered = True primitives = [Primitive(1, 0.0, shapecpycpy)] self.complex_instances.append(ComplexInstance(instance_name, Point(shapecpy.x, shapecpy.y), primitives)) else: self.shape_instances.append(ShapeInstance(Point(shapecpy.x, shapecpy.y), Aperture(None, shapecpy, None))) elif isinstance(shapecpy, RoundedRectangle): # Rounded rectangle is added as a macro with two rectangles to fill out the body and four circles to make up # the corners. `roundrect` is assumed to already be centered. primitives = [] radius = abs(shapecpy.radius) inner_height = abs(shapecpy.height) - (radius * 2) inner_width = abs(shapecpy.width) - (radius * 2) half_width = inner_width / 2.0 half_height = inner_height / 2.0 high_rect = Rectangle(0, 0, abs(inner_width), abs(shapecpy.height), is_centered=True) wide_rect = Rectangle(0, 0, abs(shapecpy.width), abs(inner_height), is_centered=True) primitives.append(Primitive(1, 0.0, high_rect)) primitives.append(Primitive(1, 0.0, wide_rect)) primitives.append(Primitive(1, 0.0, Circle(-half_width, half_height, shapecpy.radius))) primitives.append(Primitive(1, 0.0, Circle(half_width, half_height, shapecpy.radius))) primitives.append(Primitive(1, 0.0, Circle(half_width, -half_height, shapecpy.radius))) primitives.append(Primitive(1, 0.0, Circle(-half_width, -half_height, shapecpy.radius))) # rotate the positioning of the rounded corners (the circles) for primitive in primitives: primitive.shape.rotate(shapecpy.rotation) instance_name = 'RR-H{height}-W{width}-R{radius}-RO{rotation}'.format(height=abs(shapecpy.height), width=abs(shapecpy.width), radius=radius, rotation=shapecpy.rotation) self.complex_instances.append(ComplexInstance(instance_name, Point(shapecpy.x, shapecpy.y), primitives)) elif isinstance(shapecpy, Label): # TODO(shamer): cache positions segments for glyphs # FIXME((shamer): make baseline shift # TODO(shamer) select the correct font based off of the label.font_family # Debugging, used to show the anchor point of the label #self.shape_instances.append(ShapeInstance(Point(shapecpy.x, shapecpy.y), Aperture(None, Circle(0, 0, 1000000 / 5), None))) for segments in shapecpy._segments: line = Line(segments[0], segments[1]) line.shift(shapecpy.x, shapecpy.y) self.smears.append(Smear(line, Circle(0, 0, 0.1016 * 1000000))) # 4 Mils
def parse_shape(self, shape): """ Extract a shape. """ # pylint: disable=R0914 # pylint: disable=R0911 rotation = shape.get('rotation', 0.0) flip_horizontal = shape.get('flip_horizontal', False) shape_type = shape.get('type') if 'rectangle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) parsed_shape = Rectangle(x, y, width, height) elif 'rounded_rectangle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) height = int(shape.get('height')) width = int(shape.get('width')) radius = int(shape.get('radius')) parsed_shape = RoundedRectangle(x, y, width, height, radius) elif 'arc' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) start_angle = float(shape.get('start_angle')) end_angle = float(shape.get('end_angle')) radius = int(shape.get('radius')) parsed_shape = Arc(x, y, start_angle, end_angle, radius) elif 'circle' == shape_type: x = int(shape.get('x')) y = int(shape.get('y')) radius = int(shape.get('radius')) parsed_shape = Circle(x, y, radius) elif 'label' == shape_type: parsed_shape = self.parse_label(shape) elif 'line' == shape_type: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = Line(p1, p2) elif 'polygon' == shape_type: parsed_shape = Polygon() for point in shape.get('points'): parsed_shape.add_point(self.parse_point(point)) elif 'bezier' == shape_type: control1 = self.parse_point(shape.get('control1')) control2 = self.parse_point(shape.get('control2')) p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) parsed_shape = BezierCurve(control1, control2, p1, p2) elif 'rounded_segment' == shape_type: p1 = self.parse_point(shape.get('p1')) p2 = self.parse_point(shape.get('p2')) width = int(shape.get('width')) parsed_shape = RoundedSegment(p1, p2, width) parsed_shape.rotation = rotation parsed_shape.flip_horizontal = flip_horizontal parsed_shape.styles = shape.get('styles') or {} parsed_shape.attributes = shape.get('attributes') or {} return parsed_shape
def _define_images(self, design, layer_name): """ Define the images that make up the layer information. """ log.debug('creating images for layer "%s"', layer_name) # trace segments on this layer traces_image = Image(layer_name + '_traces', font_renderer=self.face) for segment in design.trace_segments: if segment.layer != layer_name: continue log.debug('Creating smear for trace: %s', segment) # Assumes segment is rounded, straignt trace_smear = Smear(Line(segment.p1, segment.p2), Circle(0, 0, segment.width / 2.0)) traces_image.smears.append(trace_smear) # Generated objects in the design (vias, PTHs) zero_pos = FootprintPos(0, 0, 0.0, False, 'top') for gen_obj in design.layout_objects: # XXX(shamer): body attr is only being used to hold the layer, other placement details are contained # elsewhere for body_attr, body in gen_obj.bodies(zero_pos, {}): if body_attr.layer == layer_name: for shape in body.shapes: traces_image.add_shape(shape, design, zero_pos, body_attr) self.images.append(traces_image) # Pours on this layer for pour in design.pours: log.debug('adding body for pour: %s points, %s', len(pour.points), pour.layer) if layer_name == pour.layer: log.debug('adding body for pour: %s points, %s subtractive shapes', len(pour.points), len(pour.subtractive_shapes)) fill_image = Image('pour fill', font_renderer=self.face) fill_image.fills.append(Fill(pour.points)) self.images.append(fill_image) subtractive_image = Image('pour subtractive shapes', font_renderer=self.face, is_additive=False) for shape in pour.subtractive_shapes: if shape.type == 'rounded_segment': trace_smear = Smear(Line(shape.p1, shape.p2), Circle(0, 0, shape.width / 2.0)) subtractive_image.smears.append(trace_smear) else: subtractive_image.add_shape(shape, None, FootprintPos(0, 0, 0.0, False, ''), FootprintPos(0, 0, 0.0, False, '')) self.images.append(subtractive_image) readded_image = Image('pour readded shapes', font_renderer=self.face, is_additive=True) for shape in pour.readded_shapes: if shape.type == 'rounded_segment': trace_smear = Smear(Line(shape.p1, shape.p2), Circle(0, 0, shape.width / 2.0)) readded_image.smears.append(trace_smear) else: readded_image.add_shape(shape, None, FootprintPos(0, 0, 0.0, False, ''), FootprintPos(0, 0, 0.0, False, '')) self.images.append(readded_image) # trace segments on this layer traces_image = Image(layer_name + '_traces', font_renderer=self.face) for segment in design.trace_segments: if segment.layer != layer_name: continue log.debug('Creating smear for trace: %s', segment) # Assumes segment is rounded, straignt trace_smear = Smear(Line(segment.p1, segment.p2), Circle(0, 0, segment.width / 2.0)) traces_image.smears.append(trace_smear) # Generated objects in the design (vias, PTHs) zero_pos = FootprintPos(0, 0, 0.0, False, 'top') for gen_obj in design.layout_objects: # XXX(shamer): body attr is only being used to hold the layer, other placement details are contained # elsewhere for body_attr, body in gen_obj.bodies(zero_pos, {}): if body_attr.layer == layer_name: for shape in body.shapes: traces_image.add_shape(shape, design, zero_pos, body_attr) self.images.append(traces_image) # Component aspects on this layer # a separate image is used for each component for component_instance in design.component_instances: component = design.components.components[component_instance.library_id] component_image = Image(layer_name + ' component ' + component_instance.instance_id, font_renderer=self.face) footprint_pos = component_instance.footprint_pos if footprint_pos.side is None: continue for idx, footprint_attr in enumerate(component_instance.footprint_attributes): log.debug('footprint pos: %s, side %s, flip %s', footprint_attr.layer, footprint_pos.side, footprint_pos.flip_horizontal) fp_attr_cpy = copy.deepcopy(footprint_attr) if footprint_attr.layer: if footprint_pos.side == 'bottom': rev_sides = {'top': 'bottom', 'bottom': 'top'} fp_attr_cpy.layer = ' '.join([rev_sides.get(piece, piece) for piece in footprint_attr.layer.split(' ')]) if fp_attr_cpy.layer == layer_name: footprint_body = component.footprints[component_instance.footprint_index].bodies[idx] log.debug('adding footprint attribute: %s, %d shapes', fp_attr_cpy, len(footprint_body.shapes)) for shape in footprint_body.shapes: component_image.add_shape(shape, component_instance, footprint_pos, fp_attr_cpy) for idx, gen_obj_attr in enumerate(component_instance.gen_obj_attributes): gen_obj = component.footprints[component_instance.footprint_index].gen_objs[idx] # FIXME(shamer): check for unplaced generated objects. # XXX(shamer): body attr is only being used to hold the layer, other placement details are contained # elsewhere for body_attr, body in gen_obj.bodies(footprint_pos, gen_obj_attr.attributes): if body_attr.layer == layer_name: log.debug('adding body for generated object: %s, %s', footprint_pos, gen_obj_attr) for shape in body.shapes: component_image.add_shape(shape, component_instance, footprint_pos, body_attr) if component_image.not_empty(): self.images.append(component_image) # paths on the layer for path in design.paths: if layer_name == path.layer: log.debug('adding body for path: %s points, %s, %s, is closed: %s', len(path.points), path.width, path.layer, path.is_closed) path_image = Image('path', font_renderer=self.face) start = path.points[0] for point in path.points[1:]: path_image.add_shape(Line(start, point), Circle(0, 0, path.width), zero_pos, zero_pos) start = point if path.is_closed: path_image.add_shape(Line(path.points[0], path.points[-1]), Circle(0, 0, path.width), zero_pos, zero_pos) self.images.append(path_image) # stand alone text on the layer text_image = Image('text', font_renderer=self.face) for text in design.pcb_text: if layer_name == text.layer: log.debug('adding body for text: "%s"', text.value) text_image.add_shape(text.label, design, text, zero_pos) if text_image.not_empty(): self.images.append(text_image)
def add_shape(self, shape, parent, parent_offset, offset): """ Add a shape to the image. (might be added as a smear or fill) """ # Copy the shape so it can be mutated without affecting other instances shapecpy = copy.deepcopy(shape) # XXX(shamer): a label needs to be rendered before in-place rotations are made so the bounding box for the shape # are known if isinstance(shapecpy, Label): self.face.set_char_size(int(shapecpy.font_size)) label_contours = [] x_offset = 0 y_offset = 0 label_text = self.resolve_text(shapecpy.text, parent.get_attribute) for i, c in enumerate(label_text): self.face.load_char(c, flags=freetype.ft_enums.FT_LOAD_NO_BITMAP) slot = self.face.glyph outline = slot.outline glyph_contours = [] start, end = 0, 0 # Iterate over each contour separately. Characters like 'g' have multiple contours as they cannot be # walked with a contiguous set of arcs. The contours list contains the index of the point that the # contour starts on. for contour_idx in range(len(outline.contours)): end = outline.contours[contour_idx] points = [ Point(t[0], t[1]) for t in outline.points[start:end + 1] ] tags = outline.tags[start:end + 1] # Close the contour by repeating the last point. points.append(copy.deepcopy(points[0])) tags.append(copy.deepcopy(tags[0])) segments = [ [ points[0], ], ] # Group points into segments. The tag identifies real vs control points. for point_idx in range(1, len(points)): segments[-1].append(points[point_idx]) if tags[point_idx] & (1 << 0) and point_idx < ( len(points) - 1): segments.append([ copy.deepcopy(points[point_idx]), ]) # take the fist and last points of each segment (the non-control points). To approximate the curves # using straight lines. glyph_contours.append([[segment[0], segment[-1]] for segment in segments]) start = end + 1 # update the segments in the glyph with the x_offset for contour_segments in glyph_contours: for segments in contour_segments: for point in segments: point.x += x_offset point.y += y_offset label_contours.extend(glyph_contours) x_offset += slot.advance.x # adjust amount to advance with kerning offset if i + 1 < len(label_text): next_c = label_text[i + 1] x_offset += self.face.get_kerning(c, next_c).x # Update the segments for pre-render shifts, rotates, alignment for contour_segments in label_contours: for segments in contour_segments: if shapecpy.align == 'center': segments[0].shift(-(x_offset / 2), 0) segments[1].shift(-(x_offset / 2), 0) if shapecpy.rotation: segments[0].rotate(shapecpy.rotation) segments[1].rotate(shapecpy.rotation) if shapecpy.flip_horizontal: segments[0].flip(shapecpy.flip_horizontal) segments[1].flip(shapecpy.flip_horizontal) shapecpy._segments.append(segments) # Calculate the bounding fox the label from the segments min_point = [2**100, 2**100] max_point = [-2**100, -2**100] for contour_segments in label_contours: for segments in contour_segments: min_point[0] = min(segments[0].x, segments[1].x, min_point[0]) max_point[0] = max(segments[0].x, segments[1].x, max_point[0]) min_point[1] = min(segments[0].y, segments[1].y, min_point[1]) max_point[1] = max(segments[0].y, segments[1].y, max_point[1]) shapecpy._min_point = Point(min_point[0], min_point[1]) shapecpy._max_point = Point(max_point[0], max_point[1]) shapecpy.shift(offset.x, offset.y) if parent_offset.rotation != 0: shapecpy.rotate(parent_offset.rotation) if parent_offset.flip_horizontal: shapecpy.flip(parent_offset.flip_horizontal) shapecpy.shift(parent_offset.x, parent_offset.y) if offset.rotation != 0: if parent_offset.flip_horizontal: shapecpy.rotate(-offset.rotation, in_place=True) else: shapecpy.rotate(offset.rotation, in_place=True) if offset.flip_horizontal: shapecpy.flip(offset.flip_horizontal) if isinstance(shapecpy, Line): # FIXME(shamer): line doesn't have an explicit width. Gets used for outlines. Defaulted to 0.15mm self.smears.append(Smear(shapecpy, Circle(0, 0, 0.15 * 1000000))) elif isinstance(shapecpy, Circle): self.shape_instances.append( ShapeInstance(Point(shapecpy.x, shapecpy.y), Aperture(None, shapecpy, None))) elif isinstance(shapecpy, Rectangle): #shapecpy.x += shapecpy.width #shapecpy.y -= shapecpy.height / 2 shapecpy.width = abs(shapecpy.width) shapecpy.height = abs(shapecpy.height) if shapecpy.rotation != 0: instance_name = 'Rect-W{width}-H{height}-RO{rotation}'.format( height=shapecpy.height, width=shapecpy.width, rotation=shapecpy.rotation) # XXX(shamer): additional copy is made so the x, y can be reset for use as a ComplexInstance shapecpycpy = copy.deepcopy(shapecpy) shapecpycpy.x = 0 shapecpycpy.y = 0 shapecpycpy.is_centered = True primitives = [Primitive(1, 0.0, shapecpycpy)] self.complex_instances.append( ComplexInstance(instance_name, Point(shapecpy.x, shapecpy.y), primitives)) else: self.shape_instances.append( ShapeInstance(Point(shapecpy.x, shapecpy.y), Aperture(None, shapecpy, None))) elif isinstance(shapecpy, RoundedRectangle): # Rounded rectangle is added as a macro with two rectangles to fill out the body and four circles to make up # the corners. `roundrect` is assumed to already be centered. primitives = [] radius = abs(shapecpy.radius) inner_height = abs(shapecpy.height) - (radius * 2) inner_width = abs(shapecpy.width) - (radius * 2) half_width = inner_width / 2.0 half_height = inner_height / 2.0 high_rect = Rectangle(0, 0, abs(inner_width), abs(shapecpy.height), is_centered=True) wide_rect = Rectangle(0, 0, abs(shapecpy.width), abs(inner_height), is_centered=True) primitives.append(Primitive(1, 0.0, high_rect)) primitives.append(Primitive(1, 0.0, wide_rect)) primitives.append( Primitive(1, 0.0, Circle(-half_width, half_height, shapecpy.radius))) primitives.append( Primitive(1, 0.0, Circle(half_width, half_height, shapecpy.radius))) primitives.append( Primitive(1, 0.0, Circle(half_width, -half_height, shapecpy.radius))) primitives.append( Primitive(1, 0.0, Circle(-half_width, -half_height, shapecpy.radius))) # rotate the positioning of the rounded corners (the circles) for primitive in primitives: primitive.shape.rotate(shapecpy.rotation) instance_name = 'RR-H{height}-W{width}-R{radius}-RO{rotation}'.format( height=abs(shapecpy.height), width=abs(shapecpy.width), radius=radius, rotation=shapecpy.rotation) self.complex_instances.append( ComplexInstance(instance_name, Point(shapecpy.x, shapecpy.y), primitives)) elif isinstance(shapecpy, Label): # TODO(shamer): cache positions segments for glyphs # FIXME((shamer): make baseline shift # TODO(shamer) select the correct font based off of the label.font_family # Debugging, used to show the anchor point of the label #self.shape_instances.append(ShapeInstance(Point(shapecpy.x, shapecpy.y), Aperture(None, Circle(0, 0, 1000000 / 5), None))) for segments in shapecpy._segments: line = Line(segments[0], segments[1]) line.shift(shapecpy.x, shapecpy.y) self.smears.append(Smear(line, Circle(0, 0, 0.1016 * 1000000))) # 4 Mils