from FreeCAD import Base, Vector, Matrix, Placement, Rotation, Draft import Part from enum import Enum import math # These names are silly, class should be named Axis and its values X, Y, Z class MeasurementDirection(Enum): width = 1 # X height = 2 # Z depth = 3 # Y # X and Y rotation factors class LineDirection(Enum): right = (1, 0, 0, 1) down = (0, -1, 1, 0) left = (-1, 0, 0, -1) up = (0, 1, -1, 0) def __init__(self, x_x, x_y, y_x, y_y): self.x_x = x_x self.x_y = x_y self.y_x = y_x self.y_y = y_y # Configuration for each line in a box side class SideSize(Enum): large = (False, True, False, True, False, True, False, True) medium = (True, True, False, False, True, True, False, False) small = (True, False, True, False, True, False, True, False) def __init__(self, rightInv, rightIncludeStart, downInv, downIncludeStart, leftInv, leftIncludeStart, upInv, upIncludeStart): self.rightInv = rightInv self.rightIncludeStart = rightIncludeStart self.downInv = downInv self.downIncludeStart = downIncludeStart self.leftInv = leftInv self.leftIncludeStart = leftIncludeStart self.upInv = upInv self.upIncludeStart = upIncludeStart def inv(self, direction): if direction == LineDirection.right: return self.rightInv elif direction == LineDirection.down: return self.downInv elif direction == LineDirection.left: return self.leftInv elif direction == LineDirection.up: return self.upInv else: raise Exception("Unknown direction: " + repr(direction)) def includeStart(self, direction): if direction == LineDirection.right: return self.rightIncludeStart elif direction == LineDirection.down: return self.downIncludeStart elif direction == LineDirection.left: return self.leftIncludeStart elif direction == LineDirection.up: return self.upIncludeStart else: raise Exception("Unknown direction: " + repr(direction)) class SideType(Enum): front = 1 back = 2 left = 3 right = 4 top = 5 bottom = 6 class BoxSide(object): def __init__(self, cfg, title, directionX, directionY, sideSize, extrudeVector, sideTypesByLineDirections): self.cfg = cfg self.title = title self.directionX = directionX self.directionY = directionY self.sideSize = sideSize self.extrudeVector = Vector(extrudeVector) self.enabled = True self.sideTypesByLineDirections = sideTypesByLineDirections self._thickness = 10 def left(self, lineDirection): sideType = self.sideTypesByLineDirections[lineDirection][0] return self.cfg.sides[sideType] def opposite(self, lineDirection): sideType = self.sideTypesByLineDirections[lineDirection][1] return self.cfg.sides[sideType] def right(self, lineDirection): sideType = self.sideTypesByLineDirections[lineDirection][2] return self.cfg.sides[sideType] @property def thickness(self): return self._thickness @thickness.setter def thickness(self, thickness): self._thickness = thickness self.cfg.calculate() class NotchConfig(object): def __init__(self, count, size): self.count = count self.size = size def __str__(self): return "count = " + str(self.count) + ", size = " + str(self.size) class BoxCfg(object): def __init__(self): # yapf: disable self.sides = { SideType.front: BoxSide(self, 'Front', MeasurementDirection.width, MeasurementDirection.height, SideSize.large, (0, -1, 0), {LineDirection.right: (SideType.left, SideType.top, SideType.right), LineDirection.down: (SideType.top, SideType.right, SideType.bottom), LineDirection.left: (SideType.right, SideType.bottom, SideType.left), LineDirection.up: (SideType.bottom, SideType.left, SideType.top)}), SideType.back: BoxSide(self, 'Back', MeasurementDirection.width, MeasurementDirection.height, SideSize.large, (0, 1, 0), {LineDirection.right: (SideType.right, SideType.top, SideType.left), LineDirection.down: (SideType.top, SideType.left, SideType.bottom), LineDirection.left: (SideType.left, SideType.bottom, SideType.right), LineDirection.up: (SideType.bottom, SideType.right, SideType.top)}), SideType.left: BoxSide(self, 'Left', MeasurementDirection.depth, MeasurementDirection.height, SideSize.small, (-1, 0, 0), {LineDirection.right: (SideType.front, SideType.top, SideType.back), LineDirection.down: (SideType.top, SideType.back, SideType.bottom), LineDirection.left: (SideType.back, SideType.bottom, SideType.front), LineDirection.up: (SideType.bottom, SideType.front, SideType.top)}), SideType.right: BoxSide(self, 'Right', MeasurementDirection.depth, MeasurementDirection.height, SideSize.small, (1, 0, 0), {LineDirection.right: (SideType.front, SideType.top, SideType.back), LineDirection.down: (SideType.top, SideType.back, SideType.bottom), LineDirection.left: (SideType.back, SideType.bottom, SideType.front), LineDirection.up: (SideType.bottom, SideType.front, SideType.top)}), SideType.top: BoxSide(self, 'Top', MeasurementDirection.width, MeasurementDirection.depth, SideSize.medium, (0, 0, 1), {LineDirection.right: (SideType.left, SideType.back, SideType.right), LineDirection.down: (SideType.back, SideType.right, SideType.front), LineDirection.left: (SideType.right, SideType.front, SideType.left), LineDirection.up: (SideType.front, SideType.left, SideType.back)}), SideType.bottom: BoxSide(self, 'Bottom', MeasurementDirection.width, MeasurementDirection.depth, SideSize.medium, (0, 0, -1), {LineDirection.right: (SideType.left, SideType.front, SideType.right), LineDirection.down: (SideType.front, SideType.right, SideType.back), LineDirection.left: (SideType.right, SideType.back, SideType.left), LineDirection.up: (SideType.back, SideType.left, SideType.front)}) } # yapf: enable self._notchSize = 10 self.outerDimmensions(100, 100, 100) self.calculate() self.generateExtrudes = True self.generateViews = True @property def thickness(self): raise Exception('thickness is de-implemented') @property def notchSize(self): return self._notchSize @notchSize.setter def notchSize(self, notchSize): self._notchSize = notchSize def sideForType(self, sideType): return self.sides[sideType] def outerDimmensions(self, width, height, depth): self.outerWidth = width self.outerHeight = height self.outerDepth = depth self.calculate() def innerLength(self, lineDirection): if lineDirection == MeasurementDirection.width: return self.innerWidth elif lineDirection == MeasurementDirection.height: return self.innerHeight elif lineDirection == MeasurementDirection.depth: return self.innerDepth else: raise Exception("Unknown LineDirection: " + str(lineDirection)) def notchConfig(self, direction): if direction == MeasurementDirection.width: return self._notchConfigWidth elif direction == MeasurementDirection.height: return self._notchConfigHeight elif direction == MeasurementDirection.depth: return self._notchConfigDepth else: raise Exception("Unknown MeasurementDirection: " + str(direction)) def createNotchConfig(self, sideLength): count = sideLength / self._notchSize size = float(sideLength) / float(count * 2 + 1) return NotchConfig(count, size) def calculate(self): self.innerWidth = self.outerWidth - self.sideForType(SideType.left).thickness - self.sideForType(SideType.right).thickness self.innerHeight = self.outerHeight - self.sideForType(SideType.top).thickness - self.sideForType(SideType.bottom).thickness self.innerDepth = self.outerDepth - self.sideForType(SideType.front).thickness - self.sideForType(SideType.back).thickness self._notchConfigWidth = self.createNotchConfig(self.innerWidth) self._notchConfigHeight = self.createNotchConfig(self.innerHeight) self._notchConfigDepth = self.createNotchConfig(self.innerDepth) def prt(self): print("Box configuration") print("Outer w/h/d: " + str(self.outerWidth) + "/" + str(self.outerHeight) + "/" + str(self.outerDepth)) print("Inner w/h/d: " + str(self.innerWidth) + "/" + str(self.innerHeight) + "/" + str(self.innerDepth)) print("Sides:") for sideType in SideType: boxSide = self.sideForType(sideType) print(" " + str(boxSide.title)) print(" enabled: " + str(boxSide.enabled)) print(" thickness: " + str(boxSide.thickness)) print("Notch size : " + str(self._notchSize)) print(" Width: " + str(self.notchConfig(MeasurementDirection.width))) print(" Height: " + str(self.notchConfig(MeasurementDirection.height))) print(" Depth: " + str(self.notchConfig(MeasurementDirection.depth))) class VGen(object): def __init__(self, current, dir): self.current = current self.dir = dir self.list = [] def move(self, x, y, z=0): pos = Vector(x, y, z) self.current = self.current.add(pos) self.list.append(self.current) def moveX(self, value): self.move(self.dir.x_x * value, self.dir.x_y * value, 0) def moveY(self, value): self.move(self.dir.y_x * value, self.dir.y_y * value, 0) # A line is though drawn from left to right, but is actually automatically # rotated to go either right, down, left or up by the lineDirection parameter. def line(cfg, start, boxSide, lineDirection, notchConfig, innerLength): sideSize = boxSide.sideSize opposite = boxSide.opposite(lineDirection) left = boxSide.left(lineDirection) right = boxSide.right(lineDirection) inv = -1 if sideSize.inv(lineDirection) else 1 includeStart = sideSize.includeStart(lineDirection) g = VGen(start, lineDirection) if includeStart: g.moveX(left.thickness) if opposite.enabled: g.moveX(notchConfig.size) for i in range(0, notchConfig.count): g.moveY(-opposite.thickness * inv) g.moveX(notchConfig.size) g.moveY(opposite.thickness * inv) g.moveX(notchConfig.size) else: g.moveX(innerLength) if includeStart: g.moveX(right.thickness) return g.list def makeBoxSide(cfg, boxSide): notchConfigX = cfg.notchConfig(boxSide.directionX) notchConfigY = cfg.notchConfig(boxSide.directionY) innerLengthX = cfg.innerLength(boxSide.directionX) innerLengthY = cfg.innerLength(boxSide.directionY) opposite = boxSide.opposite(LineDirection.right) left = boxSide.left(LineDirection.right) # Offset the start so that the inner area starts at (0, 0) if boxSide.sideSize is SideSize.large: start = Vector(-left.thickness, opposite.thickness, 0) elif boxSide.sideSize is SideSize.medium: start = Vector(-left.thickness, 0, 0) else: start = Vector(0, 0, 0) v = [start] top = line(cfg, v[-1], boxSide, LineDirection.right, notchConfigX, innerLengthX) v.extend(top) left = line(cfg, v[-1], boxSide, LineDirection.down, notchConfigY, innerLengthY) v.extend(left) bottom = line(cfg, v[-1], boxSide, LineDirection.left, notchConfigX, innerLengthX) v.extend(bottom) right = line(cfg, v[-1], boxSide, LineDirection.up, notchConfigY, innerLengthY) v.extend(right) return v def makeParts(doc, cfg): parts = {} sep = cfg.outerWidth * 0.05 def m(rotX, rotY, rotZ, moveX, moveY, moveZ): m = Matrix() if rotX != 0: x = Matrix() x.rotateX(math.radians(rotX)) m = m * x if rotY != 0: y = Matrix() y.rotateY(math.radians(rotY)) m = m * y if rotZ != 0: z = Matrix() z.rotateZ(math.radians(rotZ)) m = m * z v = Vector(cfg.innerWidth, cfg.innerDepth, cfg.innerHeight) v.scale(moveX, moveY, moveZ) m.move(v) return m # yapf: disable ms = { SideType.front: m( 90, 0, 0, -0.5, -0.5, 0.5), SideType.back: m( 90, 180, 0, 0.5, 0.5, 0.5), SideType.left: m( 90, 90, 0, -0.5, -0.5, 0.5), SideType.right: m( 90, 90, 0, 0.5, -0.5, 0.5), SideType.top: m( 0, 0, 0, -0.5, 0.5, 0.5), SideType.bottom: m( 0, 180, 180, -0.5, -0.5, -0.5) } # yapf: enable for sideType in SideType: boxSide = cfg.sideForType(sideType) if not boxSide.enabled: continue s = Part.makePolygon(makeBoxSide(cfg, boxSide)) s.transformShape(ms[sideType]) feature = doc.addObject("Part::Feature", boxSide.title + '_Part') feature.Label = boxSide.title + ' Part' feature.Shape = s parts[sideType] = feature return parts def makeExtrudes(doc, parts, cfg): def makeExtrude(doc, cfg, sideType): side = cfg.sideForType(sideType) part = parts[sideType] extrude = doc.addObject("Part::Extrusion", side.title + "_Extrude") extrude.Base = part extrude.Dir = side.extrudeVector * side.thickness extrude.Solid = (True) extrude.TaperAngle = (0) extrude.Label = side.title + ' Extrude' partView = part.ViewObject extrudeView = extrude.ViewObject if partView is not None and extrudeView is not None: partView.Visibility = False extrudeView.ShapeColor = partView.ShapeColor extrudeView.LineColor = partView.LineColor extrudeView.PointColor = partView.PointColor return extrude extrudes = {} for sideType, part in parts.items(): extrudes[sideType] = makeExtrude(doc, cfg, sideType) return extrudes def makeShape2DViews(doc, cfg, extrudes): pvs = { SideType.front: Vector(0, 1, 0), SideType.back: Vector(0, 1, 0), SideType.left: Vector(1, 0, 0), SideType.right: Vector(1, 0, 0), SideType.top: Vector(0, 0, 1), SideType.bottom: Vector(0, 0, 1) } def makeShape2DView(extrude, sideType): boxSide = cfg.sideForType(sideType) s = Draft.makeShape2DView(extrude, projectionVector = pvs[sideType]) s.Label = boxSide.title + ' View' s.ViewObject.Visibility = False return s objects = {} for sideType, extrude in extrudes.items(): objects[sideType] = makeShape2DView(extrude, sideType) return objects def make(doc, cfg): # Cretate the underlying parts parts = makeParts(doc, cfg) extrudes = {} if cfg.generateExtrudes: extrudes = makeExtrudes(doc, parts, cfg) box = doc.addObject("App::DocumentObjectGroup", "Box") for e in extrudes.values(): box.addObject(e) views = None cfg.generateViews = True if cfg.generateViews: views = makeShape2DViews(doc, cfg, extrudes) viewsGroup = doc.addObject("App::DocumentObjectGroup", "Views") viewsGroup.Label = 'Views' pos = Vector(0, 0, 0) r = Rotation(Vector(0, 0, 1), 0) # print('pos: ' + str(pos.x)) previous = None for sideType in SideType: try: view = views[sideType] if previous: pos.x += previous.Shape.BoundBox.XLength/2 pos.x += view.Shape.BoundBox.XLength/2 pos.x += 10 # spacing between each side view.Placement = Placement(Vector(pos), r) #print('Placement: ', view.Placement.Base) viewsGroup.addObject(view) previous = view except KeyError, e: print('error finding view', e) viewsGroup.ViewObject.Visibility = False doc.recompute() return { 'parts': parts, 'extrudes': extrudes, 'views': views} def removeEverything(doc): def rm(name): if hasattr(doc, name): doc.removeObject(name) rm("Box") rm("Views") for key in ['Front', 'Back', 'Left', 'Right', 'Top', 'Bottom']: rm(key + '_Cross_Section') rm(key + '_Extrude') rm(key + '_Part') rm('Shape2DView') rm('Shape2DView001') rm('Shape2DView002') rm('Shape2DView003') rm('Shape2DView004') rm('Shape2DView005')