def __init__(self, ske, yflip=True, expose_int=True): self.ske = ske self.m = None self.yflip = yflip self.expose_int = expose_int self.bbox = BoundBox() # prepare counters how many objects of each type we generate. self.stats = {} for s in ('pathList', 'pathString', 'objRect', 'objRoundedRect', 'objArc', 'objEllipse'): self.stats[s] = 0
def get_bound_box(obj: object) -> Optional[BoundBox]: bound_box = BoundBox() if (hasattr(obj, 'Shape') and obj.Shape and obj.Shape.BoundBox.isValid()): bound_box.add(obj.Shape.BoundBox) else: Console.PrintWarning( f'{obj.Label} has no Shape, calculating manual bounding box.') bound_box.add(Draft.get_bbox(obj)) if not bound_box.isValid(): Console.PrintError( f'{obj.Label} does not have a a valid bounding box.') return None return bound_box
def __init__(self): self.camera_state = { 'position': (20, 20, 20), 'height': 20, 'bound box': BoundBox(-20, 20, -20, 20, -20, 20) }
class SketchPathGen(PathGenerator): """ Generate XML code for a FreeCAD sketch. """ def __init__(self, ske, yflip=True, expose_int=True): self.ske = ske self.m = None self.yflip = yflip self.expose_int = expose_int self.bbox = BoundBox() # prepare counters how many objects of each type we generate. self.stats = {} for s in ('pathList', 'pathString', 'objRect', 'objRoundedRect', 'objArc', 'objEllipse'): self.stats[s] = 0 def _coord_from_svg(self, m=None): """ Use SVG properties set by InkSVG.handleViewBox() to define the transformation matrix from dpi'ish SVG space into metric FreeCAD space. """ xs = 1.0 if self._svg.docTransform is not None: xs = self._svg.docTransform[0][0] if abs(xs) < epsilon: xs = 1.0 # avoid divison by zero. ys = xs if self.yflip: ys = -ys if m is None: m = self.m m.move(0, -self._svg.docHeight, 0) m.scale(1 / xs, 1 / ys, 1) return m def _ellipse_vertices2d(self, C, P, Q): """ Compute two vertices of the ellipse, major and minor axis. Given two conjugated half diameters P, Q. Vectors are expected as FreeCAD Base.Vector() assuming that all Z=0. Using a computation derived from Rytz's construction as seen in https://en.wikipedia.org/wiki/Rytz%27s_construction. Returns (V1, V2, V3, V4) where V1-V4 is the diameter of the major axis and V2-V3 is the diameter of the minor axis. """ f0 = C f1 = P - C f2 = Q - C # det = (f1*f1 - f2*f2) / ( 2 * f1 * f2 ) # raise NaN, if flat. idet = (2 * f1 * f2) / (f1 * f1 - f2 * f2 ) # raise NaN, if flat or colinear. def math_cot(a): return 1 / math.tan(a) def p(t): return f0 + f1 * math.cos(t) + f2 * math.sin(t) # cot(2*t0) = det # tan(2*t0) = 1/det # tan(2*t0) = idet # 2*t0 = atan(idet) t0 = 0.5 * math.atan(idet) V1 = p(t0) V2 = p(t0 + 0.5 * math.pi) V3 = p(t0 - 0.5 * math.pi) V4 = p(t0 + math.pi) if (V1 - V4).Length < (V2 - V3).Length: # V1-V4 should be the major axis. (V1, V4, V2, V3) = (V2, V3, V1, V4) return (V1, V2, V3, V4) def _decompose_matrix2d(self, m=None): """ Decompose a 4x4 matrix into 2d translation vector, 2d scale vector, and rotation angle. Inspired by https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix """ if m is None: m = self.m a = m.A11 b = m.A12 xc = m.A14 # FIXME: check if correct in _matrix_from_svg() c = m.A21 d = m.A22 yc = m.A24 # FIXME: check if correct in _matrix_from_svg() sign_a = -1 if a < 0 else 1 sign_d = -1 if d < 0 else 1 sx = sign_a * math.sqrt(a * a + b * b) sy = sign_d * math.sqrt(c * c + d * d) sign = math.atan(-c / a) rad = math.acos(a / sx) deg = rad * 180. / math.pi if (deg > 90 and sign > 0) or (deg < 90 and sign < 0): deg = 360 - deg rad = deg / 180. * math.pi return (Base.Vector(xc, yc), Base.Vector(sx, sy), (rad, deg)) def _matrix_from_svg(self, svgmat, coordcvt=True): """ Convert a 2D matrix from SVG into a FreeCAD 3D Matrix. e.g. mat = [[0.9659258262890683, 0.25881904510252074, 0.0], [-0.25881904510252074, 0.9659258262890683, 0.0]] If coordcvt is True, then coordinate system conversion from SVG to FreeCAD is applied to the matrix. Otherwise only datatype conversion is performed. Returns: e.g. Matrix ((0.965926,0.258819,0,0),(-0.258819,0.965926,0,0),(0,0,1,0),(0,0,0,1)) """ # FIXME: is the handling of svgmat[*][2] correct? self.m = Base.Matrix(svgmat[0][0], svgmat[0][1], 0, svgmat[0][2], svgmat[1][0], svgmat[1][1], 0, svgmat[1][2]) if coordcvt: self._coord_from_svg() return self.m def _from_svg(self, x, y, m=None, bbox=True): """ Converts a 2D Vector from SVG into a FreeCAD 3D vector applying Base.Matrix m. """ if m is None: m = self.m v = m.multiply(Base.Vector(x, y, 0)) if bbox: self.bbox.add(v) return v def _same_point(self, p1, p2, eps=epsilon): if p1 is None or p2 is None: return True if abs(p1[0] - p2[0]) > eps: return False if abs(p1[1] - p2[1]) > eps: return False return True def _round_sigdigs(self, val, n=2): """ Rounds the value to at most n significant digits. 0.00426221 -> 0.0043 3.78 -> 3.8 997 -> 1000 994 -> 990 """ n = int(max(1, n)) exp10 = math.floor(math.log10(abs(val))) if val != 0 else 0 decimal_shifter = float(10**exp10) val = round(val / decimal_shifter, n - 1) # this final round is mathematically useless, but # avoids _round_sigdigs(0.000491) -> 0.0004900000000000001 return round(val * decimal_shifter, int(-exp10 + n - 1)) def _average_handle_length(self, sp): (tra, sca, rot) = self._decompose_matrix2d() sca = 0.5 * (abs(sca[0]) + abs(sca[1])) tot = 0 cnt = 0 for tri in sp: (h1, p, h2) = tri if not self._same_point(h1, p): tot += math.sqrt((h1[0] - p[0]) * (h1[0] - p[0]) + (h1[1] - p[1]) * (h1[1] - p[1])) cnt += 1 if not self._same_point(h2, p): tot += math.sqrt((h2[0] - p[0]) * (h2[0] - p[0]) + (h2[1] - p[1]) * (h2[1] - p[1])) cnt += 1 if (cnt > 0 and tot > 0): return self._round_sigdigs(sca * tot / cnt, 2) return self._round_sigdigs(sca * 10, 2) def pathString(self, d, node, mat): """ d is expected formatted as an svg path string here. d = "M 30.994048,129.93452 72.571427,88.357143 V 129.93452 H 127" means path = [ [ # handle_0 point handle_1 [[30.994048, 129.93452], [30.994048, 129.93452], [30.994048, 129.93452]], [[72.571427, 88.357143], [72.571427, 88.357143], [72.571427, 88.357143]], [[72.571427, 129.93452], [72.571427, 129.93452], [72.571427, 129.93452]], [[127.0, 129.93452], [127.0, 129.93452], [127.0, 129.93452]] ] ] """ self._matrix_from_svg(mat) path = cubicsuperpath.parsePath(d) for subpath in path: spt = SubPathTracker(self.ske, lambda a, b: self._same_point(a, b), self.expose_int, circ_r=0.1 * self._average_handle_length(subpath), debug=(verbose > 0)) # These are the off by one's: four points -> three lines -> two constraints. j = 0 while j < len(subpath) - 1: # lists of three, http://wiki.inkscape.org/wiki/index.php/Python_modules_for_extensions#cubicsuperpath.py (h0, p1, h1) = subpath[j] j = j + 1 while j < len(subpath): (h2, p2, h3) = subpath[j] if not self._same_point(p1, p2): break # no null-segments, please! j += 1 if j >= len(subpath): break # nothing left. spt.cubic(self._from_svg(p1[0], p1[1]), self._from_svg(h1[0], h1[1]), self._from_svg(h2[0], h2[1]), self._from_svg(p2[0], p2[1]), closing=(j + 1 == len(subpath))) self.stats['pathString'] += 1 # count subpaths def pathList(self, d, node, mat): """ d is expected as an [[cmd, [args]], ...] arrray """ print(d, node, mat) i = int(self.ske.GeometryCount) # 0 geo = [] geo.append(Part.LineSegment(Base.Vector(4, 8, 0), Base.Vector(9, 8, 0))) geo.append(Part.LineSegment(Base.Vector(9, 8, 0), Base.Vector(9, 2, 0))) self.ske.addGeometry(geo, False) print("GeometryCount changed from %d to %d" % (i, int(self.ske.GeometryCount))) print("not impl. simplePath: ", d, node, mat) def objRoundedRect(self, x, y, w, h, rx, ry, node, mat): """ Construct four arcs, one for each corner, and connect them with line segments, if space permits. Connect them directly otherwise. """ if rx == 0: rx = ry if ry == 0: ry = rx if rx < epsilon or ry < epsilon: return self.objRect(x, y, w, h, node, mat) if 2 * rx > w - epsilon: rx = 0.5 * ( w - epsilon ) # avoid Part.OCCError: Both points are equal" on LineSegment #12 if 2 * ry > h - epsilon: ry = 0.5 * (h - epsilon) if verbose > 0: print("objRoundedRect: ", x, y, w, h, rx, ry, node.get('id'), mat) self._matrix_from_svg(mat) i = int(self.ske.GeometryCount) ske.addGeometry( [ # construction outline of the box Part.LineSegment(self._from_svg(x, y), self._from_svg( x + w, y)), # 0 Part.LineSegment(self._from_svg(x + w, y), self._from_svg(x + w, y + h)), # 1 Part.LineSegment(self._from_svg(x + w, y + h), self._from_svg(x, y + h)), # 2 Part.LineSegment(self._from_svg(x, y + h), self._from_svg( x, y)), # 3 # construction four corners Part.LineSegment(self._from_svg(x + rx, y), self._from_svg(x + rx, y + ry)), # 4 Part.LineSegment(self._from_svg(x + rx, y + ry), self._from_svg(x, y + ry)), # 5 Part.LineSegment(self._from_svg(x + w - rx, y), self._from_svg(x + w - rx, y + ry)), # 6 Part.LineSegment(self._from_svg(x + w - rx, y + ry), self._from_svg(x + w, y + ry)), # 7 Part.LineSegment(self._from_svg(x + w - rx, y + h), self._from_svg(x + w - rx, y + h - ry)), # 8 Part.LineSegment(self._from_svg(x + w - rx, y + h - ry), self._from_svg(x + w, y + h - ry)), # 9 Part.LineSegment(self._from_svg(x + rx, y + h), self._from_svg(x + rx, y + h - ry)), # 10 Part.LineSegment(self._from_svg(x + rx, y + h - ry), self._from_svg(x, y + h - ry)) # 11 ], True) self.ske.addConstraint([ ## outer construction corners Sketcher.Constraint('Coincident', i + 0, 2, i + 1, 1), Sketcher.Constraint('Coincident', i + 1, 2, i + 2, 1), Sketcher.Constraint('Coincident', i + 2, 2, i + 3, 1), Sketcher.Constraint('Coincident', i + 3, 2, i + 0, 1), ## inner construction corners Sketcher.Constraint('Coincident', i + 4, 2, i + 5, 1), Sketcher.Constraint('Coincident', i + 6, 2, i + 7, 1), Sketcher.Constraint('Coincident', i + 8, 2, i + 9, 1), Sketcher.Constraint('Coincident', i + 10, 2, i + 11, 1), ## inner construction equality Sketcher.Constraint('Equal', i + 4, i + 6), Sketcher.Constraint('Equal', i + 4, i + 8), Sketcher.Constraint('Equal', i + 4, i + 10), Sketcher.Constraint('Equal', i + 5, i + 7), Sketcher.Constraint('Equal', i + 5, i + 9), Sketcher.Constraint('Equal', i + 5, i + 11), ## corner cube outlines construction Sketcher.Constraint('PointOnObject', i + 4, 1, i + 0), Sketcher.Constraint('PointOnObject', i + 5, 2, i + 3), Sketcher.Constraint('PointOnObject', i + 6, 1, i + 0), Sketcher.Constraint('PointOnObject', i + 7, 2, i + 1), Sketcher.Constraint('PointOnObject', i + 8, 1, i + 2), Sketcher.Constraint('PointOnObject', i + 9, 2, i + 1), Sketcher.Constraint('PointOnObject', i + 10, 1, i + 2), Sketcher.Constraint('PointOnObject', i + 11, 2, i + 3), ## horizontal construction Sketcher.Constraint('Parallel', i, i + 2), Sketcher.Constraint('Parallel', i, i + 5), Sketcher.Constraint('Parallel', i, i + 7), Sketcher.Constraint('Parallel', i, i + 9), Sketcher.Constraint('Parallel', i, i + 11), ## vertical construction Sketcher.Constraint('Parallel', i + 1, i + 3), Sketcher.Constraint('Parallel', i + 1, i + 4), Sketcher.Constraint('Parallel', i + 1, i + 6), Sketcher.Constraint('Parallel', i + 1, i + 8), Sketcher.Constraint('Parallel', i + 1, i + 10) ]) ske.addGeometry([ # sides of the rect Part.LineSegment(self._from_svg(x + rx, y), self._from_svg(x + w - rx, y)), # 12 Part.LineSegment(self._from_svg(x + w, y + ry), self._from_svg(x + w, y + h - ry)), # 13 Part.LineSegment(self._from_svg(x + w - rx, y + h), self._from_svg(x + rx, y + h)), # 14 Part.LineSegment(self._from_svg(x, y + h - ry), self._from_svg(x, y + ry)) # 15 ]) # arcs top left, top right, botton right, bottom left. # circles rotate counter clockwise. pi/2 is north, pi is west, 2*pi is east a_tl = self.objArc("", x + rx, y + ry, rx, ry, -2 / 2. * math.pi, -1 / 2. * math.pi, False, node, mat) a_tr = self.objArc("", x - rx + w, y + ry, rx, ry, -1 / 2. * math.pi, 0 / 2. * math.pi, False, node, mat) a_br = self.objArc("", x - rx + w, y - ry + h, rx, ry, 0 / 2. * math.pi, 1 / 2. * math.pi, False, node, mat) a_bl = self.objArc("", x + rx, y - ry + h, rx, ry, 1 / 2. * math.pi, 2 / 2. * math.pi, False, node, mat) if True: self.ske.addConstraint([ # connect the corners to the edges. smooth Sketcher.Constraint('Tangent', a_tl[1][0], a_tl[1][1], i + 12, 1), Sketcher.Constraint('Tangent', i + 12, 2, a_tr[0][0], a_tr[0][1]), Sketcher.Constraint('Tangent', a_tr[1][0], a_tr[1][1], i + 13, 1), Sketcher.Constraint('Tangent', i + 13, 2, a_br[0][0], a_br[0][1]), Sketcher.Constraint('Tangent', a_br[1][0], a_br[1][1], i + 14, 1), Sketcher.Constraint('Tangent', i + 14, 2, a_bl[0][0], a_bl[0][1]), Sketcher.Constraint('Tangent', a_bl[1][0], a_bl[1][1], i + 15, 1), Sketcher.Constraint('Tangent', i + 15, 2, a_tl[0][0], a_bl[0][1]), ]) if False: self.ske.addConstraint([ # stitch the rounded rect to the construction grid Sketcher.Constraint('Coincident', i + 12, 1, i + 4, 1), Sketcher.Constraint('Coincident', i + 12, 2, i + 6, 1), Sketcher.Constraint('Coincident', i + 13, 1, i + 7, 2), Sketcher.Constraint('Coincident', i + 13, 2, i + 9, 2), Sketcher.Constraint('Coincident', i + 14, 1, i + 8, 1), Sketcher.Constraint('Coincident', i + 14, 2, i + 10, 1), Sketcher.Constraint('Coincident', i + 15, 1, i + 11, 2), Sketcher.Constraint('Coincident', i + 15, 2, i + 5, 2) ]) if False and a_tr[3] is not None: # ArcOfCirle has no majAxis self.ske.addConstraint([ # make all major axis parallel, and same length Sketcher.Constraint('Parallel', a_tr[3], a_tl[3]), Sketcher.Constraint('Equal', a_tr[3], a_tl[3]), Sketcher.Constraint('Parallel', a_tr[3], a_bl[3]), Sketcher.Constraint('Equal', a_tr[3], a_bl[3]), Sketcher.Constraint('Parallel', a_tr[3], a_br[3]), # Sketcher.Constraint('Equal', a_tr[3], a_br[3]) # makes everything immobole ]) self.stats['objRect'] += 1 def objRect(self, x, y, w, h, node, mat): self._matrix_from_svg(mat) i = int(self.ske.GeometryCount) self.ske.addGeometry([ Part.LineSegment(self._from_svg(x, y), self._from_svg(x + w, y)), Part.LineSegment(self._from_svg(x + w, y), self._from_svg(x + w, y + h)), Part.LineSegment(self._from_svg(x + w, y + h), self._from_svg(x, y + h)), Part.LineSegment(self._from_svg(x, y + h), self._from_svg(x, y)) ], False) self.ske.addConstraint([ Sketcher.Constraint('Coincident', i + 0, 2, i + 1, 1), Sketcher.Constraint('Coincident', i + 1, 2, i + 2, 1), Sketcher.Constraint('Coincident', i + 2, 2, i + 3, 1), Sketcher.Constraint('Coincident', i + 3, 2, i + 0, 1), Sketcher.Constraint('Parallel', i + 2, i + 0), Sketcher.Constraint('Parallel', i + 3, i + 1) ]) self.stats['objRect'] += 1 def objEllipse(self, cx, cy, rx, ry, node, mat): """ We distinguish two cases. If it looks like a circle (after transformation), we produce a Circle, else we produce an Ellipse. The difference is clearly visible as we exposeInternalGeometry() of the Ellipse. """ ### CAUTION: Keep in sync with objArc() below. self._matrix_from_svg(mat) c = self._from_svg(cx, cy, bbox=False) ori = self._from_svg(0, 0, bbox=False) vrx = self._from_svg(rx, 0, bbox=False) - ori vry = self._from_svg(0, ry, bbox=False) - ori i = int(self.ske.GeometryCount) if abs(vrx.Length - vry.Length) < epsilon: # it is a circle. self.bbox.add(c + vrx + vry) self.bbox.add(c - vrx - vry) self.ske.addGeometry([ Part.Circle(Center=c, Normal=Base.Vector(0, 0, 1), Radius=vrx.Length) ]) self.stats['objEllipse'] += 1 else: # major axis is defined by Center and S1, # major radius is the distance between Center and S1, # minor radius is the distance between S2 and the major axis. s1 = self._from_svg(cx + rx, cy, bbox=False) s2 = self._from_svg(cx, cy + ry, bbox=False) (V1, V2, V3, V4) = self._ellipse_vertices2d(c, s1, s2) self.bbox.add(V1) self.bbox.add(V2) self.bbox.add(V3) self.bbox.add(V4) self.ske.addGeometry([ Part.Ellipse(S1=V1, S2=V2, Center=c), ]) if self.expose_int: self.ske.exposeInternalGeometry(i) self.stats['objEllipse'] += 1 def objArc(self, d, cx, cy, rx, ry, st, en, closed, node, mat): """ We ignore the path d, and produce a nice arc object. We distinguish two cases. If it looks like a circle, we produce ArcOfCircle, else we produce ArcOfEllipse. To find the arc end points, we use the value() property of the Circle and Ellipse objects. With Circle we have to take care of mirror and rotation ourselves. If closed, we connect the two ends to the center with lines. The connections are secured with constraints. Radii are currently not secured with constraints. """ ### CAUTION: Keep in sync with objEllipse() above. # print("objArc: st,en,closed", st, en, closed) self._matrix_from_svg(mat) c = self._from_svg(cx, cy, bbox=False) ori = self._from_svg(0, 0, bbox=False) vrx = self._from_svg(rx, 0, bbox=False) - ori vry = self._from_svg(0, ry, bbox=False) - ori i = self.ske.GeometryCount (st_idx, en_idx) = (1, 2) majAxisIdx = None if abs(vrx.Length - vry.Length) < epsilon: # it is a circle. self.bbox.add(c + vrx + vry) self.bbox.add(c - vrx - vry) ce = Part.Circle(Center=c, Normal=Base.Vector(0, 0, 1), Radius=vrx.Length) # Circles are immune to rotation and mirorring. Apply this manually. if self.yflip: (st, en) = (-en, -st) ## coord system is mirrored. else: (st, en) = (en, st) ## hmm. print("FIXME: ArcOfCircle() with yflip=False needs debugging.") r = Base.Matrix() r.rotateZ(st) pst = r.multiply(vrx) st = pst.getAngle(Base.Vector( 1, 0, 0)) # ce.rotateZ() is a strange beast. pst = pst + c r = Base.Matrix() r.rotateZ(en) pen = r.multiply(vrx) en = pen.getAngle(Base.Vector( 1, 0, 0)) # ce.rotateZ() is a strange beast. pen = pen + c self.ske.addGeometry([Part.ArcOfCircle(ce, st, en)]) self.stats['objArc'] += 1 else: # major axis is defined by Center and S1, # major radius is the distance between Center and S1, # minor radius is the distance between S2 and the major axis. s1 = self._from_svg(cx + rx, cy, bbox=False) s2 = self._from_svg(cx, cy + ry, bbox=False) (V1, V2, V3, V4) = self._ellipse_vertices2d(c, s1, s2) self.bbox.add(V1) self.bbox.add(V2) self.bbox.add(V3) self.bbox.add(V4) i = int(self.ske.GeometryCount) ce = Part.Ellipse(S1=V1, S2=V2, Center=c) self.ske.addGeometry([Part.ArcOfEllipse(ce, st, en)]) if self.expose_int: self.ske.exposeInternalGeometry(i) majAxisIdx = i + 1 # CAUTION: is that a safe assumption? self.stats['objArc'] += 1 ## CAUTION: with yflip=True sketcher reverses the endpoints of ## an ArcOfEllipse to: en=1, st=2 ## ArcOfCircle seems unaffected. if self.yflip: (st_idx, en_idx) = (2, 1) r = Base.Matrix() r.rotateZ(st) pst = r.multiply(vrx) + c r = Base.Matrix() r.rotateZ(en) pen = r.multiply(vrx) + c j = self.ske.GeometryCount if closed: self.ske.addGeometry([ Part.LineSegment(ce.value(en), c), Part.LineSegment(c, ce.value(st)) ]) if True: # when debugging deformations, switch off constriants first. self.ske.addConstraint([ Sketcher.Constraint('Coincident', i + 0, en_idx, j + 0, 1), # arc with line Sketcher.Constraint('Coincident', j + 1, 2, i + 0, st_idx), # line with arc Sketcher.Constraint('Coincident', j + 0, 2, j + 1, 1), # line with line Sketcher.Constraint('Coincident', j + 0, 2, i + 0, 3) ]) # line with center if False: # some debugging circles. self.ske.addGeometry( [ # Part.Circle(Center=pst, Normal=Base.Vector(0,0,1), Radius=2), # Part.Circle(Center=pen, Normal=Base.Vector(0,0,1), Radius=3), # Part.Circle(Center=ce.value(st), Normal=Base.Vector(0,0,1), Radius=4), Part.Circle(Center=ce.value(en), Normal=Base.Vector(0, 0, 1), Radius=5) ], True) # we return the start, end and center points, as triple (sketcher_index, sketcher_index_point, Vector) return ((i + 0, st_idx, ce.value(st)), (i + 0, en_idx, ce.value(en)), (i + 0, 3, c), majAxisIdx)
def BoundBox(self): return BoundBox(CENTER, VEC(1.0, 1.0, 1.0))
def BoundBox(self): minBB = CENTER maxBB = VEC(1.0, 1.0, 1.0) return BoundBox(minBB, maxBB)
def readBoundBox(data, offset, log): a, i = getFloat32A(data, offset, 6) log.write("BOUND-BOX: (%g,%g,%g)-(%g,%g,%g)\n\n" % (a[0], a[1], a[2], a[3], a[4], a[5])) return BoundBox(VEC(a[0:3]), VEC(a[3:])), i