def _makeNotchedEllipse(center, ellipse, start_theta, thickness, notches, parent, invertNotches): start_theta += pi # rotate 180 degrees to put the lid on the topside ell_radius = Coordinate(ellipse.x_radius, ellipse.y_radius) ell_radius_t = ell_radius + Coordinate(thickness, thickness) theta = ellipse.theta_from_dist(start_theta, notches[0]) ell_point = center + ellipse.coordinate_at_theta(theta) prev_offset = ellipse.tangent(theta) * thickness p = svg.Path() p.move_to(ell_point, absolute=True) for n in range(len(notches) - 1): theta = ellipse.theta_from_dist(start_theta, notches[n + 1]) ell_point = center + ellipse.coordinate_at_theta(theta) notch_offset = ellipse.tangent(theta) * thickness notch_point = ell_point + notch_offset if (n % 2 == 0) != invertNotches: p.arc_to(ell_radius, ell_point, absolute=True) prev_offset = notch_offset else: p.line_to(prev_offset) p.arc_to(ell_radius_t, notch_point, absolute=True) p.line_to(-notch_offset) p.path(parent)
def test_t_to_theta(self): #arc = EllipticArc(C10_0, C0_10, 10, 10, 45) # quarter circle, axis rotated by 45 degree s = Coordinate(20, 0) s.t += pi / 4 e = Coordinate(0, 10) e.t += pi / 4 #arc = EllipticArc(C10_0, C0_10, 10, 10, 45) # quarter circle, axis rotated by 45 degree arc = EllipticArc(s, e, 20, 10, 45) # quarter circle, axis rotated by 45 degree self.assertEqual(arc.t_to_theta(0), 0) self.assertEqual(arc.t_to_theta(1), pi / 2) self.assertEqual(arc.t_to_theta(0.5), pi / 4)
def test_real_world(self): rx, ry = 136.87567000000001, 46.467018000000003 rot_deg = 11.8225 c1 = Coordinate(400.32499000000001, 546.23693000000003) c2 = Coordinate(256.83267000000001, 563.67507999999998) c3 = Coordinate(132.38073, 490.15062999999998) c4 = Coordinate(275.87304, 472.71246000000002) center = Coordinate(300, 400) x = 200 y = 100 rot_deg = 3 rot_rad = rot_deg * pi / 180 right = Coordinate(x, 0) top = Coordinate(0, y) left = Coordinate(-x, 0) bottom = Coordinate(0, -y) right.t += rot_rad top.t += rot_rad left.t += rot_rad bottom.t += rot_rad self.rw_tests(right, top, left, bottom, x, y, rot_deg, True) self.rw_tests(right, bottom, left, top, x, y, rot_deg, False)
def _makeCurvedSurface(topLeft, w, h, cutSpacing, hCutCount, thickness, parent, invertNotches=False, centralRib=False): width = Coordinate(w, 0) height = Coordinate(0, h) wCutCount = int(floor(w / cutSpacing)) if wCutCount % 2 == 0: wCutCount += 1 # make sure we have an odd number of cuts xCutDist = w / wCutCount xSpacing = Coordinate(xCutDist, 0) ySpacing = Coordinate(0, cutSpacing) cut = height / hCutCount - ySpacing plateThickness = Coordinate(0, thickness) notchEdges = [] topHCuts = [] bottomHCuts = [] p = svg.Path() for cutIndex in range(wCutCount): if (cutIndex % 2 == 1) != invertNotches: # make a notch here inset = plateThickness else: inset = Coordinate(0, 0) # A-column of cuts aColStart = topLeft + xSpacing * cutIndex notchEdges.append((aColStart - topLeft).x) if cutIndex > 0: # no cuts at x == 0 p.move_to(aColStart, True) p.line_to(cut / 2) for j in range(hCutCount - 1): pos = aColStart + cut / 2 + ySpacing + (cut + ySpacing) * j p.move_to(pos, True) p.line_to(cut) p.move_to(aColStart + height - cut / 2, True) p.line_to(cut / 2) # B-column of cuts, offset by half the cut length; these cuts run in the opposite direction bColStart = topLeft + xSpacing * cutIndex + xSpacing / 2 for j in reversed(range(hCutCount)): end = bColStart + ySpacing / 2 + (cut + ySpacing) * j start = end + cut if centralRib and hCutCount % 2 == 0 and cutIndex % 2 == 1: holeTopLeft = start + (ySpacing - plateThickness - xSpacing) / 2 if j == hCutCount // 2 - 1: start -= plateThickness / 2 p.move_to(holeTopLeft + plateThickness + xSpacing, True) p.line_to(-xSpacing) p.move_to(holeTopLeft, True) p.line_to(xSpacing) elif j == hCutCount // 2: end += plateThickness / 2 if j == 0: # first row end += inset elif j == hCutCount - 1: # last row start -= inset p.move_to(start, True) p.line_to(end, True) #horizontal cuts (should be done last) topHCuts.append((aColStart + inset, aColStart + inset + xSpacing)) bottomHCuts.append((aColStart + height - inset, aColStart + height - inset + xSpacing)) # draw the outline for c in reversed(bottomHCuts): p.move_to(c[1], True) p.line_to(c[0], True) p.move_to(topLeft + height, True) p.line_to(-height) for c in topHCuts: p.move_to(c[0], True) p.line_to(c[1], True) p.move_to(topLeft + width, True) p.line_to(height) group = svg.group(parent) p.path(group) notchEdges.append(w) return notchEdges
def test_coordinate_at_theta(self): ell = Ellipse(12,8) self.assertEqual(ell.coordinate_at_theta(0), Coordinate(12, 0), 'coordinate at angle 0') self.assertTrue(Coordinate(0, 8).close_enough_to(ell.coordinate_at_theta(pi/2)), 'coordinate at angle pi/2') self.assertTrue(Coordinate(-12, 0).close_enough_to(ell.coordinate_at_theta(pi)), 'coordinate at angle pi') self.assertTrue(Coordinate(0, -8).close_enough_to(ell.coordinate_at_theta(3*pi/2)), 'coordinate at angle 3*pi/2')
def __init__(self, start, end, rx, ry, axis_rot, pos_dir=True, large_arc=False): self.rx = rx self.ry = ry # calculate ellipse center # the center is on two ellipses one with its center at the start point, the other at the end point # for simplicity take the one ellipse at the origin and the other with offset (tx, ty), # find the center and translate back to the original offset at the end axis_rot *= pi / 180 # convert to radians # start and end are mutable objects, copy to avoid modifying them r_start = copy.copy(start) r_end = copy.copy(end) r_start.t -= axis_rot r_end.t -= axis_rot end_o = r_end - r_start # offset end vector tx = end_o.x ty = end_o.y # some helper variables for the intersection points # used sympy to come up with the equations ff = (rx**2 * ty**2 + ry**2 * tx**2) cx = rx**2 * ry * tx * ty**2 + ry**3 * tx**3 cy = rx * ty * ff sx = rx * ty * sqrt(4 * rx**4 * ry**2 * ty**2 - rx**4 * ty**4 + 4 * rx**2 * ry**4 * tx**2 - 2 * rx**2 * ry**2 * tx**2 * ty**2 - ry**4 * tx**4) sy = ry * tx * sqrt( -ff * (-4 * rx**2 * ry**2 + rx**2 * ty**2 + ry**2 * tx**2)) # intersection points c1 = Coordinate((cx - sx) / (2 * ry * ff), (cy + sy) / (2 * rx * ff)) c2 = Coordinate((cx + sx) / (2 * ry * ff), (cy - sy) / (2 * rx * ff)) if end_o.cross_norm(c1 - r_start) < 0: # c1 is to the left of end_o left = c1 right = c2 else: left = c2 right = c1 if pos_dir != large_arc: #center should be on the left of end_o center_o = left else: #center should be on the right of end_o center_o = right #re-use ellipses with same rx, ry to save some memory if (rx, ry) in self.ell_dict: self.ellipse = self.ell_dict[(rx, ry)] else: self.ellipse = Ellipse(rx, ry) self.ell_dict[(rx, ry)] = self.ellipse self.start = start self.end = end self.axis_rot = axis_rot self.pos_dir = pos_dir self.large_arc = large_arc self.start_theta = self.ellipse.theta_at_angle((-center_o).t) self.end_theta = self.ellipse.theta_at_angle((end_o - center_o).t) # translate center back to original offset center_o.t += axis_rot self.center = center_o + start
def test_pathpoint_at_t(self): arc = EllipticArc(C10_0, Coordinate(-10, 0), 10, 20, 0) self.assertTrue( arc.pathpoint_at_t(0.5).coord.close_enough_to(Coordinate(0, 20)), 'coordinate at 90') self.assertAlmostEqual(arc.tangent(0.5).t, pi / 2)
def test_eq(self): self.assertEqual(Coordinate(1, 2), Coordinate(1, 2))
import unittest from inkscape_helper.Effect import Effect from inkscape_helper.EllipticArc import EllipticArc from inkscape_helper.Coordinate import Coordinate from math import pi C00 = Coordinate(0, 0) C20_0 = Coordinate(20, 0) C0_10 = Coordinate(0, 10) C10_0 = Coordinate(10, 0) C20_10 = Coordinate(20, 10) def between(mid, end1, end2): small = min(end1, end2) large = max(end1, end2) return small < mid < large class TestEllipticArc(unittest.TestCase, Effect): def test_elliptic_arc_center(self): arc = EllipticArc(C20_0, C0_10, 20, 10, 0) self.assertTrue(arc.center.close_enough_to(C00), 'ellipse center') large_arc = EllipticArc(C20_0, C0_10, 20, 10, 0, large_arc=True) self.assertEqual(large_arc.center, C20_10, 'ellipse center large arc') neg_arc = EllipticArc(C20_0, C0_10, 20, 10, 0, pos_dir=False) self.assertEqual(neg_arc.center, C20_10, 'ellipse center neg dir') def test_t_to_theta(self):
def coordinate_at_theta(self, theta): """Coordinate of the point at theta.""" return Coordinate(self.x_radius * cos(theta), self.y_radius * sin(theta))
def tangent(self, theta): angle = self.theta_at_angle(theta) return Coordinate(cos(angle), sin(angle))
def test_div(self): quot = C11 / 2 self.assertEqual(quot, Coordinate(.5, .5))
def test_rmul(self): prod = 2 * C11 self.assertEqual(prod, Coordinate(2, 2))
def test_mul(self): prod = C11 * 2 self.assertEqual(prod, Coordinate(2, 2))
def effect(self): """ Draws as basic elliptical box, based on provided parameters """ # input sanity check error = False if min(self.options.height, self.options.width, self.options.depth) == 0: eff.errormsg('Error: Dimensions must be non zero') error = True if self.options.cut_nr < 1: eff.errormsg('Error: Number of cuts should be at least 1') error = True if (self.options.central_rib_lid or self.options.central_rib_body ) and self.options.cut_nr % 2 == 1: eff.errormsg( 'Error: Central rib is only valid with an even number of cuts') error = True if self.options.unit not in self.knownUnits: eff.errormsg('Error: unknown unit. ' + self.options.unit) error = True if error: exit() # convert units unit = self.options.unit H = self.svg.unittouu(str(self.options.height) + unit) W = self.svg.unittouu(str(self.options.width) + unit) D = self.svg.unittouu(str(self.options.depth) + unit) thickness = self.svg.unittouu(str(self.options.thickness) + unit) cutSpacing = self.svg.unittouu(str(self.options.cut_dist) + unit) cutNr = self.options.cut_nr doc_root = self.document.getroot() layer = svg.layer(doc_root, 'Elliptical Box') ell = Ellipse(W / 2, H / 2) #body and lid lidAngleRad = self.options.lid_angle * 2 * pi / 360 lid_start_theta = ell.theta_at_angle(pi / 2 - lidAngleRad / 2) lid_end_theta = ell.theta_at_angle(pi / 2 + lidAngleRad / 2) lidLength = ell.dist_from_theta(lid_start_theta, lid_end_theta) bodyLength = ell.dist_from_theta(lid_end_theta, lid_start_theta) # do not put elements right at the edge of the page xMargin = 3 yMargin = 3 bottom_grp = svg.group(layer) top_grp = svg.group(layer) bodyNotches = _makeCurvedSurface(Coordinate(xMargin, yMargin), bodyLength, D, cutSpacing, cutNr, thickness, bottom_grp, False, self.options.central_rib_body) lidNotches = _makeCurvedSurface(Coordinate(xMargin, D + 2 * yMargin), lidLength, D, cutSpacing, cutNr, thickness, top_grp, not self.options.invert_lid_notches, self.options.central_rib_lid) # create elliptical sides sidesGrp = svg.group(layer) elCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + H / 2 + thickness + 3 * yMargin) # indicate the division between body and lid p = svg.Path() if self.options.invert_lid_notches: p.move_to(elCenter + ell.coordinate_at_theta(lid_start_theta + pi), True) p.line_to(elCenter, True) p.line_to(elCenter + ell.coordinate_at_theta(lid_end_theta + pi), True) else: angleA = ell.theta_from_dist(lid_start_theta, lidNotches[1]) angleB = ell.theta_from_dist(lid_start_theta, lidNotches[-2]) p.move_to(elCenter + ell.coordinate_at_theta(angleA + pi), True) p.line_to(elCenter, True) p.line_to(elCenter + ell.coordinate_at_theta(angleB + pi), True) _makeNotchedEllipse(elCenter, ell, lid_end_theta, thickness, bodyNotches, sidesGrp, False) _makeNotchedEllipse(elCenter, ell, lid_start_theta, thickness, lidNotches, sidesGrp, not self.options.invert_lid_notches) p.path(sidesGrp, greenStyle) # ribs if self.options.central_rib_lid or self.options.central_rib_body: innerRibCenter = Coordinate( xMargin + thickness + W / 2, 2 * D + 1.5 * (H + 2 * thickness) + 4 * yMargin) innerRibGrp = svg.group(layer) outerRibCenter = Coordinate( 2 * xMargin + 1.5 * (W + 2 * thickness), 2 * D + 1.5 * (H + 2 * thickness) + 4 * yMargin) outerRibGrp = svg.group(layer) if self.options.central_rib_lid: _makeNotchedEllipse(innerRibCenter, ell, lid_start_theta, thickness, lidNotches, innerRibGrp, False) _makeNotchedEllipse(outerRibCenter, ell, lid_start_theta, thickness, lidNotches, outerRibGrp, True) if self.options.central_rib_body: spacer = Coordinate(0, 10) _makeNotchedEllipse(innerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, innerRibGrp, False) _makeNotchedEllipse(outerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, outerRibGrp, True) if self.options.central_rib_lid or self.options.central_rib_body: svg.text(sidesGrp, elCenter, 'side (duplicate this)') svg.text(innerRibGrp, innerRibCenter, 'inside rib') svg.text(outerRibGrp, outerRibCenter, 'outside rib')
import unittest from inkscape_helper.Effect import Effect from inkscape_helper.PathSegment import PathSegment from inkscape_helper.BezierCurve import BezierCurve from inkscape_helper.Line import Line from inkscape_helper.Coordinate import Coordinate from math import sqrt C00 = Coordinate(0, 0) C10 = Coordinate(1, 0) C01 = Coordinate(0, 1) C11 = Coordinate(1, 1) class TestPathSegment(unittest.TestCase, Effect): #def setUp(self): def test_quadratic_bezier(self): quadratic = BezierCurve([C10, C11, C01]) self.assertEqual(quadratic.pathpoint_at_t(0).coord, C10, 'start point') self.assertEqual(quadratic.pathpoint_at_t(1).coord, C01, 'end point') def test_cubic_bezier(self): cubic = BezierCurve([C10, C11, C00, C01]) self.assertEqual(cubic.pathpoint_at_t(0).coord, C10, 'start point') self.assertEqual(cubic.pathpoint_at_t(1).coord, C01, 'end point') def test_bezier_length(self): line = BezierCurve([C00, C10, C10 * 2]) self.assertEqual(line.pathpoint_at_t(0.5).coord, C10, 'midpoint by t')
def setUp(self): self.CX = Coordinate(1, 0) self.CY = Coordinate(0, 1)