# Build the CNX Vinyl inline optical QC station (concept) in FreeCAD. # # Run inside the FreeCAD GUI (26.3): # exec(open(r"C:/dev/cnx_vinyl/docs/equipment/inline-qc-station/build_station.py", encoding="utf-8").read()) # It creates the document "InlineQCStation", saves inline-qc-station.FCStd next # to this script and leaves the view open for rendering (render_station.py). # # Concept, not a fabrication drawing: the frame uses the real 8-slot 40 series # section (profile_shape), bought parts are drawn as their envelope boxes. Sizes # follow ../inline-optical-qc.md. At the end the script lists any part that does # not reach the floor through parts it touches (panels not counted). # # Axes: x = record flow (infeed at x = 0, outfeed at x = L), y = across # (operator front at -y), z = up from the floor. All values in mm. import os import FreeCAD as App import Part try: import FreeCADGui as Gui HAVE_GUI = App.GuiUp except Exception: HAVE_GUI = False HERE = os.path.dirname(os.path.abspath(__file__)) if "__file__" in dir() else r"C:/dev/cnx_vinyl/docs/equipment/inline-qc-station" OUT = os.path.join(HERE, "inline-qc-station.FCStd") V = App.Vector # ---------------------------------------------------------------- parameters L = 1900 # frame length along the flow YF = 300 # centre of the outer frame profiles, +/- y P = 40 # profile size (40 x 40 slot profile; 40 x 80 for the base) ZB = 900 # top of the rim belts = record underside while conveyed R_REC = 150.8 # 12" record radius (IEC 98: 301.6 mm diameter) T_REC = 2.0 # record thickness (approx.) R_LABEL = 50.0 # label radius R_HOLE = 3.62 # centre hole 7.24 mm R_REC_AREA = 146.3 # recorded outer radius (292.6 mm max): the rim outside it carries no grooves Y_BELT = 148.5 # round belts run under the unrecorded rim ZL = 960 # record underside when the chuck has lifted it LIFT = 75 # chuck stroke: pad top 885 (below the belts) -> 960 X_IN = 200 # infeed: operator or Classic unloader puts the record here X_LB = 520 # label gate: both label cameras, on the fly with strobe X_IS = 880 # inspection: lift, clamp, one turn under the line-scans X_RJ = 1320 # reject pusher X_OUT = L - 30 - R_REC - 4 # outfeed end stop: good records wait here XT0, XT1 = 320, 1100 # light-tight tunnel uprights (inner faces 360 / 1100) TOP_LINE = (X_IS + 50, X_IS + 155) # top line-scan sees the +x radius, r 50..155 BOT_LINE = (X_IS - 155, X_IS - 50) # bottom line-scan sees the -x radius WD_LINE = 330 # 4k x 7 um sensor, 160 mm field, f = 50 mm -> ~330 mm working distance WD_LABEL = 303 # 2840 px x 2.74 um sensor, 140 mm field, f = 16 mm # ---------------------------------------------------------------- colours ALU = (0.78, 0.80, 0.83) DARK = (0.18, 0.19, 0.21) BLACK = (0.04, 0.04, 0.05) CAM = (0.36, 0.52, 0.70) # camera bodies LENS = (0.08, 0.08, 0.09) LIGHT = (0.97, 0.95, 0.80) # emitting faces LIGHTBODY = (0.25, 0.25, 0.27) BELT = (0.20, 0.62, 0.34) # green PU round belt UHMW = (0.95, 0.95, 0.93) PNEU = (0.70, 0.72, 0.75) SERVO = (0.12, 0.12, 0.14) RED = (0.85, 0.10, 0.10) YELLOW = (0.95, 0.80, 0.10) GREEN = (0.15, 0.70, 0.25) AMBER = (0.98, 0.60, 0.10) PANEL = (0.06, 0.06, 0.07) CLEAR = (0.75, 0.85, 0.95) CABINET = (0.72, 0.73, 0.70) LASER = (1.0, 0.05, 0.05) for _d in list(App.listDocuments().values()): if _d.Name.startswith("InlineQCStation") or _d.FileName.replace("\\", "/").endswith("inline-qc-station.FCStd"): App.closeDocument(_d.Name) doc = App.newDocument("InlineQCStation") doc.Label = "CNX inline optical QC station (concept)" groups = {} def grp(name): if name not in groups: g = doc.addObject("App::DocumentObjectGroup", name) groups[name] = g return groups[name] def add(group, name, shape, color=ALU, transp=0): o = doc.addObject("Part::Feature", name) o.Shape = shape grp(group).addObject(o) if HAVE_GUI: vo = o.ViewObject vo.ShapeColor = color vo.Transparency = transp vo.LineColor = (0.25, 0.25, 0.25) return o def box(x0, y0, z0, x1, y1, z1): return Part.makeBox(x1 - x0, y1 - y0, z1 - z0, V(x0, y0, z0)) def cyl(r, h, base, d=V(0, 0, 1)): return Part.makeCylinder(r, h, base, d) # ---------------------------------------------------------------- real slot profiles # 8-slot 40 series (item / Misumi HFS8 class): 8.2 mm slot opening, 4.3 mm lip, # undercut chamber 16 mm wide, slot depth 12 mm, centre bore 6.8 mm (M8 tap), # corner cavities, 0.8 mm corner chamfer. 40 x 80 adds a centre cavity. ALU_BRACKET = (0.60, 0.62, 0.65) _FACES = {} def _slot(cx, cy, nx, ny): td = [(-4.1, -1), (-4.1, 4.3), (-8.0, 4.3), (-8.0, 7.5), (-4.1, 12.0), (4.1, 12.0), (8.0, 7.5), (8.0, 4.3), (4.1, 4.3), (4.1, -1)] tx, ty = -ny, nx pts = [V(cx + t * tx + d * nx, cy + t * ty + d * ny, 0) for t, d in td] return Part.Face(Part.makePolygon(pts + [pts[0]])) def profile_face(a, b): """Cross-section a (local X) by b (local Y), both multiples of 40, corner at the origin.""" if (a, b) in _FACES: return _FACES[(a, b)] c = 0.8 o = [V(c, 0, 0), V(a - c, 0, 0), V(a, c, 0), V(a, b - c, 0), V(a - c, b, 0), V(c, b, 0), V(0, b - c, 0), V(0, c, 0)] f = Part.Face(Part.makePolygon(o + [o[0]])) cuts = [] for i in range(int(a // 40)): cuts += [_slot(20 + 40 * i, 0, 0, 1), _slot(20 + 40 * i, b, 0, -1)] for j in range(int(b // 40)): cuts += [_slot(0, 20 + 40 * j, 1, 0), _slot(a, 20 + 40 * j, -1, 0)] for i in range(int(a // 40)): for j in range(int(b // 40)): cuts.append(Part.Face(Part.Wire(Part.makeCircle(3.4, V(20 + 40 * i, 20 + 40 * j, 0))))) for cx, cy in [(5.4, 5.4), (a - 5.4, 5.4), (a - 5.4, b - 5.4), (5.4, b - 5.4)]: cuts.append(Part.Face(Part.Wire(Part.makeCircle(2.2, V(cx, cy, 0))))) if a == 40 and b == 80: cuts.append(Part.Face(Part.makePolygon([V(14, 33, 0), V(26, 33, 0), V(26, 47, 0), V(14, 47, 0), V(14, 33, 0)]))) if a == 80 and b == 40: cuts.append(Part.Face(Part.makePolygon([V(33, 14, 0), V(47, 14, 0), V(47, 26, 0), V(33, 26, 0), V(33, 14, 0)]))) for k in cuts: f = f.cut(k) _FACES[(a, b)] = f return f def profile_shape(x0, y0, z0, x1, y1, z1): """A real slot profile filling the axis-aligned bar x0..x1, y0..y1, z0..z1.""" dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 long_axis = max((dx, "x"), (dy, "y"), (dz, "z"))[1] if long_axis == "x": # local X -> Y, local Y -> Z, extrusion -> X a, b, n = dy, dz, dx m = App.Matrix(0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1) elif long_axis == "y": # local X -> Z, local Y -> X, extrusion -> Y a, b, n = dz, dx, dy m = App.Matrix(0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1) else: a, b, n = dx, dy, dz m = App.Matrix() a, b = round(a), round(b) if a % 40 or b % 40: return box(x0, y0, z0, x1, y1, z1) sol = profile_face(a, b).extrude(V(0, 0, n)) if long_axis != "z": sol.transformShape(m) sol.translate(V(x0, y0, z0)) return sol def prof(group, name, x0, y0, z0, x1, y1, z1): return add(group, name, profile_shape(x0, y0, z0, x1, y1, z1), ALU) def gusset(group, name, corner, d1, d2, w): """Cast angle bracket in the inside corner: legs 32 mm along d1 and d2, 34 mm wide along w.""" p0 = corner - w * 17 pts = [p0, p0 + d1 * 32, p0 + d2 * 32, p0] tri = Part.Face(Part.makePolygon(pts)) return add(group, name, tri.extrude(w * 34), ALU_BRACKET) def rot(shape, center, axis, deg): s = shape.copy() s.rotate(center, axis, deg) return s def ring(r_out, r_in, h, base): return cyl(r_out, h, base).cut(cyl(r_in, h + 2, base - V(0, 0, 1))) # ---------------------------------------------------------------- frame G = "Frame" for i, (x, y) in enumerate([(20, -YF), (20, YF), (XT0 + 20, -YF), (XT0 + 20, YF), (XT1 + 20, -YF), (XT1 + 20, YF), (L - 20, -YF), (L - 20, YF)]): add(G, f"Foot{i}", cyl(30, 12, V(x, y, 0)).fuse(cyl(8, 50, V(x, y, 12))), DARK) for s in (-1, 1): prof(G, f"BaseBeam{'F' if s < 0 else 'B'}", 0, s * YF - 20, 60, L, s * YF + 20, 140) # 40 x 80 for x0, z1, nm in [(0, 870, "End0"), (XT0, 1500, "Tunnel0"), (XT1, 1500, "Tunnel1"), (L - P, 1340 if s < 0 else 1080, "End1")]: # front end upright carries the touch screen prof(G, f"Upright{nm}{'F' if s < 0 else 'B'}", x0, s * YF - 20, 140, x0 + P, s * YF + 20, z1) prof(G, f"TopBeam{'F' if s < 0 else 'B'}", XT0 + P, s * YF - 20, 1460, XT1, s * YF + 20, 1500) prof(G, f"LowBeam{'F' if s < 0 else 'B'}", XT0 + P, s * YF - 20, 420, XT1, s * YF + 20, 460) prof(G, f"RejectBeam{'F' if s < 0 else 'B'}", XT1 + P, s * YF - 20, 1040, L - P, s * YF + 20, 1080) for x0, nm in [(0, "End0"), (XT0, "Tunnel0"), (XT1, "Tunnel1"), (L - P, "End1")]: prof(G, f"BaseCross{nm}", x0, -YF + 20, 100, x0 + P, YF - 20, 140) prof(G, f"ConvCross{nm}", x0, -YF + 20, 790, x0 + P, YF - 20, 830) # top bridge: label camera and top line-scan prof(G, "TopCrossLabel", X_LB + 70, -YF + 20, 1460, X_LB + 110, YF - 20, 1500) xt = sum(TOP_LINE) / 2 prof(G, "TopCrossLineScan", xt + 48, -YF + 20, 1460, xt + 88, YF - 20, 1500) prof(G, "TopCrossThermal", XT0 + P, -YF + 20, 1460, XT0 + P + 40, YF - 20, 1500) prof(G, "TopCrossBacklight", sum(BOT_LINE) / 2 - 38, -YF + 20, 1460, sum(BOT_LINE) / 2 + 2, YF - 20, 1500) # lower bridge: bottom label camera, bottom line-scan, chuck lift xb = sum(BOT_LINE) / 2 prof(G, "LowCrossLabel", X_LB + 70, -YF + 20, 420, X_LB + 110, YF - 20, 460) prof(G, "LowCrossLineScan", xb - 20, -YF + 20, 420, xb + 20, YF - 20, 460) prof(G, "LowCrossChuck", X_IS - 20, -YF + 20, 420, X_IS + 20, YF - 20, 460) prof(G, "LowCrossBacklight", xt - 20, -YF + 20, 420, xt + 20, YF - 20, 460) prof(G, "BaseCrossCabinet0", 1200, -YF + 20, 100, 1240, YF - 20, 140) prof(G, "BaseCrossCabinet1", 1580, -YF + 20, 100, 1620, YF - 20, 140) prof(G, "RejectBridgeCross", X_RJ - 20, -YF + 20, 1040, X_RJ + 20, YF - 20, 1080) # cast angle brackets in the visible frame corners EX, EZ, EY = V(1, 0, 0), V(0, 0, 1), V(0, 1, 0) _k = 0 for s in (-1, 1): y = s * YF for x, dirs in [(P, (1,)), (XT0, (-1,)), (XT0 + P, (1,)), (XT1, (-1,)), (XT1 + P, (1,)), (L - P, (-1,))]: for d in dirs: gusset(G, f"Bracket{_k}", V(x, y, 140), EX * d, EZ, EY); _k += 1 for x, d, z in [(XT0 + P, 1, 1460), (XT1, -1, 1460), (XT0 + P, 1, 420), (XT1, -1, 420), (XT1 + P, 1, 1040), (L - P, -1, 1040)]: gusset(G, f"Bracket{_k}", V(x, y, z), EX * d, EZ * -1, EY); _k += 1 # ---------------------------------------------------------------- conveyor G = "Conveyor" for s in (-1, 1): side = "F" if s < 0 else "B" prof(G, f"Rail{side}", 0, s * 180 - 20, 830, L, s * 180 + 20, 870) add(G, f"BeltSlideCarrier{side}", box(45, min(s * 140, s * 200), 870, L - 45, max(s * 140, s * 200), 880), DARK) add(G, f"BeltSlide{side}", box(45, s * Y_BELT - 5, 880, L - 45, s * Y_BELT + 5, 894), UHMW) add(G, f"BeltTop{side}", cyl(3, L - 40, V(20, s * Y_BELT, ZB - 3), V(1, 0, 0)), BELT) add(G, f"BeltReturn{side}", cyl(3, L - 40, V(20, s * Y_BELT, ZB - 49), V(1, 0, 0)), BELT) for x, nm in [(20, "Idler"), (L - 20, "Drive")]: add(G, f"Pulley{nm}{side}", cyl(23, 14, V(x, s * Y_BELT - 7, ZB - 26), V(0, 1, 0)), DARK) add(G, f"PulleyAxle{nm}{side}", cyl(6, 6, V(x, min(s * (Y_BELT + 6), s * 160), ZB - 26), V(0, 1, 0)), PNEU) # infeed side guides with a flared mouth, PU strip on the rim height add(G, f"SideGuide{side}", box(110, s * 157 - 3, ZB + 2, XT0 - 10, s * 157 + 3, ZB + 14), UHMW) add(G, f"SideGuideFlare{side}", rot(box(40, s * 157 - 3, ZB + 2, 112, s * 157 + 3, ZB + 14), V(110, s * 157, ZB), V(0, 0, 1), s * 25), UHMW) add(G, f"GuidePost{side}", box(150, min(s * 160, s * 172), 870, 170, max(s * 160, s * 172), ZB + 14), DARK) add(G, "DriveShaft", cyl(6, 2 * Y_BELT + 60, V(L - 20, -Y_BELT - 30, ZB - 26), V(0, 1, 0)), DARK) add(G, "DriveGearmotor", cyl(35, 150, V(L - 20, 210, ZB - 26), V(0, 1, 0)) .fuse(box(L - 60, 200, ZB - 70, L + 15, 230, ZB + 15)), SERVO) add(G, "EndStopBar", box(L - 30, -200, ZB + 30, L - 12, 200, ZB + 48), DARK) for s in (-1, 1): add(G, f"EndStopPost{'F' if s < 0 else 'B'}", box(L - 30, min(s * 170, s * 190), 870, L - 12, max(s * 170, s * 190), ZB + 30), DARK) add(G, "EndStopBumper", box(L - 32, -40, ZB + 4, L - 26, 40, ZB + 30), (0.9, 0.5, 0.1)) # presence sensors (diffuse, looking up through the open centre) and the stop beam G = "Sensors" for x, nm in [(X_IN, "Infeed"), (X_LB - 120, "LabelTrigger"), (L - 120, "Outfeed")]: add(G, f"Presence{nm}", box(x - 15, -120, 850, x + 15, -100, 885), (0.15, 0.35, 0.75)) add(G, f"PresenceBracket{nm}", box(x - 10, -160, 840, x + 10, -100, 850), DARK) xs = X_IS + R_REC for s in (-1, 1): add(G, f"StopBeam{'Tx' if s < 0 else 'Rx'}", box(xs - 10, min(s * 200, s * 222), 870, xs + 10, max(s * 200, s * 222), ZB + 25), (0.15, 0.35, 0.75)) add(G, "StopBeamRay", cyl(0.8, 400, V(xs, -200, ZB + 8), V(0, 1, 0)), LASER, 40) # ---------------------------------------------------------------- label gate G = "LabelGate" add(G, "LabelCamTop", box(X_LB - 15, -15, ZB + WD_LABEL + 50, X_LB + 15, 15, ZB + WD_LABEL + 92), CAM) add(G, "LabelLensTop", cyl(17, 50, V(X_LB, 0, ZB + WD_LABEL)), LENS) add(G, "LabelRingTop", ring(70, 42, 22, V(X_LB, 0, ZB + 170)), LIGHTBODY) add(G, "LabelRingTopFace", ring(62, 48, 1, V(X_LB, 0, ZB + 169)), LIGHT) prof(G, "LabelCamTopPost", X_LB + 70, -20, ZB + 160, X_LB + 110, 20, 1460) add(G, "LabelCamTopPlate", box(X_LB + 15, -12, ZB + WD_LABEL + 55, X_LB + 70, 12, ZB + WD_LABEL + 87), DARK) add(G, "LabelRingTopClamp", box(X_LB + 58, -14, ZB + 166, X_LB + 70, 14, ZB + 196), DARK) zl = ZB - WD_LABEL add(G, "LabelCamBottom", box(X_LB - 15, -15, zl - 92, X_LB + 15, 15, zl - 50), CAM) add(G, "LabelLensBottom", cyl(17, 50, V(X_LB, 0, zl - 50)), LENS) add(G, "LabelRingBottom", ring(70, 42, 22, V(X_LB, 0, ZB - 80)), LIGHTBODY) add(G, "LabelRingBottomFace", ring(62, 48, 1, V(X_LB, 0, ZB - 58)), LIGHT) prof(G, "LabelCamBottomPost", X_LB + 70, -20, 460, X_LB + 110, 20, ZB - 48) add(G, "LabelCamBottomPlate", box(X_LB + 15, -12, zl - 87, X_LB + 70, 12, zl - 55), DARK) add(G, "LabelRingBottomClamp", box(X_LB + 58, -14, ZB - 84, X_LB + 70, 14, ZB - 54), DARK) # ---------------------------------------------------------------- inspection: lift, clamp, one turn G = "Chuck" zp = ZL # pad top, lifted (shown lifted) add(G, "ChuckPad", cyl(40, 10, V(X_IS, 0, zp - 10)), (0.85, 0.45, 0.10)) add(G, "ChuckMandrel", cyl(3.5, 9, V(X_IS, 0, zp)).fuse(Part.makeCone(3.5, 1.2, 4, V(X_IS, 0, zp + 9))), DARK) add(G, "SpindleHousing", cyl(25, 110, V(X_IS, 0, zp - 120)), PNEU) add(G, "LiftPlate", box(X_IS - 40, -40, zp - 132, X_IS + 40, 150, zp - 120), DARK) add(G, "SpindleServo", cyl(30, 110, V(X_IS, 110, zp - 242)), SERVO) add(G, "SpindleBeltCover", box(X_IS - 32, -32, zp - 120, X_IS + 32, 142, zp - 100), (0.3, 0.3, 0.32)) add(G, "LiftCylinderRods", cyl(6, 100, V(X_IS - 30, 0, zp - 232)).fuse(cyl(6, 100, V(X_IS + 30, 0, zp - 232))), PNEU) add(G, "LiftCylinder", box(X_IS - 50, -25, zp - 322, X_IS + 50, 25, zp - 232), PNEU) prof(G, "LiftPost", X_IS - 20, -20, 460, X_IS + 20, 20, zp - 322) G = "LineScanTop" zr = ZL + T_REC # top face of the lifted record add(G, "LineCamTop", box(xt - 31, -31, zr + WD_LINE + 60, xt + 31, 31, zr + WD_LINE + 105), CAM) add(G, "LineLensTop", cyl(25, 60, V(xt, 0, zr + WD_LINE)), LENS) prof(G, "LineCamTopPost", xt + 48, -20, zr + 150, xt + 88, 20, 1460) add(G, "LightHangerTop", box(xt + 58, -112, zr + 130, xt + 78, 112, zr + 150), DARK) add(G, "CoaxHangerTop", box(xt + 58, -10, zr + 80, xt + 78, 10, zr + 130), DARK) add(G, "LineCamTopPlate", box(xt + 31, -20, zr + WD_LINE + 62, xt + 48, 20, zr + WD_LINE + 103), DARK) x0, x1 = TOP_LINE[0] - 5, TOP_LINE[1] + 5 add(G, "CoaxLightTop", box(x0, -30, zr + 50, x1, 30, zr + 110), LIGHTBODY) add(G, "CoaxLightTopFace", box(x0 + 3, -26, zr + 49, x1 - 3, 26, zr + 50), LIGHT) for s in (-1, 1): b = box(x0, -15, 0, x1, 15, 30) b.translate(V(0, s * 100, zr + 15)) add(G, f"DarkFieldTop{'F' if s < 0 else 'B'}", rot(b, V(0, s * 100, zr + 30), V(1, 0, 0), s * 20), LIGHTBODY) add(G, f"DarkFieldTopBracket{'F' if s < 0 else 'B'}", box(xt + 58, s * 100 - 5, zr + 48, xt + 78, s * 100 + 5, zr + 130), DARK) bx0, bx1 = BOT_LINE[0] - 5, BOT_LINE[1] + 5 add(G, "BacklightForBottomCam", box(bx0, -20, zr + 30, bx1, 20, zr + 52), LIGHTBODY) add(G, "BacklightForBottomCamFace", box(bx0 + 3, -17, zr + 29, bx1 - 3, 17, zr + 30), LIGHT) add(G, "BacklightBracketTop", box(xb - 30, -5, zr + 52, xb - 10, 5, 1460), DARK) add(G, "ScanLineTop", box(TOP_LINE[0], -0.6, zr + 0.05, TOP_LINE[1], 0.6, zr + 0.5), LASER) add(G, "ThermalPost", box(XT0 + 60, -240, 1400, XT0 + 80, -220, 1460), DARK) add(G, "ThermalCam", rot(box(XT0 + 70, -260, 1380, XT0 + 130, -200, 1440), V(XT0 + 100, -230, 1410), V(1, 0, 0), -30), (0.95, 0.45, 0.10)) G = "LineScanBottom" zu = ZL # underside of the lifted record add(G, "LineCamBottom", box(xb - 31, -31, zu - WD_LINE - 105, xb + 31, 31, zu - WD_LINE - 60), CAM) add(G, "LineLensBottom", cyl(25, 60, V(xb, 0, zu - WD_LINE - 60)), LENS) prof(G, "LineCamBottomPost", xb - 20, 40, 460, xb + 20, 80, ZB - 20) add(G, "CoaxHangerBottom", box(xb - 10, 30, ZB - 70, xb + 10, 40, ZB - 30), DARK) add(G, "LineCamBottomPlate", box(xb - 20, 31, zu - WD_LINE - 105, xb + 20, 40, zu - WD_LINE - 60), DARK) add(G, "CoaxLightBottom", box(bx0, -30, ZB - 80, bx1, 30, ZB - 20), LIGHTBODY) add(G, "CoaxLightBottomFace", box(bx0 + 3, -26, ZB - 20, bx1 - 3, 26, ZB - 19), LIGHT) for s in (-1, 1): b = box(bx0, -15, 0, bx1, 15, 30) b.translate(V(0, s * 175, zu - 40)) add(G, f"DarkFieldBottom{'F' if s < 0 else 'B'}", rot(b, V(0, s * 175, zu - 25), V(1, 0, 0), -s * 8), LIGHTBODY) add(G, f"DarkFieldBottomBracket{'F' if s < 0 else 'B'}", box(bx0 + 40, s * 192 - 5, 870, bx0 + 60, s * 192 + 5, zu - 35), DARK) add(G, "BacklightForTopCam", box(x0, -20, ZB - 40, x1, 20, ZB - 15), LIGHTBODY) prof(G, "BacklightPost", xt - 20, -20, 460, xt + 20, 20, ZB - 40) add(G, "BacklightForTopCamFace", box(x0 + 3, -17, ZB - 15, x1 - 3, 17, ZB - 14), LIGHT) add(G, "ScanLineBottom", box(BOT_LINE[0], -0.6, zu - 0.5, BOT_LINE[1], 0.6, zu - 0.05), LASER) G = "WarpLasers" for r in (90, 130): add(G, f"Laser_r{r}", box(X_IS - 23, -r - 10, ZB - 55, X_IS + 23, -r + 10, ZB - 15), (0.15, 0.35, 0.75)) add(G, f"LaserBeam_r{r}", cyl(0.8, zu - (ZB - 15), V(X_IS, -r, ZB - 15)), LASER, 30) add(G, f"LaserBracket_r{r}", box(X_IS - 10, -r - 10, 830, X_IS + 10, -r + 10, ZB - 55), DARK) prof(G, "LaserRail", X_IS - 20, -200, 790, X_IS + 20, -70, 830) # ---------------------------------------------------------------- reject G = "Reject" add(G, "RodlessCylinder", box(X_RJ - 30, -300, 1000, X_RJ + 30, 300, 1040), PNEU) yc = -205 # carriage retracted at the front add(G, "PusherCarriage", box(X_RJ - 35, yc - 30, 960, X_RJ + 35, yc + 30, 1000), DARK) add(G, "PusherArm", box(X_RJ - 10, yc - 10, ZB + 26, X_RJ + 10, yc + 10, 960), DARK) add(G, "PusherPaddle", box(X_RJ - 100, yc + 10, ZB + 4, X_RJ + 100, yc + 22, ZB + 26), (0.9, 0.5, 0.1)) CX0, CX1 = X_RJ - 170, X_RJ + 170 Y_CH, Z_CH = 205, ZB - 7 # chute top edge: just outside the back rail, below the belt plane chute = box(CX0, 0, 0, CX1, 360, 6) chute = rot(chute, V(0, 0, 0), V(1, 0, 0), -42) chute.translate(V(0, Y_CH, Z_CH)) add(G, "RejectChute", chute, (0.55, 0.57, 0.60)) for xw in (CX0 - 6, CX1): w = box(xw, 0, 0, xw + 6, 360, 80) w = rot(w, V(0, 0, 0), V(1, 0, 0), -42) w.translate(V(0, Y_CH, Z_CH)) add(G, f"ChuteWall{xw}", w, (0.55, 0.57, 0.60)) import math _zc = Z_CH - math.tan(math.radians(42)) * (YF + 20 - Y_CH) # chute underside above the back frame line for i, xs_ in enumerate((CX0 + 10, CX1 - 50)): prof(G, f"ChuteSupport{i}", xs_, YF - 20, 140, xs_ + 40, YF + 20, math.floor(_zc)) binb = box(CX0 - 20, 380, 0, CX1 + 20, 720, 560).cut(box(CX0 - 8, 392, 12, CX1 + 8, 708, 600)) add(G, "RejectBin", binb, (0.85, 0.20, 0.15), 20) # ---------------------------------------------------------------- enclosure (black, light-tight tunnel) G = "Enclosure" slot = box(XT0 - 10, -205, 860, XT1 + P + 10, 205, ZB + 30) for x, nm in [(XT0 - 3, "Infeed"), (XT1 + P, "Outfeed")]: up = box(x, -YF - 22, 870, x + 3, YF + 22, 1500).cut(slot) add(G, f"EndWallUpper{nm}", up, PANEL, 55) lo = box(x, -YF - 22, 140, x + 3, YF + 22, 870).cut(box(x - 1, -205, 815, x + 4, 205, 880)) add(G, f"EndWallLower{nm}", lo, PANEL, 55) for s in (-1, 1): side = "Front" if s < 0 else "Back" y0 = s * (YF + 20) if s > 0 else -YF - 23 add(G, f"Panel{side}Upper", box(XT0, y0, 870, XT1 + P, y0 + 3, 1500), PANEL, 55) add(G, f"Panel{side}Lower", box(XT0, y0, 140, XT1 + P, y0 + 3, 870), PANEL, 55) add(G, "PanelTop", box(XT0, -YF - 23, 1500, XT1 + P, YF + 23, 1503), PANEL, 55) add(G, "PanelBottom", box(XT0, -YF - 20, 140, XT1 + P, YF + 20, 143), PANEL, 55) add(G, "DoorHandle", box(700, -YF - 45, 1150, 860, -YF - 23, 1175), DARK) add(G, "GuardRejectFront", box(XT1 + P, -YF - 23, 870, L, -YF - 20, 1100), CLEAR, 75) add(G, "GuardRejectTop", box(XT1 + P, -YF - 23, 1100, L, YF + 23, 1103), CLEAR, 75) # ---------------------------------------------------------------- electrical and operator G = "Electrical" add(G, "Cabinet", box(1180, -200, 140, 1640, 170, 750), CABINET) add(G, "CabinetHandle", box(1600, -215, 400, 1615, -200, 520), DARK) add(G, "StackLightPole", cyl(10, 123, V(XT1 + 20, YF, 1500)), DARK) for i, c in enumerate([GREEN, AMBER, RED]): add(G, f"StackLight{i}", cyl(25, 57, V(XT1 + 20, YF, 1623 + i * 57)), c) add(G, "HMIArm", box(L - 40, -YF - 80, 1225, L, -YF - 20, 1245), DARK) add(G, "HMI", rot(box(L - 260, -YF - 100, 1150, L, -YF - 80, 1320), V(L - 130, -YF - 90, 1235), V(1, 0, 0), -15), (0.10, 0.10, 0.12)) add(G, "EStopBox", box(XT1, -YF - 60, 930, XT1 + 80, -YF - 20, 1010), YELLOW) add(G, "EStop", cyl(22, 25, V(XT1 + 40, -YF - 60, 970), V(0, -1, 0)), RED) add(G, "ValveTerminal", box(XT0 + 80, YF + 20, 300, XT0 + 330, YF + 70, 380), PNEU) add(G, "FRL", box(XT0 + 360, YF + 20, 280, XT0 + 440, YF + 80, 420), (0.2, 0.4, 0.8)) prof(G, "PneumaticsRail", XT0 + P, YF - 20, 260, XT1, YF + 20, 300) # ---------------------------------------------------------------- records G = "Records" def record(name, x, y, z, label_color, body=BLACK, transp=0, tilt=None): disc = cyl(R_REC, T_REC, V(x, y, z)).cut(cyl(R_HOLE, T_REC + 2, V(x, y, z - 1))) lab = cyl(R_LABEL, 0.3, V(x, y, z + T_REC)).cut(cyl(R_HOLE, 2, V(x, y, z + T_REC - 0.5))) labb = cyl(R_LABEL, 0.3, V(x, y, z - 0.3)).cut(cyl(R_HOLE, 2, V(x, y, z - 1))) if tilt: c, ax, deg = tilt disc, lab, labb = rot(disc, c, ax, deg), rot(lab, c, ax, deg), rot(labb, c, ax, deg) add(G, name, disc, body, transp) add(G, name + "LabelA", lab, label_color) add(G, name + "LabelB", labb, label_color) record("RecordInfeed", X_IN, 0, ZB, (0.95, 0.85, 0.75), body=(0.80, 0.12, 0.16), transp=25) # translucent red special record("RecordInspection", X_IS, 0, ZL, (0.20, 0.45, 0.80)) record("RecordOutfeed", X_OUT, 0, ZB, (0.95, 0.60, 0.15)) import math sc, a = 200, math.radians(42) yr = Y_CH + math.cos(a) * sc + 6 * math.sin(a) zr2 = Z_CH - math.sin(a) * sc + 6 * math.cos(a) record("RecordReject", X_RJ, yr, zr2, (0.6, 0.6, 0.6), tilt=(V(X_RJ, yr, zr2), V(1, 0, 0), -42)) doc.recompute() doc.saveAs(OUT) n = len([o for o in doc.Objects if o.TypeId == "Part::Feature"]) bb = None for o in doc.Objects: if o.TypeId == "Part::Feature": bb = o.Shape.BoundBox if bb is None else bb.united(o.Shape.BoundBox) print("objects", n, "bbox", round(bb.XMin), round(bb.XMax), round(bb.YMin), round(bb.YMax), round(bb.ZMin), round(bb.ZMax)) print("saved", OUT) # every part should touch another one, without counting the panels (hidden in the open views); # list the ones that do not (gap > 1 mm) _panels = set(o.Name for o in groups["Enclosure"].Group) _parts = [o for o in doc.Objects if o.TypeId == "Part::Feature" and o.Name not in _panels] # connectivity: every part must reach the floor through touching parts (gap <= 1 mm) _adj = {o.Name: set() for o in _parts} for ia, oa in enumerate(_parts): ba = oa.Shape.BoundBox for ob in _parts[ia + 1:]: bq = ob.Shape.BoundBox if (bq.XMin > ba.XMax + 2 or bq.XMax < ba.XMin - 2 or bq.YMin > ba.YMax + 2 or bq.YMax < ba.YMin - 2 or bq.ZMin > ba.ZMax + 2 or bq.ZMax < ba.ZMin - 2): continue if oa.Shape.distToShape(ob.Shape)[0] <= 1.0: _adj[oa.Name].add(ob.Name) _adj[ob.Name].add(oa.Name) _seen = [o.Name for o in _parts if o.Shape.BoundBox.ZMin < 1] _todo = list(_seen) _seen = set(_seen) while _todo: for nb in _adj[_todo.pop()]: if nb not in _seen: _seen.add(nb) _todo.append(nb) _floating = sorted(n for n in _adj if n not in _seen) print("parts not connected to the floor:", _floating if _floating else "none")