Inline QC station: real 40x40 profiles, labelled investor render

User, 18 Oct 2026: use real 40x40 profiles, make a render with text
pointing out the parts to show the investor; the ring lights looked
like they were floating.

- build_station.py: frame drawn from the real 8-slot 40 series section
  (profile_shape: slots, centre bore, corner cavities; 40x80 with centre
  cavity) and 24 cast angle brackets; top beams now butt between the
  uprights; label-camera posts moved beside the cameras so both ring
  lights clamp straight to them; top line-scan on a side post with
  visible camera plate and light hangers; slide strips on carrier angles,
  pulley axles, end-stop posts, presence-sensor brackets, backlight post,
  cabinet on base crosses, chute on two supports and clear of the rail,
  pneumatics rail, stack light on the back upright, touch screen on the
  front end upright. A connectivity check at the end lists any part that
  does not reach the floor through touching parts: none.
- render_annotated.py (FreeCAD): front three-quarter view, camera backed
  off so nothing is clipped, no navigation cube or preselection, label
  positions projected from the camera and checked with marker renders.
- annotate_render.py (Pillow): title, 16 labels in two columns, leaders
  assigned so none cross -> inline-qc-station-annotated.png.
- render_station.py: same camera and preselection fixes; all six views
  re-rendered.
- BOM: 32 m of 40x40 in 45 pieces; total still USD 16k to 33k.
- inline-optical-qc.md and README updated; .gitignore for the render
  intermediates.
This commit is contained in:
grabowski committed 2026-10-18 14:07:41 +07:00
1 parent 609424b598
commit e8674795f5
16 files changed
+483 -46

No files matched your search

+169 -36
View File
@@ -5,8 +5,10 @@
# 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: profiles are drawn as plain 40 mm bars,
# bought parts as their envelope boxes. Sizes follow ../inline-optical-qc.md.
# 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.
@@ -107,8 +109,82 @@ 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, box(x0, y0, z0, x1, y1, z1), ALU)
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):
@@ -129,37 +205,54 @@ for i, (x, y) in enumerate([(20, -YF), (20, YF), (XT0 + 20, -YF), (XT0 + 20, YF)
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, 1080, "End1")]:
(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, s * YF - 20, 1460, XT1 + P, s * YF + 20, 1500)
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 - 20, -YF + 20, 1460, X_LB + 20, YF - 20, 1500)
prof(G, "TopCrossLabel", X_LB + 70, -YF + 20, 1460, X_LB + 110, YF - 20, 1500)
xt = sum(TOP_LINE) / 2
prof(G, "TopCrossLineScan", xt - 20, -YF + 20, 1460, xt + 20, YF - 20, 1500)
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 - 20, -YF + 20, 420, X_LB + 20, YF - 20, 460)
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"BeltSlide{side}", box(30, s * Y_BELT - 5, 880, L - 30, s * Y_BELT + 5, 894), UHMW)
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),
@@ -169,12 +262,15 @@ add(G, "DriveShaft", cyl(6, 2 * Y_BELT + 60, V(L - 20, -Y_BELT - 30, ZB - 26), V
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),
@@ -187,15 +283,17 @@ add(G, "LabelCamTop", box(X_LB - 15, -15, ZB + WD_LABEL + 50, X_LB + 15, 15, ZB
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)
add(G, "LabelCamTopPost", box(X_LB - 20, 40, ZB + WD_LABEL + 50, X_LB + 20, 80, 1460), ALU)
add(G, "LabelCamTopPlate", box(X_LB - 20, 15, ZB + WD_LABEL + 50, X_LB + 20, 40, ZB + WD_LABEL + 92), DARK)
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)
add(G, "LabelCamBottomPost", box(X_LB - 20, 40, 460, X_LB + 20, 80, zl - 50), ALU)
add(G, "LabelCamBottomPlate", box(X_LB - 20, 15, zl - 92, X_LB + 20, 40, zl - 50), DARK)
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"
@@ -208,16 +306,16 @@ 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)
add(G, "LiftPost", box(X_IS - 20, -20, 460, X_IS + 20, 20, zp - 322), ALU)
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)
add(G, "LineCamTopPost", box(xt - 20, 40, zr + 150, xt + 20, 80, 1460), ALU)
add(G, "LightHangerTop", box(xt - 10, -112, zr + 130, xt + 10, 112, zr + 150), DARK)
add(G, "CoaxHangerTop", box(xt - 10, 30, zr + 80, xt + 10, 40, zr + 130), DARK)
add(G, "LineCamTopPlate", box(xt - 20, 31, zr + WD_LINE + 60, xt + 20, 40, zr + WD_LINE + 105), DARK)
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)
@@ -225,20 +323,21 @@ 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 - 10, s * 100 - 5, zr + 48, xt + 10, s * 100 + 5, zr + 130), DARK)
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 + 90, -240, 1430, XT0 + 110, -220, 1460), DARK)
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)
add(G, "LineCamBottomPost", box(xb - 20, 40, 460, xb + 20, 80, zu - WD_LINE - 60), ALU)
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)
@@ -248,6 +347,7 @@ for s in (-1, 1):
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)
@@ -256,7 +356,7 @@ 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)
add(G, "LaserRail", box(X_IS - 20, -200, 810, X_IS + 20, -70, 830), ALU)
prof(G, "LaserRail", X_IS - 20, -200, 790, X_IS + 20, -70, 830)
# ---------------------------------------------------------------- reject
G = "Reject"
@@ -266,15 +366,20 @@ add(G, "PusherCarriage", box(X_RJ - 35, yc - 30, 960, X_RJ + 35, yc + 30, 1000),
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, 165, ZB - 12))
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, 165, ZB - 12))
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)
@@ -299,17 +404,18 @@ add(G, "GuardRejectTop", box(XT1 + P, -YF - 23, 1100, L, YF + 23, 1103), CLEAR,
# ---------------------------------------------------------------- electrical and operator
G = "Electrical"
add(G, "Cabinet", box(1180, -200, 160, 1640, 170, 770), CABINET)
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, 120, V(XT1 + 20, -YF, 1503)), 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, 55, V(XT1 + 20, -YF, 1623 + i * 57)), c)
add(G, "HMIArm", box(XT1 + 50, -YF - 80, 1200, XT1 + 70, -YF - 20, 1220), DARK)
add(G, "HMI", rot(box(XT1 + 70, -YF - 100, 1130, XT1 + 330, -YF - 80, 1300), V(XT1 + 200, -YF - 90, 1215), V(1, 0, 0), -15), (0.10, 0.10, 0.12))
add(G, "EStopBox", box(XT1 + 60, -YF - 60, 930, XT1 + 140, -YF - 20, 1010), YELLOW)
add(G, "EStop", cyl(22, 25, V(XT1 + 100, -YF - 60, 970), V(0, -1, 0)), RED)
add(G, "ValveTerminal", box(XT0 + 80, YF + 25, 300, XT0 + 330, YF + 75, 380), PNEU)
add(G, "FRL", box(XT0 + 360, YF + 25, 280, XT0 + 440, YF + 85, 420), (0.2, 0.4, 0.8))
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"
@@ -331,9 +437,9 @@ record("RecordInfeed", X_IN, 0, ZB, (0.95, 0.85, 0.75), body=(0.80, 0.12, 0.16),
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 = 230, math.radians(42)
yr = 165 + math.cos(a) * sc + 6 * math.sin(a)
zr2 = ZB - 12 - math.sin(a) * sc + 6 * math.cos(a)
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()
@@ -345,3 +451,30 @@ for o in doc.Objects:
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")