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.
This commit is contained in:
grabowski committed 2026-10-18 13:29:55 +07:00
1 parent 5207808b57
commit 609424b598
12 files changed
+676 -15

No files matched your search

+155 -14
View File
@@ -19,8 +19,11 @@ parts:
- a servo turntable with a centring chuck;
- an edge PC running open-source vision software on NixOS.
The hardware costs about USD 12k to 30k per station (estimate). The launch
plant needs two stations, one for the Duplex cell and one for the Classic.
The hardware costs about USD 16k to 33k per station (bill of materials,
partly estimates). The launch plant needs two stations, one for the Duplex
cell and one for the Classic. A concept model of the station, an aluminium
profile frame with a rim-belt conveyor and all sensors mounted to it, is
in [the station setup](#the-station-setup-frame-conveyor-and-sensors).
## What the station has to catch
@@ -133,22 +136,160 @@ each cell needs no conveyor and catches a non-fill streak within a cycle
or two. The calculator's iso view draws a conveyor to one optical
station; that drawing is schematic.
## The station setup: frame, conveyor and sensors
A concept model of one station, drawn in FreeCAD from the script
[`build_station.py`](inline-qc-station/build_station.py) (views from
[`render_station.py`](inline-qc-station/render_station.py); model file
[`inline-qc-station.FCStd`](inline-qc-station/inline-qc-station.FCStd)).
It is a layout to take to a machine builder, not a fabrication drawing:
the profiles are drawn as plain 40 mm bars and the bought parts as their
outline boxes.
![Inline QC station, open: front panels and lid removed](inline-qc-station/inline-qc-station-iso.png)
*Records run from the infeed (back left, a translucent red special on
the belts) through the light-tight tunnel to the reject pusher and the
outfeed stop (front right). The cabinet sits under the outfeed, the
reject bin behind it.*
**Frame.** The frame is built from 40 × 40 mm 8-slot aluminium profile
(27 m in 37 pieces) on two 40 × 80 mm base beams, with eight levelling
feet. It is 1.9 m long, 0.65 m wide and 1.5 m high, plus the stack
light. The reject bin adds 0.4 m behind it. Records travel at 900 mm,
a normal working height.
The middle 0.8 m is a tunnel closed with black panels on all sides
(4.3 m²), because the line-scans need their own light only. The front
panel is a door with a safety switch. The records enter and leave
through low slots in the end walls. The reject zone has a clear polycarbonate guard.
All camera and light brackets clamp into the slots, so heights and angles
can be moved during the lighting trials.
**Conveyor.** Two 6 mm PU round belts run under the record's rim, 148.5 mm
from the centre line. That is outside the recorded area: a 12" record is
301.6 mm across and its grooves end at 292.6 mm at most (IEC 98, as
[tabulated here](http://www7a.biglobe.ne.jp/~yosh/standards_table.htm)).
The belt therefore touches only the raised edge that records are made to
stand on, never a groove. The centre stays open, so cameras and the chuck
can reach the record from below.
The belts run on UHMW slide strips and are driven by one shaft at the
outfeed end. Side guides with a flared mouth centre the record at the
infeed. Supported on two rim lines, a warm 180 g record sags about 1 mm
in the middle (beam estimate); the trials should confirm that this does
no harm. For 10" and 7" records the belt spacing moves in on a slide;
the model shows the 12" setting.
| Station | Position from the infeed end | What happens |
|---|---|---|
| Infeed | 200 mm | the operator (Duplex) or the unloader (Classic) puts the record down; a presence sensor starts the cycle |
| Label gate | 520 mm | both colour cameras flash the labels on the fly through the open centre (exposure about 20 µs, under 10 µm blur at 0.5 m/s); the top camera also reads the dead-wax code that ties the record to its stamper, and so to its press and cycle (readability to test, as in [digital factory](../digital-factory.md)) |
| Inspection | 880 mm | a through-beam on the leading edge stops the belts; the chuck lifts the record 75 mm off them (the pad waits below the belt plane), expands the mandrel in the 7.24 mm hole and turns it once in about 1 s |
| Reject | 1,320 mm | a pneumatic rodless cylinder on a bridge pushes a reject sideways by its edge onto a felt-lined chute at the back and into the bin |
| Outfeed | 1,715 mm | good records wait at a soft end stop; the station holds the next one while the outfeed is full |
![Inspection zone: chuck, line-scan cameras, lights and lasers](inline-qc-station/inline-qc-station-detail.png)
![Inspection zone in elevation, the front part cut away](inline-qc-station/inline-qc-station-detail-front.png)
**Inspection zone.** The record is shown lifted on the chuck. The two
line-scans look at opposite radii along the conveyor axis, because no
belt crosses that line:
- **Top camera:** reads the downstream radius, from 50 to 155 mm, from
330 mm above. A coaxial line light sits under it, two dark-field line
lights sit at 20° either side, and a diffuse backlight below the
record gives transmitted light for clear colours.
- **Bottom camera:** reads the upstream radius from below with the same
set of lights.
- **Bottom dark-field lights:** these sit outside the record's footprint,
above the rails, so the conveyed record passes them. They light the
underside at about 8°, true dark-field.
- **Warp lasers:** two lasers under the record trace circles at 90 and
130 mm radius during the turn.
- **Spindle drive:** the spindle servo's encoder clocks both line-scans
and the lasers.
- **Lift drive:** the lift is a guided pneumatic cylinder, and the
spindle is belt-driven from a servo set to the side. That keeps the
bottom camera's view cone clear, which the elevation shows.
- **Thermal camera (option):** hangs at the tunnel entry.
**Cycle per record (estimate).**
| Step | Time |
|---|---|
| Convey to the inspection position | 1.5 to 2.5 s |
| Lift and clamp | 0.5 s |
| One turn, with run-up and stop | 1.3 s |
| Lower | 0.3 s |
| Convey out, with a push if rejected | 1 to 2 s |
| **Total** | **about 5 to 6 s** |
A Duplex cell delivers a record every 15 to 20 s and the Classic one
every 28 s. The PC finishes the verdict while the record is lowered and
carried to the pusher.
**Control.** A small PLC (Mitsubishi FX5U or Delta class, both common in
Thailand) runs the conveyor, lift, mandrel, spindle and pusher, plus the
safety circuit (e-stop, door switch, safety relay). The NixOS edge PC
under the outfeed takes the four GigE cameras on PoE, the lasers and the
light controllers. It sends pass or fail with a reason to the PLC, and
the result string and images to the factory system (see
[digital factory](../digital-factory.md)). Utilities are 230 V single
phase at about 1 kW and 6 bar compressed air (estimates).
![Front elevation, front panels removed](inline-qc-station/inline-qc-station-front.png)
![Plan view, lid and top bridge removed](inline-qc-station/inline-qc-station-top.png)
![The station closed, as it stands in the hall](inline-qc-station/inline-qc-station-closed.png)
**Bill of materials.** The line-by-line list with sources is in
[`inline-qc-station-bom.csv`](inline-qc-station/inline-qc-station-bom.csv).
By group, per station:
| Group | USD | Main parts |
|---|---|---|
| Frame | 0.7k to 1.5k | 27 m of 40 × 40, 3.8 m of 40 × 80, brackets, feet |
| Enclosure and guards | 0.3k to 0.7k | black panels, polycarbonate, door switch |
| Conveyor | 0.5k to 1.4k | round belts, pulleys, slide strips, drive |
| Handling | 2.0k to 4.9k | guided lift cylinder, 200 W spindle servo, spindle head with mandrel, rodless cylinder, pneumatics |
| Vision | 7.7k to 12.5k | 2 line-scans, 2 colour cameras, lenses, 2 coaxial, 4 dark-field and 2 backlights, 2 ring lights, controllers |
| Sensors | 1.3k to 4.0k | 2 laser distance sensors, presence and stop sensors |
| Control | 3.5k to 7.8k | PLC, edge PC with GPU, HMI, safety, cabinet and wiring |
| **Total** | **16k to 33k (0.53M to 1.08M THB)** | thermal camera (USD 2.1k to 3.0k) not included |
**Open points for the machine builder.**
1. **Sag:** confirm that the rim-only support does not mark or bend warm
records.
2. **Static:** fresh PVC attracts dust, so an ionising bar at the infeed
may be needed. Otherwise the station counts dust it attracted itself.
3. **Classic hand-off:** where and how the Classic's unloader sets the
record down.
4. **Belt width:** a quick-change spacing for 10" and 7" records.
5. **Space in the cell:** check that the 1.9 m length fits the 5 × 3 m
Duplex cell next to the trimmer.
## Where it sits
- **Duplex cell.** The station sits after the edge trimmer, in front of
the operator, before the spindle. The flow per record:
1. The operator puts the record on the chuck.
2. A presence sensor starts the scan.
3. A stack light shows green or red.
4. A reject goes into a bin, and a small screen shows the reason.
1. The operator puts the trimmed record on the infeed conveyor.
2. A presence sensor starts the cycle.
3. A stack light and the HMI show green or red.
4. A reject is pushed into a bin, and the HMI shows the reason; a good
record waits at the outfeed for the spindle.
The operator still sees every record, so the visual check stays as it
is today.
- **Classic.** The Classic unloads by itself onto the spindle stack. Ask
Newbilt whether the unload handling can set the record on an inspection
chuck first, or hand it over to a small pick-and-place unit by I/O. If
neither works, use a fixed area camera over the unload position. That
covers surface and label, and leaves warp and eccentricity to samples.
Newbilt whether the unload handling can set the record down on the
station's infeed instead; a conveyor takes a record dropped within a few
millimetres, which is easier than hitting a chuck. If that does not
work, use a fixed area camera over the unload position. That covers
surface and label, and leaves warp and eccentricity to samples.
- **Warm records.** Records leave the press warm and settle in the stack;
24 hours of cooling is the usual practice
([Stamper Discs](https://stamperdiscs.com/horns-and-quality-issues-in-vinyl-record-pressing)).
@@ -197,11 +338,11 @@ simulated.
| Item | Estimate |
|---|---|
| Hardware per station, from the table above | USD 12k to 30k |
| First station: mechanics, lighting trials, software, training data | 2 to 4 person-months, USD 15k to 40k (our own team with a local machine builder) |
| Launch plant: two stations, Duplex cell and Classic | about USD 40k to 100k (1.3M to 3.3M THB) |
| Hardware per station, from the [bill of materials](inline-qc-station/inline-qc-station-bom.csv) | USD 16k to 33k (0.53M to 1.08M THB) |
| First station: frame build and machined parts, lighting trials, software, training data | 2 to 4 person-months, USD 15k to 40k (our own team with a local machine builder) |
| Launch plant: two stations, Duplex cell and Classic | about USD 50k to 105k (1.6M to 3.5M THB) |
All three figures are estimates. They fit inside the existing metrology
All three figures are estimates; the bill of materials names a source for each priced line. They fit inside the existing metrology
launch-tier line, USD 170k to 460k for XRF, inline vision and the audio
station ([total investment](../finance/total-investment.md)). The plan
prices therefore do not change. That line has no published split, so the
@@ -0,0 +1,347 @@
# 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: profiles are drawn as plain 40 mm bars,
# bought parts as their envelope boxes. Sizes follow ../inline-optical-qc.md.
#
# 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)
def prof(group, name, x0, y0, z0, x1, y1, z1):
return add(group, name, box(x0, y0, z0, x1, y1, z1), ALU)
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, 1080, "End1")]:
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"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)
xt = sum(TOP_LINE) / 2
prof(G, "TopCrossLineScan", xt - 20, -YF + 20, 1460, xt + 20, 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, "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, "RejectBridgeCross", X_RJ - 20, -YF + 20, 1040, X_RJ + 20, YF - 20, 1080)
# ---------------------------------------------------------------- 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"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)
# 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)
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))
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)
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)
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)
# ---------------------------------------------------------------- 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)
add(G, "LiftPost", box(X_IS - 20, -20, 460, X_IS + 20, 20, zp - 322), ALU)
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)
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 - 10, s * 100 - 5, zr + 48, xt + 10, 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, "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)
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)
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)
add(G, "LaserRail", box(X_IS - 20, -200, 810, X_IS + 20, -70, 830), ALU)
# ---------------------------------------------------------------- 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
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))
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))
add(G, f"ChuteWall{xw}", w, (0.55, 0.57, 0.60))
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, 160, 1640, 170, 770), CABINET)
add(G, "CabinetHandle", box(1600, -215, 400, 1615, -200, 520), DARK)
add(G, "StackLightPole", cyl(10, 120, V(XT1 + 20, -YF, 1503)), 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))
# ---------------------------------------------------------------- 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 = 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)
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)
@@ -0,0 +1,34 @@
group,item,qty,example,unit_usd_low,unit_usd_high,line_usd_low,line_usd_high,source
Frame,"40x40 slot profile, cut to length",27 m (37 pieces),"8-slot 4040 (Misumi HFS8-4040 class, or Thai/Chinese generic)",11,21,297,567,per m; Shopee Thailand listing from THB 356; aluxprofile EUR 19 to 24 per m
Frame,"40x80 slot profile, base beams",3.8 m (2 pieces),8-slot 4080,22,42,84,160,"per m; estimate, about twice the 4040 price"
Frame,"Angle brackets, T-nuts, screws, end caps",1 lot (about 120 joints),,250,600,250,600,estimate
Frame,Levelling feet M16,8,,8,25,64,200,each; estimate
Enclosure,"Black panels, 4 mm ACM or PP, light-tight tunnel",4.3 m2,,25,60,108,258,per m2; estimate
Enclosure,"Clear polycarbonate guard, reject zone",1.4 m2,,30,70,42,98,per m2; estimate
Enclosure,"Door hinges, handle, safety door switch",1 lot,,120,350,120,350,estimate
Conveyor,"PU round belt 6 mm, welded endless",2 x 3.8 m,,15,40,30,80,"each; PU round belt listings (RS, Amazon) plus welding"
Conveyor,"Round-belt pulleys, shaft, bearings, UHMW slide strips, side guides",1 lot,,250,700,250,700,"pulleys USD 5 to 11 each (ServoCity, REV); rest estimate"
Conveyor,"Conveyor drive: small gear motor with drive, or servo",1,,250,600,250,600,estimate
Handling,"Guided lift cylinder, 20 mm bore, 75 mm stroke",1,SMC MGPM20-75,330,450,330,450,SMC MGPM20-30 USD 322 (Misumi US) to 390 (Motion); 75 mm stroke a little more
Handling,Spindle servo 200 W with drive (encoder triggers the line-scans),1,Delta ASDA-B3 class,300,700,300,700,drive USD 119 to 200 (listings); motor and cable estimate
Handling,"Spindle head: bearing housing, timing belt, label pad, pneumatic expanding mandrel",1,machined parts,600,1800,600,1800,estimate
Handling,"Rodless cylinder 25 mm, 350 mm stroke, with paddle",1,SMC MY1B / CY3B class,400,900,400,900,estimate
Handling,"Valve terminal, FRL, tubing, fittings",1 lot,,400,1000,400,1000,estimate
Vision,"4k monochrome line-scan camera, GigE",2,Basler racer 2 r2L4096 / Hikrobot MV-CL042-91GM,857,1088,1714,2176,Basler web shop via price listing
Vision,"Line-scan lens, 50 mm",2,,300,800,600,1600,estimate
Vision,Coaxial line light,2,,295,446,590,892,made-in-china listing USD 295 to 349; Vision Datum USD 420; light186 from USD 446
Vision,"Dark-field line light, about 150 mm",4,Vision Datum LiN+ class,250,400,1000,1600,Vision Datum LiN+ USD 380
Vision,Diffuse line backlight,2,,200,400,400,800,estimate
Vision,"Colour area camera, 8 MP, GigE",2,Basler ace 2 a2A2840-14gcPRO,1119,1119,2238,2238,Basler web shop via price listing
Vision,Lens 16 mm,2,,150,400,300,800,estimate
Vision,Ring light,2,,150,400,300,800,estimate
Vision,Strobe and light controllers,2,,300,800,600,1600,estimate
Sensors,"Laser distance sensor, 50 mm range",2,Micro-Epsilon optoNCDT 1420,520,1600,1040,3200,"list from EUR 470 (Micro-Epsilon); ILD1420-25 about USD 1,600 (Radwell)"
Sensors,Photoelectric presence sensors and one through-beam,5,,50,150,250,750,each; estimate
Sensors,"Thermal camera (option, not in the total)",0 (1 optional),Optris Xi 400,2100,3000,0,0,"from EUR 1,950 (Optris); USD 2,995 (moviTHERM)"
Control,PLC for sequence and safety I/O,1,Mitsubishi FX5U-32MT or Delta class,500,1096,500,1096,"FX5U-32MT/ES USD 1,096 (Mitsubishi US shop); resale from about USD 470"
Control,"Edge PC with GPU, 4 GigE PoE ports, NixOS",1,,1500,3200,1500,3200,"qscompute buying guides 2026: USD 1,490 to 3,200"
Control,"HMI touch panel, 10 inch",1,,400,900,400,900,estimate
Control,"Safety relay, e-stop, door interlock wiring",1 lot,,300,600,300,600,estimate
Control,"Cabinet, power supply, breakers, terminals, wiring, stack light",1 lot,,800,2000,800,2000,estimate
Total,"Hardware per station, thermal camera excluded",,,,,16056,32715,sum of the lines above
1 group item qty example unit_usd_low unit_usd_high line_usd_low line_usd_high source
2 Frame 40x40 slot profile, cut to length 27 m (37 pieces) 8-slot 4040 (Misumi HFS8-4040 class, or Thai/Chinese generic) 11 21 297 567 per m; Shopee Thailand listing from THB 356; aluxprofile EUR 19 to 24 per m
3 Frame 40x80 slot profile, base beams 3.8 m (2 pieces) 8-slot 4080 22 42 84 160 per m; estimate, about twice the 4040 price
4 Frame Angle brackets, T-nuts, screws, end caps 1 lot (about 120 joints) 250 600 250 600 estimate
5 Frame Levelling feet M16 8 8 25 64 200 each; estimate
6 Enclosure Black panels, 4 mm ACM or PP, light-tight tunnel 4.3 m2 25 60 108 258 per m2; estimate
7 Enclosure Clear polycarbonate guard, reject zone 1.4 m2 30 70 42 98 per m2; estimate
8 Enclosure Door hinges, handle, safety door switch 1 lot 120 350 120 350 estimate
9 Conveyor PU round belt 6 mm, welded endless 2 x 3.8 m 15 40 30 80 each; PU round belt listings (RS, Amazon) plus welding
10 Conveyor Round-belt pulleys, shaft, bearings, UHMW slide strips, side guides 1 lot 250 700 250 700 pulleys USD 5 to 11 each (ServoCity, REV); rest estimate
11 Conveyor Conveyor drive: small gear motor with drive, or servo 1 250 600 250 600 estimate
12 Handling Guided lift cylinder, 20 mm bore, 75 mm stroke 1 SMC MGPM20-75 330 450 330 450 SMC MGPM20-30 USD 322 (Misumi US) to 390 (Motion); 75 mm stroke a little more
13 Handling Spindle servo 200 W with drive (encoder triggers the line-scans) 1 Delta ASDA-B3 class 300 700 300 700 drive USD 119 to 200 (listings); motor and cable estimate
14 Handling Spindle head: bearing housing, timing belt, label pad, pneumatic expanding mandrel 1 machined parts 600 1800 600 1800 estimate
15 Handling Rodless cylinder 25 mm, 350 mm stroke, with paddle 1 SMC MY1B / CY3B class 400 900 400 900 estimate
16 Handling Valve terminal, FRL, tubing, fittings 1 lot 400 1000 400 1000 estimate
17 Vision 4k monochrome line-scan camera, GigE 2 Basler racer 2 r2L4096 / Hikrobot MV-CL042-91GM 857 1088 1714 2176 Basler web shop via price listing
18 Vision Line-scan lens, 50 mm 2 300 800 600 1600 estimate
19 Vision Coaxial line light 2 295 446 590 892 made-in-china listing USD 295 to 349; Vision Datum USD 420; light186 from USD 446
20 Vision Dark-field line light, about 150 mm 4 Vision Datum LiN+ class 250 400 1000 1600 Vision Datum LiN+ USD 380
21 Vision Diffuse line backlight 2 200 400 400 800 estimate
22 Vision Colour area camera, 8 MP, GigE 2 Basler ace 2 a2A2840-14gcPRO 1119 1119 2238 2238 Basler web shop via price listing
23 Vision Lens 16 mm 2 150 400 300 800 estimate
24 Vision Ring light 2 150 400 300 800 estimate
25 Vision Strobe and light controllers 2 300 800 600 1600 estimate
26 Sensors Laser distance sensor, 50 mm range 2 Micro-Epsilon optoNCDT 1420 520 1600 1040 3200 list from EUR 470 (Micro-Epsilon); ILD1420-25 about USD 1,600 (Radwell)
27 Sensors Photoelectric presence sensors and one through-beam 5 50 150 250 750 each; estimate
28 Sensors Thermal camera (option, not in the total) 0 (1 optional) Optris Xi 400 2100 3000 0 0 from EUR 1,950 (Optris); USD 2,995 (moviTHERM)
29 Control PLC for sequence and safety I/O 1 Mitsubishi FX5U-32MT or Delta class 500 1096 500 1096 FX5U-32MT/ES USD 1,096 (Mitsubishi US shop); resale from about USD 470
30 Control Edge PC with GPU, 4 GigE PoE ports, NixOS 1 1500 3200 1500 3200 qscompute buying guides 2026: USD 1,490 to 3,200
31 Control HMI touch panel, 10 inch 1 400 900 400 900 estimate
32 Control Safety relay, e-stop, door interlock wiring 1 lot 300 600 300 600 estimate
33 Control Cabinet, power supply, breakers, terminals, wiring, stack light 1 lot 800 2000 800 2000 estimate
34 Total Hardware per station, thermal camera excluded 16056 32715 sum of the lines above
Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

@@ -0,0 +1,135 @@
# 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")