Files
cnx_vinyl/docs/equipment/inline-qc-station/render_station.py
T
grabowski 609424b598 Inline QC station: concept setup on an aluminium profile frame
User, 18 Oct 2026: create a setup for the inline optical QC, an
aluminium extrusion frame with all sensors and a conveyor mounted to it.

- docs/equipment/inline-qc-station/: FreeCAD script (build_station.py)
  and model (.FCStd) of one station, 1.9 x 0.65 x 1.5 m on 27 m of
  40x40 and 3.8 m of 40x80 profile: rim-belt conveyor (two 6 mm PU round
  belts under the unrecorded rim, open centre), label gate with top and
  bottom colour cameras on the fly, inspection position with a lift and
  rotate chuck (75 mm lift, expanding mandrel, belt-driven servo), top and
  bottom 4k line-scans on opposite radii with coaxial, dark-field and
  backlights, two warp lasers, optional thermal camera, light-tight black
  tunnel with door switch, rodless-cylinder reject pusher with chute and
  bin, outfeed stop, cabinet, PLC, NixOS edge PC, HMI, stack light.
- render_station.py renders six views (open and closed iso, front, plan,
  inspection detail and elevation via clipped temporary documents).
- inline-qc-station-bom.csv: 32 lines with sources; USD 16k to 33k per
  station without the thermal camera.
- inline-optical-qc.md: new section "The station setup" (flow table,
  inspection zone, cycle ~5 to 6 s, control, BOM by group, open points);
  cost table updated (two launch stations ~USD 50k to 105k, still inside
  the metrology launch tier); Duplex/Classic flow now via the conveyor.
- README bullet and tree.
2026-10-18 13:29:55 +07:00

136 lines
4.9 KiB
Python

# Render the inline QC station views (run in the FreeCAD GUI after build_station.py).
# exec(open(r"C:/dev/cnx_vinyl/docs/equipment/inline-qc-station/render_station.py", encoding="utf-8").read())
# Detail views are rendered from a temporary document holding the model clipped
# to a box (Part common), because coin clip planes did not reach the render here.
import os
import FreeCAD as App
import FreeCADGui as Gui
import Part
OUTDIR = r"C:/dev/cnx_vinyl/docs/equipment/inline-qc-station"
doc = App.getDocument("InlineQCStation")
_vp = App.ParamGet("User parameter:BaseApp/Preferences/View")
_navi_before = _vp.GetBool("ShowNaviCube", True)
_vp.SetBool("ShowNaviCube", False)
def prep(d):
Gui.setActiveDocument(d.Name)
v = Gui.getDocument(d.Name).ActiveView
try:
v.setCameraType("Orthographic")
except Exception:
pass
for m in ("setNavigationCubeVisible", "setCornerCrossVisible"):
if hasattr(v, m):
getattr(v, m)(False)
return v
def members(group):
g = doc.getObject(group)
return list(g.Group) if g else []
def show_all():
for o in doc.Objects:
if o.TypeId == "Part::Feature":
o.ViewObject.Visibility = True
def hide(groups=(), names=()):
for gname in groups:
for o in members(gname):
o.ViewObject.Visibility = False
for n in names:
o = doc.getObject(n)
if o:
o.ViewObject.Visibility = False
def shoot(v, fname, orient, w=3200, h=2400):
getattr(v, orient)()
v.fitAll()
Gui.updateGui()
path = os.path.join(OUTDIR, fname)
v.saveImage(path, w, h, "White")
print("saved", path)
def clipped_doc(name, clipbox, skip_groups=()):
skip = set()
for gname in skip_groups:
skip.update(o.Name for o in members(gname))
d = App.newDocument(name)
for o in doc.Objects:
if o.TypeId != "Part::Feature" or o.Name in skip:
continue
s = o.Shape.common(clipbox)
if s.isNull() or s.Volume < 1e-3:
continue
n = d.addObject("Part::Feature", o.Name)
n.Shape = s
n.ViewObject.ShapeColor = o.ViewObject.ShapeColor
n.ViewObject.Transparency = o.ViewObject.Transparency
n.ViewObject.LineColor = o.ViewObject.LineColor
d.recompute()
return d
view = prep(doc)
enclosure_tops = ["PanelTop", "PanelFrontUpper", "PanelFrontLower", "GuardRejectFront", "GuardRejectTop", "DoorHandle"]
# 1 open iso: front panels and lid off, the rest of the enclosure translucent
show_all()
hide(names=enclosure_tops)
shoot(view, "inline-qc-station-iso.png", "viewIsometric")
# 2 closed iso: as it stands in the hall
show_all()
for o in members("Enclosure"):
o.ViewObject.Transparency = 15 if o.Name not in ("GuardRejectFront", "GuardRejectTop") else 70
shoot(view, "inline-qc-station-closed.png", "viewIsometric")
for o in members("Enclosure"):
o.ViewObject.Transparency = 55 if o.Name not in ("GuardRejectFront", "GuardRejectTop") else 75
# 3 front elevation without the front panels
show_all()
hide(names=enclosure_tops)
shoot(view, "inline-qc-station-front.png", "viewFront")
# 4 plan view without the lid and the top bridge
show_all()
hide(names=enclosure_tops + ["TopBeamF", "TopBeamB", "TopCrossLabel", "TopCrossLineScan", "TopCrossThermal",
"TopCrossBacklight", "RejectBridgeCross", "RodlessCylinder", "PusherCarriage",
"LabelCamTop", "LabelCamTopPost", "LabelCamTopPlate", "LineCamTop", "LineCamTopPost",
"LineCamTopPlate", "LightHangerTop", "CoaxHangerTop", "CoaxLightTop", "CoaxLightTopFace",
"BacklightForBottomCam", "BacklightForBottomCamFace", "BacklightBracketTop",
"LabelRingTop", "LabelRingTopFace", "ThermalCam", "ThermalPost",
"DarkFieldTopF", "DarkFieldTopB", "DarkFieldTopBracketF", "DarkFieldTopBracketB",
"StackLight0", "StackLight1", "StackLight2", "StackLightPole", "HMI", "HMIArm"])
shoot(view, "inline-qc-station-top.png", "viewTop")
show_all()
hide(names=enclosure_tops)
view.viewIsometric()
view.fitAll()
doc.save()
# 5 inspection elevation: x 600..1120, everything in front of y = -150 cut away
d5 = clipped_doc("QCDetailFront", Part.makeBox(520, 500, 1100, App.Vector(600, -150, 400)),
skip_groups=("Enclosure", "Electrical", "Reject"))
v5 = prep(d5)
shoot(v5, "inline-qc-station-detail-front.png", "viewFront", w=2400, h=2400)
App.closeDocument(d5.Name)
# 6 inspection zone, 3/4 view: x 600..1120, z 400..1500
d6 = clipped_doc("QCDetailIso", Part.makeBox(520, 700, 1100, App.Vector(600, -350, 400)),
skip_groups=("Enclosure", "Electrical", "Reject"))
v6 = prep(d6)
shoot(v6, "inline-qc-station-detail.png", "viewIsometric", w=2400, h=2400)
App.closeDocument(d6.Name)
Gui.setActiveDocument(doc.Name)
_vp.SetBool("ShowNaviCube", _navi_before)
print("done")