Files
grabowski e8674795f5 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.
2026-10-18 14:07:41 +07:00

153 lines
5.6 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)
_anim_before = _vp.GetBool("UseNavigationAnimations", True)
_vp.SetBool("UseNavigationAnimations", False)
_presel_before = _vp.GetBool("EnablePreselection", True)
_vp.SetBool("EnablePreselection", False)
Gui.Selection.clearPreselection()
Gui.Selection.clearSelection()
from pivy import coin
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)()
Gui.updateGui()
v.fitAll()
Gui.updateGui()
# move the orthographic camera well back so no part sits behind the near plane
c = v.getCameraNode()
d = c.orientation.getValue().multVec(coin.SbVec3f(0, 0, -1))
c.position.setValue(c.position.getValue() - d * 8000)
c.nearDistance.setValue(100)
c.farDistance.setValue(32000)
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)
_vp.SetBool("UseNavigationAnimations", _anim_before)
_vp.SetBool("EnablePreselection", _presel_before)
print("done")