← Commit history

Fix translucent physical board silkscreen and substrate; preserve shared materials

John Lauer ·f08b9b3977 · ·parent 7e904e3
8 files changed +91−15
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-board",  "serde",@@ -34,7 +34,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.87"+version = "0.1.88" dependencies = [  "roxmltree",  "serde",@@ -43,7 +43,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-board",  "serde",@@ -52,7 +52,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -62,7 +62,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-board",  "serde",@@ -71,7 +71,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-board",  "serde",@@ -80,7 +80,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -90,7 +90,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.87"+version = "0.1.88" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -100,7 +100,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.87"+version = "0.1.88" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.87"+version = "0.1.88" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
docs/release-0.1.88.mdadded+5
@@ -0,0 +1,5 @@+# 0.1.88: readable physical board silkscreen++Some STEP conversions give the substrate and physical ink translucent materials. In the board viewer this can make labels disappear beneath solder mask despite correct geometry. `board-glb copper` now clones those materials by board-layer identity and makes them opaque, preserving their colors, mesh data, shared component materials and solder-mask transparency. `board-glb check` rejects translucent substrate or physical ink.++Regression tests cover shared materials, repeated mesh instances, unchanged geometry, idempotence and rejection of transparent input. Component-hero reference overlays remain translucent; this correction applies to physical board layers. A recorded electronic-load before/after review confirms readable bottom labels. This is a rendering correction, not manufacturing or thermal qualification.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.87",+  "version": "0.1.88",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.87",+  "version": "0.1.88",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
skills/aiflow-molecule/SKILL.md+2−1
@@ -87,7 +87,8 @@ Pads and test points are always real copper. A footprint has a copper pad, a sol - **Copper where the mask is open.** Exposed copper shows as copper, or as the board's stated finish when the spec names one (`"fab": {"finish": "enig" | "hasl"}`, or `--finish`); the default is copper. KiCad's STEP gives the pads a bare 0.5 grey with no metal or roughness, which glTF renders as rough metal: grey, or pale peach once recoloured. `board-glb copper` finds the pad, via and track meshes by their KiCad layer name (`<board>_pad`, `_via`, `_copper`), never by a material index, and gives them a copper material with explicit metal and roughness. - **The mask opens where the footprint says.** The mask is rebuilt as the board outline minus every F.Mask / B.Mask aperture: each pad grown by its mask margin (pad, else footprint, else the board's `pad_to_mask_clearance`), mask graphics and zones, untented vias, drill holes, and KiCad's minimum mask web. Never a blanket sheet over the pads, and never lift the pads through the mask. - **Paste only where paste exists.** Test points, machine contacts and pins have no paste and stay bare copper. `--paste solder` shows reflowed solder on the F.Paste / B.Paste apertures of fitted parts (paste margin and ratio applied, DNP parts skipped) and nowhere else; the default shows every exposed pad as copper.-- **The gate.** `board-glb check` fails when a pad material is outside the finish's colour range or leaves metal/roughness at the glTF default, when the mask covers any exposed pad centre (or is still the compressed export, whose openings cannot be proven), when solder sits on a pad without paste, or when the GLB's pads do not line up with the board file (a stale `.kicad_pcb`). Use the `.kicad_pcb` the STEP was exported from.+- **Readable physical silkscreen.** `board-glb copper` makes the PCB substrate and physical silkscreen opaque by semantic layer, cloning materials so shared component materials and solder-mask transparency remain unchanged. STEP converters can emit translucent ink that disappears beneath the mask despite correct geometry. Inspect both faces in the actual viewer; this rule does not apply to translucent component-hero reference overlays.+- **The gate.** `board-glb check` fails when substrate or physical silkscreen materials are transparent, when a pad material is outside the finish's colour range or leaves metal/roughness at the glTF default, when the mask covers any exposed pad centre (or is still the compressed export, whose openings cannot be proven), when solder sits on a pad without paste, or when the GLB's pads do not line up with the board file (a stale `.kicad_pcb`). Use the `.kicad_pcb` the STEP was exported from. - **Look at it.** Render a close-up of the test points and of a pad beside a part (headless three.js, the board's own GLB) before publishing. Grey pads are never shipped.  What the step2glb service should do itself is tracked on adom/adom-step2glb issue 24; until it does, `board-glb copper` is the step that makes it right.
tools/board-glb-copper.py+35−1
@@ -29,7 +29,7 @@ Usage:                        [--finish copper|enig|hasl] [--paste solder|none] [--report r.json]   board-glb-copper.py check  --glb out.glb --board board.kicad_pcb [--finish ...] [--report r.json] """-import argparse, json, math, re, struct, sys+import argparse, copy, json, math, re, struct, sys from pathlib import Path  import numpy as np@@ -603,6 +603,30 @@ def add_node_like(g, ref_node, mesh_index, name):     return ni  +def opaque_board_materials(g, layers, changes=None):+    """Clone transparent substrate/ink materials; never alter shared component or mask materials."""+    replacements = {}+    for role in ('pcb', 'silkscreen'):+        for mi in sorted({mi for mi, _ in layers.get(role, [])}):+            for primitive in g['meshes'][mi]['primitives']:+                old = primitive.get('material')+                material = g.get('materials', [])[old] if old is not None else {}+                rgba = material.get('pbrMetallicRoughness', {}).get('baseColorFactor', [1, 1, 1, 1])+                if material.get('alphaMode', 'OPAQUE') == 'OPAQUE' and rgba[3] == 1:+                    continue+                key = (role, old)+                if key not in replacements:+                    cloned = copy.deepcopy(material)+                    cloned['name'] = material.get('name', 'default') + '_opaque_' + role+                    cloned['alphaMode'] = 'OPAQUE'+                    cloned.setdefault('pbrMetallicRoughness', {})['baseColorFactor'] = list(rgba[:3]) + [1]+                    replacements[key] = len(g.setdefault('materials', []))+                    g['materials'].append(cloned)+                primitive['material'] = replacements[key]+    if changes is not None and replacements:+        changes.append(f'opaque substrate/ink: {len(replacements)} cloned material(s); shared components and mask preserved')++ # ---------------------------------------------------------------- copper def copper(args):     fin = finish_name(args.finish)@@ -612,6 +636,7 @@ def copper(args):     fr = Frame(g, layers, L['edge'])     rep = {'tool': 'adom-aiflow board-glb copper', 'version': VERSION, 'finish': fin, 'paste': args.paste,            'board': str(args.board), 'glb': str(args.glb), 'changes': []}+    opaque_board_materials(g, layers, rep['changes'])     pcb_top = fr.z_in_pcb(fr.pcb_mesh, fr.pcb_node, 'max'); pcb_bot = fr.z_in_pcb(fr.pcb_mesh, fr.pcb_node, 'min')      rgb, metal, rough = FINISHES[fin]@@ -807,6 +832,15 @@ def check(args):         fr = Frame(g, layers, L['edge'])     except SystemExit as e:         errors.append(str(e)); fr = None+    # Opaque ink and substrate avoid transparent depth sorting beneath the mask.+    for role in ('pcb', 'silkscreen'):+        for mi in sorted({mi for mi, _ in layers.get(role, [])}):+            for primitive in g['meshes'][mi]['primitives']:+                idx = primitive.get('material')+                material = g.get('materials', [])[idx] if idx is not None else {}+                rgba = material.get('pbrMetallicRoughness', {}).get('baseColorFactor', [1, 1, 1, 1])+                if material.get('alphaMode', 'OPAQUE') != 'OPAQUE' or rgba[3] != 1:+                    errors.append(f'{role} material is transparent: substrate and silkscreen must be opaque')     # pad materials     pad_meshes = layers.get('pad', [])     if not pad_meshes:
tools/test-board-glb-copper.py+36
@@ -116,4 +116,40 @@ with tempfile.TemporaryDirectory() as d:      # 6. never overwrite the input     assert run('copper', '--glb', raw, '--board', board, '--out', raw)[0] != 0+    # Transparent export materials must be fixed without changing shared components or mesh data.+    import copy+    transparent, payload = m.read_glb(fixed)+    transparent['materials'][0]['alphaMode'] = 'BLEND'+    transparent['materials'][0]['pbrMetallicRoughness']['baseColorFactor'][3] = 0.98+    ink = copy.deepcopy(transparent['meshes'][0]); ink['name'] = 'test_silkscreen'+    component = copy.deepcopy(ink); component['name'] = 'component_body'+    for mesh in (ink, component):+        transparent['meshes'].append(mesh)+        transparent['nodes'].append({'mesh': len(transparent['meshes']) - 1})+    transparent['nodes'].append({'mesh': len(transparent['meshes']) - 2})  # repeated ink instance+    original = copy.deepcopy(transparent)+    bad = d / 'transparent.glb'; m.write_glb(bad, transparent, payload)+    code, out = run('check', '--glb', bad, '--board', board)+    assert code == 1 and 'pcb material is transparent' in out and 'silkscreen material is transparent' in out, out+    layers = m.board_meshes(transparent, 'test')+    m.opaque_board_materials(transparent, layers)+    assert transparent['materials'][:len(original['materials'])] == original['materials']+    assert len(transparent['materials']) == len(original['materials']) + 2+    assert transparent['meshes'][-1] == original['meshes'][-1], 'shared component stays untouched'+    for role in ('pcb', 'silkscreen'):+        for mi, _ in layers[role]:+            before = copy.deepcopy(original['meshes'][mi]); after = copy.deepcopy(transparent['meshes'][mi])+            for a, b in zip(before['primitives'], after['primitives']):+                material = transparent['materials'][b.pop('material')]; a.pop('material')+                assert material['alphaMode'] == 'OPAQUE'+                assert material['pbrMetallicRoughness']['baseColorFactor'] == [0.42, 0.45, 0.29, 1]+            assert before == after, 'geometry unchanged'+    once = copy.deepcopy(transparent)+    m.opaque_board_materials(transparent, layers)+    assert once == transparent, 'normalization is idempotent'+    assert transparent['nodes'] == original['nodes'] and transparent['accessors'] == original['accessors']+    repaired = d / 'opaque.glb'+    code, out = run('copper', '--glb', bad, '--board', board, '--out', repaired)+    assert code == 0, out+    assert run('check', '--glb', repaired, '--board', board)[0] == 0 print('PASS: grey/masked pads refused; copper by construction from pad, mask and paste layers; bare test points; finishes; stale board refused')