def test_constructor_copy(self): v1 = Vector2(angle=rad(30), m=1) v2 = Vector2(v1) self.assert_equals(v2.x, v1.x) self.assert_equals(v2.y, v1.y) self.assert_equals(v2.m, v1.m) self.assert_equals(v2.a, v1.a)
def precalc_node(self, node): """ Precalculate node properties that are needed by the layout algorithms. """ E = self._eval_func(node) node.fontsize = E('fontSize') self._precalc_text(node) padx = E('textPadX') pady = E('textPadY') node.width = node.textwidth + padx * 2 node.height = node.textheight + pady * 2 node.bboxwidth = node.width node.bboxheight = node.height node._textxoffs = padx node._textyoffs = pady node.text_has_background = True y = node.bboxheight / 2 node._conn_point_left = Vector2(0, y) node._conn_point_right = Vector2(node.bboxwidth, y)
def _calcbbox(self): m = sys.maxint topleft = Vector2(m, m) bottomright = Vector2(-m, -m) self._calcbbox_recurse(self.root, topleft, bottomright) return Rectangle(topleft.x, topleft.y, bottomright.x - topleft.x, bottomright.y - topleft.y)
def precalc_node(self, node): """ Precalculate node properties that are needed by the layout and colorizer algorithms. """ super(LineNodeDrawer, self).precalc_node(node) node.text_has_background = False y = node.height node._conn_point_left = Vector2(0, y) node._conn_point_right = Vector2(node.width, y)
def precalc_node(self, node): super(BoxNodeDrawer, self).precalc_node(node) E = self._eval_func(node) node._boxdepth = E('boxDepth') # Make the bounding box big enough so that the shadow can fit in # too -- the actual coordinate calculations will happen later node.bboxwidth += node._boxdepth node.bboxheight += node._boxdepth y = node.bboxheight / 2 node._conn_point_left = Vector2(0, y) node._conn_point_right = Vector2(node.bboxwidth, y)
def test_normalize(self): v = Vector2(4, -4) self.assert_equals(5.65685424949238, v.m) self.assert_equals(45.0, deg(v.a)) v.normalize() self.assert_equals(1.0, v.m) self.assert_equals(45.0, deg(v.a))
def _calc_arc_segment(cx, cy, x1, y1, x4, y4): ax = x1 - cx ay = y1 - cy bx = x4 - cx by = y4 - cy q1 = ax * ax + ay * ay q2 = q1 + ax * bx + ay * by d = ax * by - ay * bx if d == 0: d = 1e-15 k2 = 1.3333333333 * (math.sqrt(2 * q1 * q2) - q2) / d x2 = cx + ax - k2 * ay y2 = cy + ay + k2 * ax x3 = cx + bx + k2 * by y3 = cy + by - k2 * bx return [Vector2(x1, y1), Vector2(x2, y2), Vector2(x3, y3), Vector2(x4, y4)]
def calc_regular_polygon_points(cx, cy, r, numsides, rotation=0): """ Calculates the vertices of a regular n-sided polygon. """ points = [] a = 2 * math.pi / numsides sa = math.radians(rotation) for i in range(numsides + 1): x = cx + r * math.cos(sa + a * i) y = cy - r * math.sin(sa + a * i) points.append(Vector2(x, y)) return points
def intersect(p1, p2, p3, p4): c = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x) # The two lines are parallel if abs(c) < 1e-15: return None a = p1.x * p2.y - p1.y * p2.x b = p3.x * p4.y - p3.y * p4.x x = (a * (p3.x - p4.x) - (p1.x - p2.x) * b) / c y = (a * (p3.y - p4.y) - (p1.y - p2.y) * b) / c return Vector2(x, y)
def slice_shape(points, y, h, ystep): """ Calculate the intersections of a convex shape and a set of equidistant horizontal lines. `points` points defining the shape segments `y` topmost (lowest value) y coordinate of the original shape `h` height of the original shape `ystep` vertical distance between horizontal lines Only point-pairs are returned, single-point intersections are omitted (e.g. when a horizontal line goes exactly through a single vertex). The point-pairs are returned in a 3-dimensional array: points = [[l1p1, l2p2], [l2p1, l2p2], ... [lnp1, lnp2]] The points have the following properties: lnp1.y = lnp2.y lnp1.x <= lnp2.x lnpm.y < l(n + 1)pm.y """ # Close the shape if the shape is already closed that doesn't # affect the algorithm points.append(points[0]) numlines = (int) (float(h) / ystep) # Special case when ystep > h / 2 if numlines == 1: numlines = 2 # Center lines to the vertical center of the shape y = y + (h - (numlines - 1) * ystep) / 2. # Iterate through all horizontal lines (starting from the lowest y # coordinate) and calculate the two intersection points of each line # with the shape segments (some lines may result in zero or one # intersections, these will be omitted). intersections = [] for l in range(numlines): pointpair = [] for i in range(len(points) - 1): p1 = points[i] p2 = points[i + 1] if p1.y > p2.y: p1, p2 = p2, p1 if y >= p1.y and y < p2.y: # Special case for p1.x = p2.x (vertical line) dx = p2.x - p1.x + 1e-5 # Calculate the intersection of a horizontal line and a # single shape segment dy = p2.y - p1.y m = dy / dx x = p1.x + 1 / m * (y - p1.y) pointpair.append(Vector2(x, y)) # There should be two (or zero) intersections for each # horizontal line if len(pointpair) == 2: if pointpair[0].x > pointpair[1].x: pointpair[0], pointpair[1] = pointpair[1], pointpair[0] intersections.append(pointpair) break y += ystep return intersections
def _draw(self, node, direction=None): """ Draw a curved connection between a node and its child nodes. """ E = self._eval_func(node) children = node.getchildren(direction) if not children: return linewidth = E('lineWidth') _ctx.autoclosepath(True) _ctx.stroke(node.connectioncolor) _ctx.fill(node.connectioncolor) _ctx.strokewidth(linewidth) firstchild = children[0] lastchild = children[-1] direction = firstchild.direction() opp_direction = opposite_dir(direction) x1, y1 = node.connection_point(direction) xfirst, yfirst = firstchild.connection_point(opp_direction) # Special case: draw straight line if there's only one child if len(children) == 1: _ctx.line(x1, y1, xfirst, yfirst) return # Calculate junction point position jx = x1 + (xfirst - x1) * E('junctionXFactor') jy = y1 # Draw line from parent node to junction point _ctx.line(x1, y1, jx, jy) # Limit first & last corner radius to the available area ylast = lastchild.connection_point(opp_direction)[1] ysecond = children[1].connection_point(opp_direction)[1] ypenultimate = children[-2].connection_point(opp_direction)[1] # Starting corner radius cornerPad = E('cornerPad') r = min(E('cornerRadius'), abs(jx - xfirst) - cornerPad) r = max(r, 0) # Adjusted first (top) corner radius r1 = min(r, abs(yfirst - jy) - cornerPad) r1 = max(r1, 0) if ysecond < jy: r1 = min(r, abs(yfirst - ysecond) - cornerPad) r1 = max(r1, 0) # Adjusted last (bottom) corner radius r2 = min(r, abs(ylast - jy) - cornerPad) r2 = max(r2, 0) if ypenultimate > jy: r2 = min(r, abs(ylast - ypenultimate) - cornerPad) r2 = max(r2, 0) # Draw main branch as a single path to ensure line continuity p1 = Vector2(jx, yfirst + r1) p2 = Vector2(jx, ylast - r2) segments = [[p1, p2]] corner_style = E('cornerStyle') for i, child in enumerate(children): direction = child.direction() opp_direction = opposite_dir(direction) x2, y2 = child.connection_point(opp_direction) if direction == Direction.Left: x2 -= linewidth / 2 elif direction == Direction.Right: x2 += linewidth / 2 # Draw corners if direction == Direction.Left: a1 = 90 da = -90 dx1 = r1 * 2 dx2 = r2 * 2 else: a1 = da = 90 dx1 = dx2 = 0 x1 = jx if child is firstchild: x1 += -r1 if direction == Direction.Left else r1 if (corner_style == 'square' or abs(y2 - jy) < .001): p1 = Vector2(jx, y2) p2 = Vector2(jx, y2 + r1) segments.insert(0, [p1, p2]) p1 = Vector2(x1, y2) p2 = Vector2(jx, y2) segments.insert(0, [p1, p2]) elif corner_style == 'beveled': p1 = Vector2(x1, y2) p2 = Vector2(jx, y2 + r1) segments.insert(0, [p1, p2]) elif corner_style == 'rounded': arc = arcpath(jx - dx1, y2, r1 * 2, r1 * 2, a1, da) segments = arc + segments p1 = Vector2(x2, y2) p2 = Vector2(x1, y2) segments.insert(0, [p1, p2]) elif child is lastchild: x1 += -r2 if direction == Direction.Left else r2 if (corner_style == 'square' or abs(y2 - jy) < .001): p1 = Vector2(jx, y2 - r2) p2 = Vector2(jx, y2) segments.append([p1, p2]) p1 = Vector2(jx, y2) p2 = Vector2(x1, y2) segments.append([p1, p2]) elif corner_style == 'beveled': p1 = Vector2(jx, y2 - r2) p2 = Vector2(x1, y2) segments.append([p1, p2]) elif corner_style == 'rounded': arc = arcpath(jx - dx2, y2 - r2 * 2, r2 * 2, r2 * 2, a1 + da, da) segments = segments + arc p1 = Vector2(x1, y2) p2 = Vector2(x2, y2) segments.append([p1, p2]) else: _ctx.line(x1, y2, x2, y2) # Draw main branch path _ctx.nofill() path = createpath(_ctx, segments, close=False) _ctx.drawpath(path) # Draw junction point style = E('junctionStyle') if style == 'none': return r = E('junctionRadius') r2 = r / 2. _ctx.fill(E('junctionFillColor')) _ctx.stroke(E('junctionStrokeColor')) _ctx.strokewidth(E('junctionStrokeWidth')) if style == 'square': _ctx.rect(jx - r2, jy - r2, r, r) elif style == 'disc': _ctx.oval(jx - r2, jy - r2, r, r) elif style == 'diamond': _ctx.beginpath(jx, jy - r2) _ctx.lineto(jx + r2, jy) _ctx.lineto(jx, jy + r2) _ctx.lineto(jx - r2, jy) _ctx.lineto(jx, jy - r2) _ctx.endpath() # Draw junction sign sign = E('junctionSign') if sign == 'none': return _ctx.stroke(E('junctionSignColor')) d = E('junctionSignSize') / 2. _ctx.strokewidth(E('junctionSignStrokeWidth')) if sign in ('minus', 'plus'): _ctx.line(jx - d, jy, jx + d, jy) if sign == 'plus': _ctx.line(jx, jy - d, jx, jy + d)
def rounded_rect(x, y, w, h, r): points = [Vector2(x, y), Vector2(x + w, y), Vector2(x + w, y + h), Vector2(x, y + h)] return round_poly(points, r)
def test_constructor_cartesian2(self): v = Vector2(4, -4) self.assert_equals(5.6568542494923806, v.m) self.assert_equals(45.0, deg(v.a))
def test_rotate_positive(self): v = Vector2(4, -4) v.rotate(rad(-15)) self.assert_equals(30.0, deg(v.a))
def round_corner(p1, p2, p3, r): """ Calculate the Bezier-path segments defining a circular rounded corner of radius ``r`` of the two straight line segments `(p1,p2)` and `(p2,p3)`. Fit a circle of radius ``r`` into the triangle defined by points ``p1``, ``p2`` and ``p3`` so that segments `(p1,p2)` and `(p3,p2)` are tangents to the circle, then return the Bezier-path of the arc segment of radius ``r`` between points ``p1`` and ``p3``, facing ``p1``. """ # Optimization for the 90 degree case when the two segments are # parallel to the axes (~2.3x speedup). This is used frequently for # drawing rounded rectangles. d = 1e-5 s1_horiz = abs(p1.y - p2.y) < d s2_horiz = abs(p2.y - p3.y) < d s1_vert = abs(p1.x - p2.x) < d s2_vert = abs(p2.x - p3.x) < d # Handle 0, 180 degree and single point cases if (s1_horiz and s2_horiz) or (s1_vert and s2_vert): return [] if (s1_vert and s2_horiz) or (s1_horiz or s2_vert): if s1_horiz: dx = p2.x > p1.x dy = p3.y > p2.y else: dx = p3.x > p2.x dy = p2.y > p1.y # the conditions calculating the arc's angles can be derived # from the following table: # # dx dy sa da s1_horiz quadrant # --- ---- ---- ---- -------- -------- # 1 1 90 -90 1 1 # 0 1 0 -90 0 4 # 0 0 270 -90 1 3 # 1 0 180 -90 0 2 # 0 0 0 90 0 1 # 1 0 270 90 1 4 # 1 1 180 90 0 3 # 0 1 90 90 1 2 if ( (dx == dy) and s1_horiz or (dx != dy) and not s1_horiz): da = -90 else: da = 90 if s1_horiz: sa = 90 if dy else 270 else: sa = 180 if dx else 0 # Determine which quadrant should the arc be drawn in q = sa if da < 0: q -= 90 if q < 0: q += 360 r *= 2 x = p2.x y = p2.y if q == 0: x -= r elif q == 180: y -= r elif q == 270: x -= r y -= r return arcpath(x, y, r, r, sa, da) # General case (we've already handles the 0 and 180 degree cases) a1 = (p1 - p2).a a2 = (p3 - p2).a ang = a2 - a1 if ang > math.pi: ang -= 2 * math.pi elif ang < -math.pi: ang += 2 * math.pi # Length of the segment between p2 and the center of the circle aa = r / math.sin(ang / 2) # Calculate the center point of the circle c = Vector2(m=abs(aa), angle=a1 + ang / 2) + p2 # Distance from p2 to the tangent points dd = r / math.tan(ang / 2) # Tangent point of segments p1, p2 p = Vector2(m=abs(dd), angle=a1) + p2 # Tangent point of segments p3, p2 q = Vector2(m=abs(dd), angle=a2) + p2 # Calculate a third point on the circle halfway between the two # tangent points. This will be on the arc segment that should be # drawn, which is needed to be able to determine the correct # direction of the arc. m = Vector2(m=abs(aa) - r, angle=a1 + ang / 2) + p2 # Calculate the positions of points p, q and m on the circle # expressed as angles from the positive X axis. start_a = (p - c).a mid_a = (m - c).a end_a = (q - c).a # Start angle of the arc segment sa = start_a # Adjust arc length so the arc will always be drawn correctly in the # corner of the triangle. if start_a < mid_a < end_a or start_a > mid_a > end_a: da = end_a - start_a else: if start_a < end_a: da = -(start_a + 2 * math.pi - end_a) else: da = end_a + 2 * math.pi - start_a # Calculate Bezier points of the arc return arcpath(c.x - r, c.y - r, 2 * r, 2 * r, math.degrees(sa), math.degrees(da))
def test_rotate_negative(self): v = Vector2(4, -4) v.rotate(rad(30)) self.assert_equals(75.0, deg(v.a))
def slice_ellipse(x, y, w, h, ystep, rfactor=1.0): """ Calculate the intersections of an ellipse (or two elliptical arcs horizontally symmetrical to the center point) and a set of equidistant horizontal lines. ``x, y, w, h`` Bounding box of the ellipse. ``ystep`` Vertical distance between horizontal lines. ``rfactor`` If ``rfactor`` equals 1.0, a normal ellipse is used. If ``rfactor`` is greater than 1.0, two elliptical arcs horizontally symmetrical to the center point will be calculated (rfactor sets the 'flatness' of the arcs). The function returns the intersection point-pairs in the same format as ``slice_shape``. """ if ystep >= h: return [] points = [] # Store original x center for the final mirroring step cxo = x + w / 2. # Calculate the "ovalness" of the ellipse and adjust center point xscale = float(w) / h x += h * xscale / 2. # If rfactor > 1, not a full half-arc will be used but only a # smaller symmetrical segment of it. This is accomplished by # scaling the original radius up then adjusting the location of the # center point. r = h / 2. cx = x + r * (rfactor - 1) cy = y + r r *= rfactor # Special case when ystep > circle radius numlines = (int) (float(h) / ystep) if numlines == 1: numlines = 2 # Adjust the x coords of the points so that the top and bottommost # points of the arc are always in the same position, regardless of # the rfactor xcorr = x - (cx - r * xscale) - w / 2. # Center points vertically py = y + (h - (numlines - 1) * ystep) / 2. while numlines: px = cx - math.sqrt(r * r - pow(py - cy, 2)) * xscale + xcorr points.append(Vector2(px, py)) py += ystep numlines -= 1 # Mirror points on the y axis and return point-pair arrays for each # line return [[p, Vector2(2 * cxo - p.x, p.y)] for p in points]
def draw(self, node): """ Draw the node at its (x,y) anchor point. Relies on internal properties precalculated by precalc_node. """ E = self._eval_func(node) strokewidth = E('strokeWidth') drawstroke = strokewidth > 0 d = node._boxdepth _ctx.push() _ctx.translate(node.x, node.y) if drawstroke: # Set up clip path cx1 = cx6 = 0 cy2 = 0 cx3 = cx4 = cx1 + node.width + d cy5 = cy2 + node.height + d if self._vert_dir == self._horiz_dir: cy1 = d cx2 = d cy3 = 0 cy4 = cy3 + node.height cx5 = node.width cy6 = cy4 + d elif self._vert_dir != self._horiz_dir: cy1 = 0 cx2 = node.width cy3 = d cy4 = cy3 + node.height cx5 = d cy6 = cy4 - d outline = [ Vector2(cx1, cy1), Vector2(cx2, cy2), Vector2(cx3, cy3), Vector2(cx4, cy4), Vector2(cx5, cy5), Vector2(cx6, cy6) ] offs = geom.offset_poly(outline, strokewidth * .5) clippath = createpath(_ctx, offs) _ctx.beginclip(clippath) # Box drawing stuff if drawstroke: _ctx.stroke(E('strokeColor')) _ctx.strokewidth(strokewidth) else: _ctx.nostroke() if self._vert_dir == 1: y1 = d y2 = y1 - d dv = -d oy = y1 else: y1 = node.height y2 = y1 + d dv = d oy = 0 if self._horiz_dir == 1: x1 = node.width x2 = x1 + d dh = d ox = 0 else: x1 = d x2 = x1 - d dh = -d ox = x1 # Draw box path = _ctx.rect(ox, oy, node.width, node.height, draw=False) self._draw_gradient_shape(node, path, node.fillcolor) # Draw horizontal 3D side _ctx.beginpath(ox, y1) _ctx.lineto(ox + node.width, y1) _ctx.lineto(ox + node.width + dh, y2) _ctx.lineto(ox + dh, y2) _ctx.lineto(ox, y1) path = _ctx.endpath(draw=False) col = E('horizSideColor') self._draw_gradient_shape(node, path, col) # Draw vertical 3D side _ctx.beginpath(x1, oy) _ctx.lineto(x1, oy + node.height) _ctx.lineto(x2, oy + node.height + dv) _ctx.lineto(x2, oy + dv) _ctx.lineto(x1, oy) path = _ctx.endpath(draw=False) col = E('vertSideColor') self._draw_gradient_shape(node, path, col) tx = node._textxoffs + ox ty = node._textyoffs + oy _ctx.fill(node.fontcolor) self._drawtext(node, tx, ty) if drawstroke: _ctx.endclip() _ctx.pop()
def test_constructor_polar(self): v = Vector2(angle=rad(30), m=1) self.assert_equals(30.0, deg(v.a)) self.assert_equals(1.0, v.m) self.assert_equals(0.86602540378443, v.x) self.assert_equals(-0.5, v.y)
def test_scalar_multiply_left(self): v = Vector2(3, 2) m, a = v.m, v.a v = 2 * v self.assert_equals(a, v.a) self.assert_equals(m * 2, v.m)
def test_constructor_cartesian1(self): v = Vector2(3, -4) self.assert_equals(5, v.m) self.assert_equals(53.13010235415598, deg(v.a))
def test_scalar_divide_right(self): v = Vector2(3, 2) m, a = v.m, v.a v = v / 2 self.assert_equals(a, v.a) self.assert_equals(m / 2, v.m)
def test_scalar_divide_and_assign(self): v = Vector2(3, 2) m, a = v.m, v.a v /= 2 self.assert_equals(a, v.a) self.assert_equals(m / 2, v.m)
def test_scalar_multiply_and_assign(self): v = Vector2(3, 2) m, a = v.m, v.a v *= 2 self.assert_equals(a, v.a) self.assert_equals(m * 2, v.m)