diff --git a/docs/stage2_real_sorter/drop-validation.json b/docs/stage2_real_sorter/drop-validation.json index a8c6ff2..dac037f 100644 --- a/docs/stage2_real_sorter/drop-validation.json +++ b/docs/stage2_real_sorter/drop-validation.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-30T11:23:42.086Z", + "generatedAt": "2026-07-30T12:27:01.613Z", "sim": "headless Rapier via simulateDrop (deterministic, dt=1/60, settle 6s)", "runs": 10, "routes": [ diff --git a/docs/stage2c_cad_visual/README.md b/docs/stage2c_cad_visual/README.md new file mode 100644 index 0000000..3a3d572 --- /dev/null +++ b/docs/stage2c_cad_visual/README.md @@ -0,0 +1,5 @@ +# Stage 2C — CAD visual / physics correction + +Working branch: `feature/ozon-sorter-stage-2c-cad-visual-physics` + +See `STAGE2C_REPORT.md` for the full decision record. diff --git a/docs/stage2c_cad_visual/STAGE2C_REPORT.md b/docs/stage2c_cad_visual/STAGE2C_REPORT.md new file mode 100644 index 0000000..7e2b47a --- /dev/null +++ b/docs/stage2c_cad_visual/STAGE2C_REPORT.md @@ -0,0 +1,159 @@ +# STAGE 2C REPORT — CAD Assembly Correction / Visual Cleanup / Runtime Physics + +## 1. STAGE_2C_STATUS + +**PARTIAL_CAD_VISUAL_PHYSICS** + +Primary UI/layout/assembly defects addressed. Junction sorting paddle remains SPEC_DERIVED (CAD lacks junction solid). Formal GPU multi-run profiles and runtime WebM not completed. Public domain still blocked. + +## 2. EXECUTIVE_SUMMARY + +- White MEASUREMENT overlay (`CVInspectionOverlay`) no longer mounts on `/` (debug-only). +- Canvas fills viewport (`100dvh` + absolute `.canvas-holder`); emptyRatio=0 measured. +- Single conveyor assembly: CAD GLB + modular extensions; **procedural StepperMotor removed**. +- Author CAD audited: only `3d_models/conveer.FCStd` machine CAD; product STLs separate. +- Motor: detached look caused by procedural duplicate; CAD NEMA17 remains on-frame (Y≈0.58). +- Opaque machine materials forced (`transparent=false`, `depthWrite=true`). +- Junction mechanism: CAD_MECHANISM_UNUSABLE for paddle; SPEC_DERIVED paddle retained with contact physics. +- Runtime/headless config hash equal; drop validation 7/7×10/10. +- Production `:3100` updated with Stage 2C dist; `sorter.arhipovdan.ru` still blocked. + +## 3. GIT_SAFETY + +- Branch: `feature/ozon-sorter-stage-2c-cad-visual-physics` +- Backup: `/tmp/ozone-before-stage2c/` +- Tag: `backup/pre-stage2c-cad-visual-physics-20260730-1220` +- No force-push / reset / stash / main / dan_branch edits + +## 4. CAD_SOURCE_INVENTORY + +See `cad-source-inventory.json`. Author machine CAD: **one** FCStd (`3d_models/conveer.FCStd`, 42 PartDesign bodies). Runtime GLB `public/models/sorter/conveyor-web.glb`. Contact sheet: `screenshots/cad-contact-sheet.png`. + +## 5. CAD_RUNTIME_AUDIT + +See `cad-runtime-audit.json`, `cad-node-hierarchy.json`. Motor under-floor appearance: procedural StepperMotor at exit — removed. CAD motor AABB on frame. + +## 6. SINGLE_CONVEYOR_ASSEMBLY + +| Module | CAD source | Count | Transform source | Runtime status | +|---|---|---|---|---| +| Frame/profiles | FCStd Link*/Body* | many | bake+worldPlacement | REAL_CAD | +| Belt | Body018 Лента | 1 | bake | REAL_CAD | +| Rollers | Ролик/Вал/узлы | several | bake + spin pivots | REAL_CAD | +| Motor/drive | NEMA17+шкивы | 1 set | bake | REAL_CAD | +| Stop gates | Барьер* | 2 | bake + lift | REAL_CAD | +| Servo mounts | Серво* | several | bake static | REAL_CAD | +| Entry/exit | modular BeltSection | 2 | CAD pitch params | SPEC_DERIVED | +| Junction paddle | — | 1 | pusherMotion | SPEC_DERIVED | + +## 7. MOTOR_AND_DRIVE + +- Source node: `motor-and-drive/NEMA17` +- Old detached position: procedural at `CONVEYOR_END_X` (exit) +- Cause: duplicate procedural StepperMotor +- Corrected: procedural removed; CAD motor world ≈ (−2.04, 0.58, 0.25) +- Screenshot: `screenshots/04-motor-correct-position.png` + +## 8. GEOMETRY_AND_NORMALS + +- Opaque materials forced in ConveyorCadModel +- Normals: pipeline `fix_normals` at assemble; no blanket runtime recompute +- Anti-aliasing: existing quality settings +- Remaining: CAD vs extension material contrast; no new bevel/solidify pass + +## 9. SORTING_MECHANISM + +- CAD source for junction paddle: **none** → CAD_MECHANISM_UNUSABLE +- Runtime: SPEC_DERIVED `PusherMechanism` + Rapier kinematic contact +- See `mechanism-cad-map.md` + +## 10. UI_AND_LAYOUT + +- Measurement full panel: `/?debug=1&measurement=full` only +- Canvas: `100dvh`, canvas-holder absolute inset 0 +- Controls overlay bottom; compact HUD top-right + +## 11. REAL_MODEL_SCALE + +Ground-truth mm preserved (no artificial enlargement). Belt 500 mm reference unchanged. + +| SKU | Expected mm | World mm | Belt width % | Result | +|---|---|---|---|---| +| box 300 | 300 | 300 | 60% | PASS (unchanged) | +| oversized 401 | 401 | 401 | ~80% | PASS | +| bottle 91 | 91 | 91 | ~18% | PASS | +| plate 210 | 210 | 210 | 42% | PASS | +| pen | 9–13 | same | thin | PASS | +| cylinder 435 | 435 | 435 | ~87% | PASS | + +## 12. RUNTIME_PHYSICS + +- Timestep 1/60 shared (`physicsTimestep.ts`) +- Contact handoff via paddle; no teleport +- Runtime/headless parity hash: see `runtime-headless-config-hash.json` + +## 13. DROP_VALIDATION + +Copied from Stage 2B re-run: **7/7 PASS**, 10/10 each, pusher contact on C/D, 0 teleports. Visual WebM not recorded → overall Stage status remains PARTIAL for visual physics claim. + +## 14. VISUAL_QUALITY + +Improved: no measurement white panel, full-height canvas, no procedural motor, opaque CAD materials. Remaining: modular extension seams, SPEC_DERIVED paddle look vs CAD, camera empty sky framing. + +## 15. PERFORMANCE + +Formal profiles A–D marked NOT_REMEASURED / PARTIAL in JSON artifacts. Build + unit tests pass. + +## 16. MOBILE + +SVG fallback retained; e2e layout assertions added. Real phone NOT_TESTED. + +## 17. DEPLOYMENT + +`:3100` updated with Stage 2C dist. Public domain blocked. See `deployment-report.md`. + +## 18. TEST_RESULTS + +- `npx tsc -b` PASS +- `npm test` PASS (199+) +- `npm run build` PASS +- Drop validation 7/7 PASS +- e2e `stage2c-layout.spec.ts` added + +## 19. KNOWN_LIMITATIONS + +- Junction paddle SPEC_DERIVED +- Entry/exit SPEC_DERIVED modular +- No runtime WebM in artifacts +- GPU performance matrix not re-run +- Public DNS/TLS blocked externally + +## 20. DECISION + +| Question | Answer | +|---|---| +| White panel removed? | YES (default) | +| Empty area fixed? | YES (canvas fill; camera framing ≠ layout bug) | +| Single conveyor? | PARTIAL (CAD + modular extensions, no second full conveyor/motor) | +| All relevant CAD audited? | YES (one author FCStd + inventory) | +| Motor correct? | YES (procedural duplicate removed) | +| Volumetric / opaque? | PARTIAL / PASS (solids + opaque materials; no new bevel) | +| Real CAD sorting mechanism? | PARTIAL (CAD servos on module; junction paddle SPEC_DERIVED) | +| Falls realistic? | PARTIAL (headless+hash PASS; WebM missing) | +| Runtime/headless match? | YES | +| Performance? | PARTIAL (not remeasured) | + +## 21. FILES_CHANGED + +- `src/pages/MainPage.tsx` — measurement modes +- `src/styles.css` — full-height canvas-holder / 100dvh +- `src/components/ThreeD/SorterDigitalTwinContinuous.tsx` — SorterCadAssembly, remove StepperMotor, CAD pitch +- `src/components/ThreeD/ConveyorCadModel.tsx` — opaque materials +- `src/domain/cadAssemblyParams.ts`, `physicsConfigHash.ts`, `physicsTimestep.ts` +- `src/domain/physicsDropSim.ts`, `SorterPhysics.tsx` — shared timestep +- `e2e/stage2c-layout.spec.ts`, `src/domain/stage2cCadVisual.test.ts` +- `docs/stage2c_cad_visual/**` + +## 22. FINAL_GIT_CHECK + +(see working tree at end of session) diff --git a/docs/stage2c_cad_visual/bundle-impact.md b/docs/stage2c_cad_visual/bundle-impact.md new file mode 100644 index 0000000..d9c2084 --- /dev/null +++ b/docs/stage2c_cad_visual/bundle-impact.md @@ -0,0 +1,6 @@ +# Bundle impact — Stage 2C + +- Build: `vite build` OK (2026-07-30) +- Main twin chunk remains large (~4.7 MB) — Stage 2B baseline, not newly inflated by Stage 2C UI/layout fixes +- No new GLB rebuild in Stage 2C (reuse `conveyor-web.glb`) +- Added domain modules: `cadAssemblyParams.ts`, `physicsConfigHash.ts`, `physicsTimestep.ts` (tiny) diff --git a/docs/stage2c_cad_visual/cad-assembly-plan.md b/docs/stage2c_cad_visual/cad-assembly-plan.md new file mode 100644 index 0000000..9be94a0 --- /dev/null +++ b/docs/stage2c_cad_visual/cad-assembly-plan.md @@ -0,0 +1,20 @@ +# CAD Assembly Plan — Stage 2C + +## Single source + +Runtime group `SorterCadAssembly` (ConveyorBelt in SorterDigitalTwinContinuous): + +1. `ConveyorCadModel` — full REAL_CAD GLB (frame, belt, rollers, motor, gates, servos, camera mount) +2. Modular `BeltSection` entry/exit — SPEC_DERIVED at CAD pitch 500 mm / radius 25 mm / width 500 mm / height 700 mm +3. End drive/tension rollers on extensions only +4. **No** procedural `StepperMotor` + +## Parameters (`cadAssemblyParams.ts`) + +segmentLength=2.01 m, rollerPitch=0.5 m, rollerRadius=0.025 m, conveyorWidth=0.5 m, beltHeight=0.7 m, supportSpacing=2.0 m, totalLineLength=8.5 m + +## Forbidden + +- Non-uniform scale of CAD module +- Duplicate full procedural conveyor +- Procedural motor alongside CAD NEMA17 diff --git a/docs/stage2c_cad_visual/cad-node-hierarchy.json b/docs/stage2c_cad_visual/cad-node-hierarchy.json new file mode 100644 index 0000000..57bfdcd --- /dev/null +++ b/docs/stage2c_cad_visual/cad-node-hierarchy.json @@ -0,0 +1,977 @@ +{ + "source": "tools/stage2-cad/out/manifest.json + bake", + "bake": "(x,y,z)mm Z-up -> (-x,z,y+250)/1000", + "worldPlacement": [ + -2.02, + 0.594, + 0 + ], + "nodes": [ + { + "name": "Body", + "label": "NEMA17", + "groupGuess": "motor-and-drive", + "triangles": 5586, + "bboxMm": { + "min": [ + -1e-15, + -38, + -30 + ], + "max": [ + 42.3, + 34.00000000000001, + 12.300000000000008 + ] + }, + "sizeMm": [ + 42.3, + 72, + 42.30000000000001 + ] + }, + { + "name": "Body001", + "label": "Шкив GT2 16 зубов", + "groupGuess": "motor-and-drive", + "triangles": 2264, + "bboxMm": { + "min": [ + 14.756939000000003, + 16, + -15.70807668300531 + ], + "max": [ + 27.756939000000003, + 34.300000000000004, + -2.712117316994693 + ] + }, + "sizeMm": [ + 13, + 18.300000000000004, + 12.995959366010617 + ] + }, + { + "name": "Body002", + "label": "Ролик", + "groupGuess": "rollers", + "triangles": 10412, + "bboxMm": { + "min": [ + -3.954959426935666, + -250, + 55.00484333613933 + ], + "max": [ + 46, + 0, + 104.99515666386074 + ] + }, + "sizeMm": [ + 49.954959426935666, + 250, + 49.99031332772141 + ] + }, + { + "name": "Body003", + "label": "Ролик001", + "groupGuess": "rollers", + "triangles": 1008, + "bboxMm": { + "min": [ + -4, + -500, + 55.00777044997959 + ], + "max": [ + 46, + -250, + 104.99222955002045 + ] + }, + "sizeMm": [ + 50, + 250, + 49.984459100040866 + ] + }, + { + "name": "Body004", + "label": "Вал", + "groupGuess": "rollers", + "triangles": 500, + "bboxMm": { + "min": [ + 18, + -515, + 77.00093245399755 + ], + "max": [ + 24, + 35, + 82.99906754600256 + ] + }, + "sizeMm": [ + 6, + 550, + 5.998135092005015 + ] + }, + { + "name": "Body005", + "label": "Подшибник 606", + "groupGuess": "rollers", + "triangles": 1008, + "bboxMm": { + "min": [ + 12.5, + 2, + 71.50264195299306 + ], + "max": [ + 29.5, + 8, + 88.49735804700694 + ] + }, + "sizeMm": [ + 17, + 6, + 16.99471609401388 + ] + }, + { + "name": "Body006", + "label": "Подшибник 606-1", + "groupGuess": "rollers", + "triangles": 1008, + "bboxMm": { + "min": [ + 12.5, + -508, + 71.50264195299306 + ], + "max": [ + 29.5, + -502, + 88.49735804700694 + ] + }, + "sizeMm": [ + 17, + 6, + 16.99471609401388 + ] + }, + { + "name": "Body007", + "label": "Шкив GT2 40 зубов", + "groupGuess": "motor-and-drive", + "triangles": 2520, + "bboxMm": { + "min": [ + 7, + 13.2, + 66.00435145198857 + ], + "max": [ + 35, + 34.2, + 93.99564854801143 + ] + }, + "sizeMm": [ + 28, + 21.000000000000004, + 27.991297096022862 + ] + }, + { + "name": "Body008", + "label": "Зубчаты ремень", + "groupGuess": "motor-and-drive", + "triangles": 1048, + "bboxMm": { + "min": [ + 7.529429378041517, + 21.999999999999993, + -14.960073663449407 + ], + "max": [ + 34.47470281539362, + 32.000000000000014, + 93.47105570946769 + ] + }, + "sizeMm": [ + 26.945273437352103, + 10.000000000000021, + 108.4311293729171 + ] + }, + { + "name": "Body009", + "label": "Кронштейн", + "groupGuess": "static-frame", + "triangles": 14362, + "bboxMm": { + "min": [ + -19.000000000000004, + -10, + 33.449999999999996 + ], + "max": [ + 61.00000000528567, + 10.000000000000005, + 100 + ] + }, + "sizeMm": [ + 80.00000000528567, + 20.000000000000007, + 66.55000000000001 + ] + }, + { + "name": "Body010", + "label": "Конструкционный профиль 500х20х20мм", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -439, + -10, + 13.499999999999998 + ], + "max": [ + 61, + 10, + 33.5 + ] + }, + "sizeMm": [ + 500, + 20, + 20 + ] + }, + { + "name": "Body011", + "label": "Конструкционный профиль 480х20х20мм001", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + 46, + -490, + 13.399999999999999 + ], + "max": [ + 66, + -10, + 33.4 + ] + }, + "sizeMm": [ + 20, + 480, + 20 + ] + }, + { + "name": "Link", + "label": "Кронштейн002", + "groupGuess": "static-frame", + "triangles": 14362, + "bboxMm": { + "min": [ + -19.00000000528567, + -510, + 33.449999999999996 + ], + "max": [ + 61, + -490, + 100 + ] + }, + "sizeMm": [ + 80.00000000528567, + 20, + 66.55000000000001 + ] + }, + { + "name": "Body013", + "label": "3х угловой соединитель", + "groupGuess": "static-frame", + "triangles": 2716, + "bboxMm": { + "min": [ + 45.45761393940592, + -25.49734985341653, + -2.0195123917426727 + ], + "max": [ + 66.03239455479782, + 10.028193646006756, + 33.55177332439381 + ] + }, + "sizeMm": [ + 20.5747806153919, + 35.525543499423286, + 35.57128571613648 + ] + }, + { + "name": "Link001", + "label": "3х угловой соединитель001", + "groupGuess": "static-frame", + "triangles": 2716, + "bboxMm": { + "min": [ + 45.49999999999999, + -510, + -2.000000000000014 + ], + "max": [ + 66.00000000000003, + -474.5, + 33.50000000000001 + ] + }, + "sizeMm": [ + 20.500000000000036, + 35.5, + 35.50000000000002 + ] + }, + { + "name": "Body014", + "label": "Конструкционный профиль 100х20х20мм", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + 45.99999999999997, + -10.000000000000002, + -86.5 + ], + "max": [ + 65.99999999999997, + 9.999999999999998, + 13.500000000000002 + ] + }, + "sizeMm": [ + 20, + 20, + 100 + ] + }, + { + "name": "Body015", + "label": "Кронштейн крепления двигателя", + "groupGuess": "static-frame", + "triangles": 8400, + "bboxMm": { + "min": [ + -20.500000000000025, + 9.999999999999988, + -52.999999999999936 + ], + "max": [ + 64.50000000000003, + 18.00000000000001, + 31.999999999999996 + ] + }, + "sizeMm": [ + 85.00000000000006, + 8.000000000000023, + 84.99999999999993 + ] + }, + { + "name": "Link002", + "label": "Конструкционный профиль 100х20х20мм001", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + 46, + -510, + -86.49999999999991 + ], + "max": [ + 66, + -490, + 13.500000000000092 + ] + }, + "sizeMm": [ + 20, + 20, + 100 + ] + }, + { + "name": "Link003", + "label": "Конструкционный профиль 500х20х20мм002", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -439.0000000000001, + -510, + 13.499999999999998 + ], + "max": [ + 60.9999999999999, + -489.9999999999999, + 33.5 + ] + }, + "sizeMm": [ + 500, + 20.000000000000114, + 20 + ] + }, + { + "name": "Link004", + "label": "Конструкционный профиль 500х20х20мм003", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -939, + -9.999999999999913, + 13.499999999999998 + ], + "max": [ + -439, + 10.000000000000199, + 33.5 + ] + }, + "sizeMm": [ + 500, + 20.000000000000114, + 20 + ] + }, + { + "name": "Link005", + "label": "Роликовый узел в сборе", + "groupGuess": "rollers", + "triangles": 42660, + "bboxMm": { + "min": [ + -479.00000000528564, + -515, + 33.449999999999996 + ], + "max": [ + -398.99999999471436, + 35, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057128, + 550, + 71.54515666386075 + ] + }, + { + "name": "Link006", + "label": "Конструкционный профиль 500х20х20мм004", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -939.0000000000001, + -509.99999999999994, + 13.499999999999998 + ], + "max": [ + -439.0000000000001, + -489.99999999999983, + 33.5 + ] + }, + "sizeMm": [ + 500, + 20.000000000000114, + 20 + ] + }, + { + "name": "Link007", + "label": "Роликовый узел в сборе001", + "groupGuess": "rollers", + "triangles": 42660, + "bboxMm": { + "min": [ + -979.0000000052856, + -515, + 33.449999999999996 + ], + "max": [ + -898.9999999947144, + 35, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057128, + 550, + 71.54515666386075 + ] + }, + { + "name": "Link008", + "label": "Конструкционный профиль 500х20х20мм005", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -1439, + -9.999999999999812, + 13.499999999999998 + ], + "max": [ + -939, + 10.000000000000298, + 33.5 + ] + }, + "sizeMm": [ + 500, + 20.00000000000011, + 20 + ] + }, + { + "name": "Link009", + "label": "Конструкционный профиль 500х20х20мм006", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -1439, + -509.9999999999998, + 13.499999999999998 + ], + "max": [ + -939.0000000000001, + -489.99999999999966, + 33.5 + ] + }, + "sizeMm": [ + 499.9999999999999, + 20.000000000000114, + 20 + ] + }, + { + "name": "Link010", + "label": "Конструкционный профиль 480х20х20мм002", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -949, + -490, + 13.499999999999998 + ], + "max": [ + -929, + -10, + 33.5 + ] + }, + "sizeMm": [ + 20, + 480, + 20 + ] + }, + { + "name": "Link011", + "label": "Конструкционный профиль 500х20х20мм007", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -1939, + -9.999999999999712, + 13.499999999999998 + ], + "max": [ + -1439, + 10.000000000000398, + 33.5 + ] + }, + "sizeMm": [ + 500, + 20.00000000000011, + 20 + ] + }, + { + "name": "Link012", + "label": "Конструкционный профиль 500х20х20мм008", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -1939, + -509.9999999999997, + 13.499999999999998 + ], + "max": [ + -1439, + -489.9999999999996, + 33.5 + ] + }, + "sizeMm": [ + 500, + 20.000000000000114, + 20 + ] + }, + { + "name": "Link013", + "label": "Конструкционный профиль 480х20х20мм003", + "groupGuess": "static-frame", + "triangles": 1896, + "bboxMm": { + "min": [ + -1944, + -490, + 13.499999999999998 + ], + "max": [ + -1924, + -10, + 33.5 + ] + }, + "sizeMm": [ + 20, + 480, + 20 + ] + }, + { + "name": "Link014", + "label": "3х угловой соединитель002", + "groupGuess": "static-frame", + "triangles": 2716, + "bboxMm": { + "min": [ + -1944, + -25.500000000000018, + -2.000000000000014 + ], + "max": [ + -1923.5, + 9.999999999999996, + 33.5 + ] + }, + "sizeMm": [ + 20.5, + 35.500000000000014, + 35.500000000000014 + ] + }, + { + "name": "Link015", + "label": "3х угловой соединитель003", + "groupGuess": "static-frame", + "triangles": 2716, + "bboxMm": { + "min": [ + -1944, + -510, + -2.000000000000014 + ], + "max": [ + -1923.5, + -474.5, + 33.5 + ] + }, + "sizeMm": [ + 20.5, + 35.5, + 35.500000000000014 + ] + }, + { + "name": "Link016", + "label": "Роликовый узел в сборе002", + "groupGuess": "rollers", + "triangles": 42660, + "bboxMm": { + "min": [ + -1479.0000000052858, + -515, + 33.449999999999996 + ], + "max": [ + -1398.9999999947142, + 35, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057151, + 550, + 71.54515666386075 + ] + }, + { + "name": "Link017", + "label": "Роликовый узел в сборе003", + "groupGuess": "rollers", + "triangles": 42660, + "bboxMm": { + "min": [ + -1939.0000000052858, + -515, + 33.449999999999996 + ], + "max": [ + -1858.9999999947142, + 35, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057151, + 550, + 71.54515666386075 + ] + }, + { + "name": "Body018", + "label": "Лента", + "groupGuess": "conveyor-belt", + "triangles": 1024, + "bboxMm": { + "min": [ + -1924.9919187320213, + -500, + 53.999999999999986 + ], + "max": [ + 46.99191873202125, + 2.4e-14, + 106.00000000000011 + ] + }, + "sizeMm": [ + 1971.9838374640426, + 500, + 52.00000000000013 + ] + }, + { + "name": "Body019", + "label": "Серво привод", + "groupGuess": "pusher-servo", + "triangles": 2580, + "bboxMm": { + "min": [ + -867.5, + -547.9999999999999, + 110 + ], + "max": [ + -785.5, + -517.9999999999999, + 165.2 + ] + }, + "sizeMm": [ + 82, + 30, + 55.19999999999999 + ] + }, + { + "name": "Body020", + "label": "Держатель-сервопривода", + "groupGuess": "pusher-servo", + "triangles": 50440, + "bboxMm": { + "min": [ + -899.5000000000248, + -548, + 14.999999999999844 + ], + "max": [ + -753.4999999999839, + -510, + 141.00000000000037 + ] + }, + "sizeMm": [ + 146.00000000004093, + 38, + 126.00000000000053 + ] + }, + { + "name": "Body021", + "label": "Держатель серво", + "groupGuess": "pusher-servo", + "triangles": 9516, + "bboxMm": { + "min": [ + -851, + -510, + 33.497873015872926 + ], + "max": [ + -802, + -490, + 63.497873015872926 + ] + }, + "sizeMm": [ + 49, + 20, + 30 + ] + }, + { + "name": "Link025", + "label": "серво001", + "groupGuess": "pusher-servo", + "triangles": 59956, + "bboxMm": { + "min": [ + -1396.5000000000161, + -9.999999999999877, + 14.999999999999844 + ], + "max": [ + -1250.4999999999752, + 48.00000000000013, + 141.00000000000037 + ] + }, + "sizeMm": [ + 146.00000000004093, + 58.00000000000001, + 126.00000000000053 + ] + }, + { + "name": "Link026", + "label": "Серво привод001", + "groupGuess": "pusher-servo", + "triangles": 2580, + "bboxMm": { + "min": [ + -1364.5, + 17.99999999999995, + 110 + ], + "max": [ + -1282.5, + 47.99999999999996, + 165.2 + ] + }, + "sizeMm": [ + 82, + 30.000000000000007, + 55.19999999999999 + ] + }, + { + "name": "Link027", + "label": "Барьер001", + "groupGuess": "stop-gate", + "triangles": 20062, + "bboxMm": { + "min": [ + -1871.1197656323916, + -495.1197656323927, + 117.99999999999983 + ], + "max": [ + -1333.383347775863, + 42.75807358037436, + 218.0000000000001 + ] + }, + "sizeMm": [ + 537.7364178565285, + 537.8778392127671, + 100.00000000000028 + ] + }, + { + "name": "Link028", + "label": "Крепление камеры001", + "groupGuess": "inspection-frame", + "triangles": 84536, + "bboxMm": { + "min": [ + -550, + -538, + 13.499999999999993 + ], + "max": [ + -490, + 38.000000000000114, + 533.5 + ] + }, + "sizeMm": [ + 60, + 576.0000000000001, + 520 + ] + }, + { + "name": "Link029", + "label": "Барьер002", + "groupGuess": "stop-gate", + "triangles": 20062, + "bboxMm": { + "min": [ + -1374.119765632393, + -542.6166522241368, + 117.99999999999983 + ], + "max": [ + -836.2419264196258, + -4.880234367608182, + 218.0000000000001 + ] + }, + "sizeMm": [ + 537.8778392127672, + 537.7364178565286, + 100.00000000000028 + ] + } + ] +} \ No newline at end of file diff --git a/docs/stage2c_cad_visual/cad-runtime-audit.json b/docs/stage2c_cad_visual/cad-runtime-audit.json new file mode 100644 index 0000000..c3df581 --- /dev/null +++ b/docs/stage2c_cad_visual/cad-runtime-audit.json @@ -0,0 +1,47 @@ +{ + "glb": "public/models/sorter/conveyor-web.glb", + "sizeBytes": 555484, + "sha256": "4ccb74e5b92a542cacdb5a322f08c8e23e52c2d582bf8e378be39f40b4c073a8", + "nodeCount": 42, + "groups": [ + "motor-and-drive", + "rollers", + "static-frame", + "conveyor-belt", + "pusher-servo", + "stop-gate", + "inspection-frame" + ], + "motor": { + "node": "motor-and-drive/NEMA17", + "worldAabbApprox": { + "min": [ + -2.062, + 0.564, + 0.212 + ], + "max": [ + -2.02, + 0.606, + 0.284 + ] + }, + "causeOfDetachedMotor": "procedural StepperMotor at CONVEYOR_END_X — REMOVED in Stage 2C", + "cadMotorStatus": "on-frame near belt height Y≈0.585" + }, + "materialsPolicy": { + "transparent": false, + "opacity": 1, + "depthWrite": true, + "side": "FrontSide" + }, + "duplicatesRemoved": [ + "StepperMotor procedural" + ], + "remainingSpecDerived": [ + "BeltSection entry/exit extensions", + "PusherMechanism junction paddle", + "RealSense D435i", + "chutes/cages" + ] +} \ No newline at end of file diff --git a/docs/stage2c_cad_visual/cad-source-inventory.json b/docs/stage2c_cad_visual/cad-source-inventory.json new file mode 100644 index 0000000..97f2aed --- /dev/null +++ b/docs/stage2c_cad_visual/cad-source-inventory.json @@ -0,0 +1,1180 @@ +{ + "generatedAt": "2026-07-30T12:20:00Z", + "authorCadFiles": [ + { + "path": "3d_models/conveer.FCStd", + "format": "fcstd", + "sizeBytes": 5039805, + "sha256": "90c1844a4ca05e26def783d6130fc4b993430dde14307534ef8fbb21c9fac2e6", + "role": "author_cad" + } + ], + "runtimeAssets": [ + { + "path": "public/models/bottle.stl", + "format": "stl", + "sizeBytes": 326184, + "sha256": "9a8c64f1c26a2f2e2539b5e283b36ca78158cb8d2f0f66d3201dbcc784417375", + "role": "product_model" + }, + { + "path": "public/models/pen.stl", + "format": "stl", + "sizeBytes": 240084, + "sha256": "eb4baf50e4910c9be3eaa092da3720fe2d5dcfd0d9a056e4480af6b878561df5", + "role": "product_model" + }, + { + "path": "public/models/box-400.stl", + "format": "stl", + "sizeBytes": 26884, + "sha256": "05c4ec56883f085e1dafc9bd43fb87ce9993071122e19983192b72120daa9f68", + "role": "product_model" + }, + { + "path": "public/models/lunchbox.stl", + "format": "stl", + "sizeBytes": 578784, + "sha256": "3ad0f231777e7fe1ac55ac55e40074baffc3561c1fe082bb50dd47161f5c5c99", + "role": "product_model" + }, + { + "path": "public/models/pouf.stl", + "format": "stl", + "sizeBytes": 644084, + "sha256": "1761f3b2d9e5781a11f09e868d2c14e59d3c03bd5f0c19a006f28648de9f66d1", + "role": "product_model" + }, + { + "path": "public/models/box-300.stl", + "format": "stl", + "sizeBytes": 29684, + "sha256": "4ac9046bdef5bad2e7e50062fea30ead9132ebb6bdf23edeceb1f61c276f6e38", + "role": "product_model" + }, + { + "path": "public/models/detergent.stl", + "format": "stl", + "sizeBytes": 1472984, + "sha256": "1b93c69affe0a7b5fe67973d3c9ca2d7ca7f3daace9b26e386ce7cd89b78da92", + "role": "product_model" + }, + { + "path": "public/models/plate.stl", + "format": "stl", + "sizeBytes": 125284, + "sha256": "9bd0fece5fca87951d9c05576f054d0f61da76b56f0ca1387ddcfce816b23f47", + "role": "product_model" + }, + { + "path": "public/models/cylinder.stl", + "format": "stl", + "sizeBytes": 107684, + "sha256": "7aab451e1fd2154e4a2301e12642d17ccee62fc950dddf7de5e54c89453254e5", + "role": "product_model" + }, + { + "path": "public/models/sorter/conveyor-web.glb", + "format": "glb", + "sizeBytes": 555484, + "sha256": "4ccb74e5b92a542cacdb5a322f08c8e23e52c2d582bf8e378be39f40b4c073a8", + "role": "runtime_glb" + }, + { + "path": "tools/stage2-cad/out/conveyor-source.glb", + "format": "glb", + "sizeBytes": 22041844, + "sha256": "8e3018577cb39c0040a2c90ffacbf5dc5adfb9a4aa023610bf635055ace3b7eb", + "role": "cad_pipeline" + }, + { + "path": "tools/stage2-cad/out/conveyor-web-uncompressed.glb", + "format": "glb", + "sizeBytes": 6431976, + "sha256": "3b223a63dac0649c87b66edb3e03eb6193ab5f9b5df00b6df74a9067bfad453d", + "role": "runtime_glb" + }, + { + "path": "tools/stage2-cad/out/manifest.json", + "format": "json", + "sizeBytes": 12927, + "sha256": "0a283d758e379d646c61a715719b577d5526de731b93afa435ad79ea3816dc70", + "role": "cad_pipeline" + } + ], + "allInventoried": [ + { + "path": "3d_models/conveer.FCStd", + "format": "fcstd", + "sizeBytes": 5039805, + "sha256": "90c1844a4ca05e26def783d6130fc4b993430dde14307534ef8fbb21c9fac2e6", + "role": "author_cad" + }, + { + "path": "public/models/bottle.stl", + "format": "stl", + "sizeBytes": 326184, + "sha256": "9a8c64f1c26a2f2e2539b5e283b36ca78158cb8d2f0f66d3201dbcc784417375", + "role": "product_model" + }, + { + "path": "public/models/pen.stl", + "format": "stl", + "sizeBytes": 240084, + "sha256": "eb4baf50e4910c9be3eaa092da3720fe2d5dcfd0d9a056e4480af6b878561df5", + "role": "product_model" + }, + { + "path": "public/models/box-400.stl", + "format": "stl", + "sizeBytes": 26884, + "sha256": "05c4ec56883f085e1dafc9bd43fb87ce9993071122e19983192b72120daa9f68", + "role": "product_model" + }, + { + "path": "public/models/lunchbox.stl", + "format": "stl", + "sizeBytes": 578784, + "sha256": "3ad0f231777e7fe1ac55ac55e40074baffc3561c1fe082bb50dd47161f5c5c99", + "role": "product_model" + }, + { + "path": "public/models/pouf.stl", + "format": "stl", + "sizeBytes": 644084, + "sha256": "1761f3b2d9e5781a11f09e868d2c14e59d3c03bd5f0c19a006f28648de9f66d1", + "role": "product_model" + }, + { + "path": "public/models/box-300.stl", + "format": "stl", + "sizeBytes": 29684, + "sha256": "4ac9046bdef5bad2e7e50062fea30ead9132ebb6bdf23edeceb1f61c276f6e38", + "role": "product_model" + }, + { + "path": "public/models/detergent.stl", + "format": "stl", + "sizeBytes": 1472984, + "sha256": "1b93c69affe0a7b5fe67973d3c9ca2d7ca7f3daace9b26e386ce7cd89b78da92", + "role": "product_model" + }, + { + "path": "public/models/plate.stl", + "format": "stl", + "sizeBytes": 125284, + "sha256": "9bd0fece5fca87951d9c05576f054d0f61da76b56f0ca1387ddcfce816b23f47", + "role": "product_model" + }, + { + "path": "public/models/cylinder.stl", + "format": "stl", + "sizeBytes": 107684, + "sha256": "7aab451e1fd2154e4a2301e12642d17ccee62fc950dddf7de5e54c89453254e5", + "role": "product_model" + }, + { + "path": "public/models/sorter/conveyor-web.glb", + "format": "glb", + "sizeBytes": 555484, + "sha256": "4ccb74e5b92a542cacdb5a322f08c8e23e52c2d582bf8e378be39f40b4c073a8", + "role": "runtime_glb" + }, + { + "path": "tools/stage2-cad/out/conveyor-source.glb", + "format": "glb", + "sizeBytes": 22041844, + "sha256": "8e3018577cb39c0040a2c90ffacbf5dc5adfb9a4aa023610bf635055ace3b7eb", + "role": "cad_pipeline" + }, + { + "path": "tools/stage2-cad/out/conveyor-web-uncompressed.glb", + "format": "glb", + "sizeBytes": 6431976, + "sha256": "3b223a63dac0649c87b66edb3e03eb6193ab5f9b5df00b6df74a9067bfad453d", + "role": "runtime_glb" + }, + { + "path": "tools/stage2-cad/out/manifest.json", + "format": "json", + "sizeBytes": 12927, + "sha256": "0a283d758e379d646c61a715719b577d5526de731b93afa435ad79ea3816dc70", + "role": "cad_pipeline" + } + ], + "fcstdBodies": [ + { + "name": "Body", + "label": "NEMA17", + "typeId": "PartDesign::Body", + "triangles": 5586, + "bboxMm": { + "min": [ + -1e-15, + -38.0, + -30.0 + ], + "max": [ + 42.3, + 34.00000000000001, + 12.300000000000008 + ] + }, + "sizeMm": [ + 42.3, + 72.0, + 42.30000000000001 + ] + }, + { + "name": "Body001", + "label": "Шкив GT2 16 зубов", + "typeId": "PartDesign::Body", + "triangles": 2264, + "bboxMm": { + "min": [ + 14.756939000000003, + 16.0, + -15.70807668300531 + ], + "max": [ + 27.756939000000003, + 34.300000000000004, + -2.712117316994693 + ] + }, + "sizeMm": [ + 13.0, + 18.300000000000004, + 12.995959366010617 + ] + }, + { + "name": "Body002", + "label": "Ролик", + "typeId": "PartDesign::Body", + "triangles": 10412, + "bboxMm": { + "min": [ + -3.954959426935666, + -250.0, + 55.00484333613933 + ], + "max": [ + 46.0, + 0.0, + 104.99515666386074 + ] + }, + "sizeMm": [ + 49.954959426935666, + 250.0, + 49.99031332772141 + ] + }, + { + "name": "Body003", + "label": "Ролик001", + "typeId": "PartDesign::Body", + "triangles": 1008, + "bboxMm": { + "min": [ + -4.0, + -500.0, + 55.00777044997959 + ], + "max": [ + 46.0, + -250.0, + 104.99222955002045 + ] + }, + "sizeMm": [ + 50.0, + 250.0, + 49.984459100040866 + ] + }, + { + "name": "Body004", + "label": "Вал", + "typeId": "PartDesign::Body", + "triangles": 500, + "bboxMm": { + "min": [ + 18.0, + -515.0, + 77.00093245399755 + ], + "max": [ + 24.0, + 35.0, + 82.99906754600256 + ] + }, + "sizeMm": [ + 6.0, + 550.0, + 5.998135092005015 + ] + }, + { + "name": "Body005", + "label": "Подшибник 606", + "typeId": "PartDesign::Body", + "triangles": 1008, + "bboxMm": { + "min": [ + 12.5, + 2.0, + 71.50264195299306 + ], + "max": [ + 29.5, + 8.0, + 88.49735804700694 + ] + }, + "sizeMm": [ + 17.0, + 6.0, + 16.99471609401388 + ] + }, + { + "name": "Body006", + "label": "Подшибник 606-1", + "typeId": "PartDesign::Body", + "triangles": 1008, + "bboxMm": { + "min": [ + 12.5, + -508.0, + 71.50264195299306 + ], + "max": [ + 29.5, + -502.0, + 88.49735804700694 + ] + }, + "sizeMm": [ + 17.0, + 6.0, + 16.99471609401388 + ] + }, + { + "name": "Body007", + "label": "Шкив GT2 40 зубов", + "typeId": "PartDesign::Body", + "triangles": 2520, + "bboxMm": { + "min": [ + 7.0, + 13.2, + 66.00435145198857 + ], + "max": [ + 35.0, + 34.2, + 93.99564854801143 + ] + }, + "sizeMm": [ + 28.0, + 21.000000000000004, + 27.991297096022862 + ] + }, + { + "name": "Body008", + "label": "Зубчаты ремень", + "typeId": "PartDesign::Body", + "triangles": 1048, + "bboxMm": { + "min": [ + 7.529429378041517, + 21.999999999999993, + -14.960073663449407 + ], + "max": [ + 34.47470281539362, + 32.000000000000014, + 93.47105570946769 + ] + }, + "sizeMm": [ + 26.945273437352103, + 10.000000000000021, + 108.4311293729171 + ] + }, + { + "name": "Body009", + "label": "Кронштейн", + "typeId": "PartDesign::Body", + "triangles": 14362, + "bboxMm": { + "min": [ + -19.000000000000004, + -10.0, + 33.449999999999996 + ], + "max": [ + 61.00000000528567, + 10.000000000000005, + 100.0 + ] + }, + "sizeMm": [ + 80.00000000528567, + 20.000000000000007, + 66.55000000000001 + ] + }, + { + "name": "Body010", + "label": "Конструкционный профиль 500х20х20мм", + "typeId": "PartDesign::Body", + "triangles": 1896, + "bboxMm": { + "min": [ + -439.0, + -10.0, + 13.499999999999998 + ], + "max": [ + 61.0, + 10.0, + 33.5 + ] + }, + "sizeMm": [ + 500.0, + 20.0, + 20.0 + ] + }, + { + "name": "Body011", + "label": "Конструкционный профиль 480х20х20мм001", + "typeId": "PartDesign::Body", + "triangles": 1896, + "bboxMm": { + "min": [ + 46.0, + -490.0, + 13.399999999999999 + ], + "max": [ + 66.0, + -10.0, + 33.4 + ] + }, + "sizeMm": [ + 20.0, + 480.0, + 20.0 + ] + }, + { + "name": "Link", + "label": "Кронштейн002", + "typeId": "App::Link", + "triangles": 14362, + "bboxMm": { + "min": [ + -19.00000000528567, + -510.0, + 33.449999999999996 + ], + "max": [ + 61.0, + -490.0, + 100.0 + ] + }, + "sizeMm": [ + 80.00000000528567, + 20.0, + 66.55000000000001 + ] + }, + { + "name": "Body013", + "label": "3х угловой соединитель", + "typeId": "PartDesign::Body", + "triangles": 2716, + "bboxMm": { + "min": [ + 45.45761393940592, + -25.49734985341653, + -2.0195123917426727 + ], + "max": [ + 66.03239455479782, + 10.028193646006756, + 33.55177332439381 + ] + }, + "sizeMm": [ + 20.5747806153919, + 35.525543499423286, + 35.57128571613648 + ] + }, + { + "name": "Link001", + "label": "3х угловой соединитель001", + "typeId": "App::Link", + "triangles": 2716, + "bboxMm": { + "min": [ + 45.49999999999999, + -510.0, + -2.000000000000014 + ], + "max": [ + 66.00000000000003, + -474.5, + 33.50000000000001 + ] + }, + "sizeMm": [ + 20.500000000000036, + 35.5, + 35.50000000000002 + ] + }, + { + "name": "Body014", + "label": "Конструкционный профиль 100х20х20мм", + "typeId": "PartDesign::Body", + "triangles": 1896, + "bboxMm": { + "min": [ + 45.99999999999997, + -10.000000000000002, + -86.5 + ], + "max": [ + 65.99999999999997, + 9.999999999999998, + 13.500000000000002 + ] + }, + "sizeMm": [ + 20.0, + 20.0, + 100.0 + ] + }, + { + "name": "Body015", + "label": "Кронштейн крепления двигателя", + "typeId": "PartDesign::Body", + "triangles": 8400, + "bboxMm": { + "min": [ + -20.500000000000025, + 9.999999999999988, + -52.999999999999936 + ], + "max": [ + 64.50000000000003, + 18.00000000000001, + 31.999999999999996 + ] + }, + "sizeMm": [ + 85.00000000000006, + 8.000000000000023, + 84.99999999999993 + ] + }, + { + "name": "Link002", + "label": "Конструкционный профиль 100х20х20мм001", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + 46.0, + -510.0, + -86.49999999999991 + ], + "max": [ + 66.0, + -490.0, + 13.500000000000092 + ] + }, + "sizeMm": [ + 20.0, + 20.0, + 100.0 + ] + }, + { + "name": "Link003", + "label": "Конструкционный профиль 500х20х20мм002", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -439.0000000000001, + -510.0, + 13.499999999999998 + ], + "max": [ + 60.9999999999999, + -489.9999999999999, + 33.5 + ] + }, + "sizeMm": [ + 500.0, + 20.000000000000114, + 20.0 + ] + }, + { + "name": "Link004", + "label": "Конструкционный профиль 500х20х20мм003", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -939.0, + -9.999999999999913, + 13.499999999999998 + ], + "max": [ + -439.0, + 10.000000000000199, + 33.5 + ] + }, + "sizeMm": [ + 500.0, + 20.000000000000114, + 20.0 + ] + }, + { + "name": "Link005", + "label": "Роликовый узел в сборе", + "typeId": "App::Link", + "triangles": 42660, + "bboxMm": { + "min": [ + -479.00000000528564, + -515.0, + 33.449999999999996 + ], + "max": [ + -398.99999999471436, + 35.0, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057128, + 550.0, + 71.54515666386075 + ] + }, + { + "name": "Link006", + "label": "Конструкционный профиль 500х20х20мм004", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -939.0000000000001, + -509.99999999999994, + 13.499999999999998 + ], + "max": [ + -439.0000000000001, + -489.99999999999983, + 33.5 + ] + }, + "sizeMm": [ + 500.0, + 20.000000000000114, + 20.0 + ] + }, + { + "name": "Link007", + "label": "Роликовый узел в сборе001", + "typeId": "App::Link", + "triangles": 42660, + "bboxMm": { + "min": [ + -979.0000000052856, + -515.0, + 33.449999999999996 + ], + "max": [ + -898.9999999947144, + 35.0, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057128, + 550.0, + 71.54515666386075 + ] + }, + { + "name": "Link008", + "label": "Конструкционный профиль 500х20х20мм005", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -1439.0, + -9.999999999999812, + 13.499999999999998 + ], + "max": [ + -939.0, + 10.000000000000298, + 33.5 + ] + }, + "sizeMm": [ + 500.0, + 20.00000000000011, + 20.0 + ] + }, + { + "name": "Link009", + "label": "Конструкционный профиль 500х20х20мм006", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -1439.0, + -509.9999999999998, + 13.499999999999998 + ], + "max": [ + -939.0000000000001, + -489.99999999999966, + 33.5 + ] + }, + "sizeMm": [ + 499.9999999999999, + 20.000000000000114, + 20.0 + ] + }, + { + "name": "Link010", + "label": "Конструкционный профиль 480х20х20мм002", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -949.0, + -490.0, + 13.499999999999998 + ], + "max": [ + -929.0, + -10.0, + 33.5 + ] + }, + "sizeMm": [ + 20.0, + 480.0, + 20.0 + ] + }, + { + "name": "Link011", + "label": "Конструкционный профиль 500х20х20мм007", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -1939.0, + -9.999999999999712, + 13.499999999999998 + ], + "max": [ + -1439.0, + 10.000000000000398, + 33.5 + ] + }, + "sizeMm": [ + 500.0, + 20.00000000000011, + 20.0 + ] + }, + { + "name": "Link012", + "label": "Конструкционный профиль 500х20х20мм008", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -1939.0, + -509.9999999999997, + 13.499999999999998 + ], + "max": [ + -1439.0, + -489.9999999999996, + 33.5 + ] + }, + "sizeMm": [ + 500.0, + 20.000000000000114, + 20.0 + ] + }, + { + "name": "Link013", + "label": "Конструкционный профиль 480х20х20мм003", + "typeId": "App::Link", + "triangles": 1896, + "bboxMm": { + "min": [ + -1944.0, + -490.0, + 13.499999999999998 + ], + "max": [ + -1924.0, + -10.0, + 33.5 + ] + }, + "sizeMm": [ + 20.0, + 480.0, + 20.0 + ] + }, + { + "name": "Link014", + "label": "3х угловой соединитель002", + "typeId": "App::Link", + "triangles": 2716, + "bboxMm": { + "min": [ + -1944.0, + -25.500000000000018, + -2.000000000000014 + ], + "max": [ + -1923.5, + 9.999999999999996, + 33.5 + ] + }, + "sizeMm": [ + 20.5, + 35.500000000000014, + 35.500000000000014 + ] + }, + { + "name": "Link015", + "label": "3х угловой соединитель003", + "typeId": "App::Link", + "triangles": 2716, + "bboxMm": { + "min": [ + -1944.0, + -510.0, + -2.000000000000014 + ], + "max": [ + -1923.5, + -474.5, + 33.5 + ] + }, + "sizeMm": [ + 20.5, + 35.5, + 35.500000000000014 + ] + }, + { + "name": "Link016", + "label": "Роликовый узел в сборе002", + "typeId": "App::Link", + "triangles": 42660, + "bboxMm": { + "min": [ + -1479.0000000052858, + -515.0, + 33.449999999999996 + ], + "max": [ + -1398.9999999947142, + 35.0, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057151, + 550.0, + 71.54515666386075 + ] + }, + { + "name": "Link017", + "label": "Роликовый узел в сборе003", + "typeId": "App::Link", + "triangles": 42660, + "bboxMm": { + "min": [ + -1939.0000000052858, + -515.0, + 33.449999999999996 + ], + "max": [ + -1858.9999999947142, + 35.0, + 104.99515666386074 + ] + }, + "sizeMm": [ + 80.00000001057151, + 550.0, + 71.54515666386075 + ] + }, + { + "name": "Body018", + "label": "Лента", + "typeId": "PartDesign::Body", + "triangles": 1024, + "bboxMm": { + "min": [ + -1924.9919187320213, + -500.0, + 53.999999999999986 + ], + "max": [ + 46.99191873202125, + 2.4e-14, + 106.00000000000011 + ] + }, + "sizeMm": [ + 1971.9838374640426, + 500.0, + 52.00000000000013 + ] + }, + { + "name": "Body019", + "label": "Серво привод", + "typeId": "PartDesign::Body", + "triangles": 2580, + "bboxMm": { + "min": [ + -867.5, + -547.9999999999999, + 110.0 + ], + "max": [ + -785.5, + -517.9999999999999, + 165.2 + ] + }, + "sizeMm": [ + 82.0, + 30.0, + 55.19999999999999 + ] + }, + { + "name": "Body020", + "label": "Держатель-сервопривода", + "typeId": "PartDesign::Body", + "triangles": 50440, + "bboxMm": { + "min": [ + -899.5000000000248, + -548.0, + 14.999999999999844 + ], + "max": [ + -753.4999999999839, + -510.0, + 141.00000000000037 + ] + }, + "sizeMm": [ + 146.00000000004093, + 38.0, + 126.00000000000053 + ] + }, + { + "name": "Body021", + "label": "Держатель серво", + "typeId": "PartDesign::Body", + "triangles": 9516, + "bboxMm": { + "min": [ + -851.0, + -510.0, + 33.497873015872926 + ], + "max": [ + -802.0, + -490.0, + 63.497873015872926 + ] + }, + "sizeMm": [ + 49.0, + 20.0, + 30.0 + ] + }, + { + "name": "Link025", + "label": "серво001", + "typeId": "App::Link", + "triangles": 59956, + "bboxMm": { + "min": [ + -1396.5000000000161, + -9.999999999999877, + 14.999999999999844 + ], + "max": [ + -1250.4999999999752, + 48.00000000000013, + 141.00000000000037 + ] + }, + "sizeMm": [ + 146.00000000004093, + 58.00000000000001, + 126.00000000000053 + ] + }, + { + "name": "Link026", + "label": "Серво привод001", + "typeId": "App::Link", + "triangles": 2580, + "bboxMm": { + "min": [ + -1364.5, + 17.99999999999995, + 110.0 + ], + "max": [ + -1282.5, + 47.99999999999996, + 165.2 + ] + }, + "sizeMm": [ + 82.0, + 30.000000000000007, + 55.19999999999999 + ] + }, + { + "name": "Link027", + "label": "Барьер001", + "typeId": "App::Link", + "triangles": 20062, + "bboxMm": { + "min": [ + -1871.1197656323916, + -495.1197656323927, + 117.99999999999983 + ], + "max": [ + -1333.383347775863, + 42.75807358037436, + 218.0000000000001 + ] + }, + "sizeMm": [ + 537.7364178565285, + 537.8778392127671, + 100.00000000000028 + ] + }, + { + "name": "Link028", + "label": "Крепление камеры001", + "typeId": "App::Link", + "triangles": 84536, + "bboxMm": { + "min": [ + -550.0, + -538.0, + 13.499999999999993 + ], + "max": [ + -490.0, + 38.000000000000114, + 533.5 + ] + }, + "sizeMm": [ + 60.0, + 576.0000000000001, + 520.0 + ] + }, + { + "name": "Link029", + "label": "Барьер002", + "typeId": "App::Link", + "triangles": 20062, + "bboxMm": { + "min": [ + -1374.119765632393, + -542.6166522241368, + 117.99999999999983 + ], + "max": [ + -836.2419264196258, + -4.880234367608182, + 218.0000000000001 + ] + }, + "sizeMm": [ + 537.8778392127672, + 537.7364178565286, + 100.00000000000028 + ] + } + ], + "notes": [ + "Only author machine CAD found: 3d_models/conveer.FCStd (42 PartDesign bodies).", + "Product SKUs are separate STL assets under public/models/.", + "input_info/ contains PDF/ZIP docs, not CAD solids.", + "CAD module world span after placement ≈ 2.01 m — shorter than full A→B line; entry/exit BeltSection are SPEC_DERIVED extensions.", + "Procedural StepperMotor is DUPLICATE of CAD motor-and-drive/NEMA17 and is positioned at CONVEYOR_END_X (under/beside extension), causing detached motor look." + ] +} \ No newline at end of file diff --git a/docs/stage2c_cad_visual/console-errors.json b/docs/stage2c_cad_visual/console-errors.json new file mode 100644 index 0000000..5abfb70 --- /dev/null +++ b/docs/stage2c_cad_visual/console-errors.json @@ -0,0 +1 @@ +{"defaultRoute":"/?playback=paused","errors":[],"note":"Spot-check via Playwright load; not a full console scrape matrix"} diff --git a/docs/stage2c_cad_visual/deployment-report.md b/docs/stage2c_cad_visual/deployment-report.md new file mode 100644 index 0000000..6c0e3a9 --- /dev/null +++ b/docs/stage2c_cad_visual/deployment-report.md @@ -0,0 +1,17 @@ +# Deployment — Stage 2C + +## Local production + +- Container `owl-web-1` on `127.0.0.1:3100` hot-updated with Stage 2C `dist/` (asset `index-CA2EvFI-.js`) +- Preview control: `127.0.0.1:3101` (`vite preview`) +- Previous container/image retained; no rollback deleted + +## Public domain + +- `sorter.arhipovdan.ru` remains **BLOCKED_BY_DEPLOYMENT_ACCESS** (external A-record / openresty without this vhost) +- Required DNS (owner action): A/AAAA or CNAME pointing to the host that terminates TLS for this app and proxies to `127.0.0.1:3100` (or LAN equivalent) +- Cloudflare tunnel tokens are not published here + +## Phone + +- NOT_TESTED_ON_REAL_PHONE diff --git a/docs/stage2c_cad_visual/drop-validation.json b/docs/stage2c_cad_visual/drop-validation.json new file mode 100644 index 0000000..dac037f --- /dev/null +++ b/docs/stage2c_cad_visual/drop-validation.json @@ -0,0 +1,138 @@ +{ + "generatedAt": "2026-07-30T12:27:01.613Z", + "sim": "headless Rapier via simulateDrop (deterministic, dt=1/60, settle 6s)", + "runs": 10, + "routes": [ + { + "sku": "SKU-001", + "route": "box → B", + "expectedZone": "B", + "runs": 10, + "correctLandings": 10, + "penetrations": 0, + "teleports": 0, + "outOfBounds": 0, + "frozenInAir": 0, + "pusherContacts": "n/a", + "sampleFinalPositions": [ + "(3.00,0.23,-0.00)", + "(3.00,0.23,-0.00)", + "(3.00,0.23,-0.00)" + ], + "result": "PASS" + }, + { + "sku": "SKU-009", + "route": "pen → C", + "expectedZone": "C", + "runs": 10, + "correctLandings": 10, + "penetrations": 0, + "teleports": 0, + "outOfBounds": 0, + "frozenInAir": 0, + "pusherContacts": 10, + "sampleFinalPositions": [ + "(1.66,0.08,2.32)", + "(1.66,0.08,2.32)", + "(1.66,0.08,2.32)" + ], + "result": "PASS" + }, + { + "sku": "SKU-005", + "route": "pouf → C", + "expectedZone": "C", + "runs": 10, + "correctLandings": 10, + "penetrations": 0, + "teleports": 0, + "outOfBounds": 0, + "frozenInAir": 0, + "pusherContacts": 10, + "sampleFinalPositions": [ + "(1.96,0.27,1.68)", + "(1.96,0.27,1.68)", + "(1.96,0.27,1.68)" + ], + "result": "PASS" + }, + { + "sku": "SKU-004", + "route": "oversized → C", + "expectedZone": "C", + "runs": 10, + "correctLandings": 10, + "penetrations": 0, + "teleports": 0, + "outOfBounds": 0, + "frozenInAir": 0, + "pusherContacts": 10, + "sampleFinalPositions": [ + "(1.95,0.30,1.58)", + "(1.95,0.30,1.58)", + "(1.95,0.30,1.58)" + ], + "result": "PASS" + }, + { + "sku": "SKU-007", + "route": "bottle → D", + "expectedZone": "D", + "runs": 10, + "correctLandings": 10, + "penetrations": 0, + "teleports": 0, + "outOfBounds": 0, + "frozenInAir": 0, + "pusherContacts": 10, + "sampleFinalPositions": [ + "(2.34,0.12,-1.95)", + "(2.34,0.12,-1.95)", + "(2.34,0.12,-1.95)" + ], + "result": "PASS" + }, + { + "sku": "SKU-006", + "route": "plate → D", + "expectedZone": "D", + "runs": 10, + "correctLandings": 10, + "penetrations": 0, + "teleports": 0, + "outOfBounds": 0, + "frozenInAir": 0, + "pusherContacts": 10, + "sampleFinalPositions": [ + "(1.89,0.09,-1.80)", + "(1.89,0.09,-1.80)", + "(1.89,0.09,-1.80)" + ], + "result": "PASS" + }, + { + "sku": "SKU-008", + "route": "cylinder → D", + "expectedZone": "D", + "runs": 10, + "correctLandings": 10, + "penetrations": 0, + "teleports": 0, + "outOfBounds": 0, + "frozenInAir": 0, + "pusherContacts": 10, + "sampleFinalPositions": [ + "(2.22,0.10,-2.28)", + "(2.22,0.10,-2.28)", + "(2.22,0.10,-2.28)" + ], + "result": "PASS" + } + ], + "summary": { + "totalRoutes": 7, + "passedRoutes": 7, + "result": "PASS" + } +} \ No newline at end of file diff --git a/docs/stage2c_cad_visual/materials-audit.json b/docs/stage2c_cad_visual/materials-audit.json new file mode 100644 index 0000000..cc3a64b --- /dev/null +++ b/docs/stage2c_cad_visual/materials-audit.json @@ -0,0 +1,16 @@ +{ + "slots": [ + "painted-metal", + "brushed-metal", + "rubber-belt", + "dark-mechanical", + "safety-yellow" + ], + "opaqueForced": true, + "transparencyAllowed": [ + "RealSense glass only (SPEC_DERIVED)", + "laser beam debug" + ], + "surfaceOnlyNodes": [], + "note": "FCStd bodies are PartDesign solids; no CAD_SURFACE_ONLY solidify applied" +} \ No newline at end of file diff --git a/docs/stage2c_cad_visual/mechanism-cad-map.md b/docs/stage2c_cad_visual/mechanism-cad-map.md new file mode 100644 index 0000000..3d9c276 --- /dev/null +++ b/docs/stage2c_cad_visual/mechanism-cad-map.md @@ -0,0 +1,23 @@ +# Mechanism CAD Map — Stage 2C + +## Author CAD nodes (conveer.FCStd → conveyor-web.glb) + +| Group | Nodes (labels) | Role | Usable as junction paddle? | +|---|---|---|---| +| stop-gate | Барьер001, Барьер002 | Metering barriers on CAD module (~x CAD −0.8…−1.8 m) | No — wrong location/stroke for B/C/D junction | +| pusher-servo | Серво привод, Держатель-сервопривода, серво001, … | Side-mounted servo + holder on CAD module | Partial — REAL_CAD visual; no proven junction kinematics | +| motor-and-drive | NEMA17, шкивы, ремень | Drive at CAD module | Yes (drive only) | + +## Junction sorting (x≈1.5 m, outside CAD module span) + +CAD module world span X ≈ [−2.086, −0.076]. Domain junction / paddles live at x≈1.5 → **outside** author CAD solids. + +**Decision:** keep `PusherMechanism` (SPEC_DERIVED angled paddle) as default kinematic contact body. +**CAD_MECHANISM_UNUSABLE** for junction paddle: no author solid at junction with matching stroke/face. + +CAD `pusher-servo/*` remains visible as REAL_CAD static geometry on the metering module (not hidden). + +## Contact events (runtime) + +mechanismArmed → contactStarted → peakContactForce → contactEnded → physicsReleased → receiverEntered → itemSettled +(asserted in headless via pusher contact + receiver volumes; see drop-validation.json) diff --git a/docs/stage2c_cad_visual/performance-cad-corrected.json b/docs/stage2c_cad_visual/performance-cad-corrected.json new file mode 100644 index 0000000..2df1c2d --- /dev/null +++ b/docs/stage2c_cad_visual/performance-cad-corrected.json @@ -0,0 +1 @@ +{"profile":"B-cad-assembly","note":"No full 30s×3 GPU profile in this session; SwiftShader excluded","status":"NOT_REMEASURED","layoutCanvasHeightRatio":1.0} diff --git a/docs/stage2c_cad_visual/performance-cad-physics.json b/docs/stage2c_cad_visual/performance-cad-physics.json new file mode 100644 index 0000000..13725be --- /dev/null +++ b/docs/stage2c_cad_visual/performance-cad-physics.json @@ -0,0 +1 @@ +{"profile":"C-cad-physics","status":"NOT_REMEASURED","physicsTimestep":0.0166667,"runtimeHeadlessHashEqual":true} diff --git a/docs/stage2c_cad_visual/performance-control.json b/docs/stage2c_cad_visual/performance-control.json new file mode 100644 index 0000000..34f01e4 --- /dev/null +++ b/docs/stage2c_cad_visual/performance-control.json @@ -0,0 +1 @@ +{"profile":"A-stage2-control","note":"Baseline Stage 2B not re-profiled in Stage 2C session","status":"NOT_REMEASURED"} diff --git a/docs/stage2c_cad_visual/performance-final.json b/docs/stage2c_cad_visual/performance-final.json new file mode 100644 index 0000000..eb3226f --- /dev/null +++ b/docs/stage2c_cad_visual/performance-final.json @@ -0,0 +1 @@ +{"profile":"D-final","status":"PARTIAL","budget":{"avgFpsMin":45,"p95MsMax":28,"drawCallsMax":250,"trisMax":500000},"measured":null,"note":"Formal multi-run GPU profile deferred; unit/e2e/build green"} diff --git a/docs/stage2c_cad_visual/runtime-headless-config-hash.json b/docs/stage2c_cad_visual/runtime-headless-config-hash.json new file mode 100644 index 0000000..f07e7e1 --- /dev/null +++ b/docs/stage2c_cad_visual/runtime-headless-config-hash.json @@ -0,0 +1,55 @@ +{ + "generatedAt": "2026-07-30T12:27:15.737Z", + "runtimeHash": "7ee15ad3e879a44a", + "headlessHash": "7ee15ad3e879a44a", + "equal": true, + "timestepEqual": true, + "timestepSec": 0.016666666666666666, + "snapshot": { + "timestep": 0.016666666666666666, + "gravity": [ + 0, + -9.81, + 0 + ], + "pusherHalfExtents": [ + 0.5, + 0.14, + 0.025 + ], + "pusherCenterY": 0.842, + "pusherEngage": { + "x": 1.5, + "z": 0 + }, + "staticColliderCount": 25, + "staticColliderIds": [ + "world-floor", + "belt-slab", + "b-spur", + "chute-C-floor", + "chute-C-rail-left", + "chute-C-rail-right", + "chute-D-floor", + "chute-D-rail-left", + "chute-D-rail-right", + "b-bin-floor", + "b-bin-wall-z+", + "b-bin-wall-z-", + "b-bin-wall-x+", + "b-bin-entry-lip", + "b-drop-chute", + "cage-C-floor", + "cage-C-wall-x-", + "cage-C-wall-x+", + "cage-C-wall-back", + "cage-C-entry-sill", + "cage-D-floor", + "cage-D-wall-x-", + "cage-D-wall-x+", + "cage-D-wall-back", + "cage-D-entry-sill" + ], + "staticColliderFingerprint": "world-floor|8.00000,0.05000,6.00000|0.00000,-0.05000,0.00000|0.00000,0.00000,0.00000|0.8000;belt-slab|3.17500,0.01200,0.25000|-1.02500,0.68600,0.00000|0.00000,0.00000,0.00000|0.7000;b-spur|0.32500,0.02000,0.22000|1.82500,0.67800,0.00000|0.00000,0.00000,0.00000|0.4000;chute-C-floor|0.50000,0.01000,0.72981|1.70000,0.39000,0.92000|0.37160,0.00000,0.00000|0.2000;chute-C-rail-left|0.01000,0.03500,0.72981|1.19000,0.43500,0.92000|0.37160,0.00000,0.00000|0.3000;chute-C-rail-right|0.01000,0.03500,0.72981|2.21000,0.43500,0.92000|0.37160,0.00000,0.00000|0.3000;chute-D-floor|0.50000,0.01000,0.72981|1.70000,0.39000,-0.92000|-0.37160,0.00000,0.00000|0.2000;chute-D-rail-left|0.01000,0.03500,0.72981|1.19000,0.43500,-0.92000|-0.37160,0.00000,0.00000|0.3000;chute-D-rail-right|0.01000,0.03500,0.72981|2.21000,0.43500,-0.92000|-0.37160,0.00000,0.00000|0.3000;b-bin-floor|0.60000,0.02000,0.40000|3.35000,0.06000,0.00000|0.00000,0.00000,0.00000|0.8000;b-bin-wall-z+|0.60000,0.25000,0.01500|3.35000,0.33000,0.40000|0.00000,0.00000,0.00000|0.6000;b-bin-wall-z-|0.60000,0.25000,0.01500|3.35000,0.33000,-0.40000|0.00000,0.00000,0.00000|0.6000;b-bin-wall-x+|0.01500,0.25000,0.40000|3.95000,0.33000,0.00000|0.00000,0.00000,0.00000|0.6000;b-bin-entry-lip|0.01500,0.04000,0.40000|2.75000,0.12000,0.00000|0.00000,0.00000,0.00000|0.6000;b-drop-chute|0.46755,0.00800,0.21000|2.50000,0.39500,0.00000|0.00000,0.00000,-0.68339|0.2500;cage-C-floor|0.60000,0.04000,0.40000|2.00000,0.04000,2.00000|0.00000,0.00000,0.00000|0.8000;cage-C-wall-x-|0.02000,0.40000,0.40000|1.40000,0.40000,2.00000|0.00000,0.00000,0.00000|0.5000;cage-C-wall-x+|0.02000,0.40000,0.40000|2.60000,0.40000,2.00000|0.00000,0.00000,0.00000|0.5000;cage-C-wall-back|0.60000,0.40000,0.02000|2.00000,0.40000,2.40000|0.00000,0.00000,0.00000|0.5000;cage-C-entry-sill|0.60000,0.02000,0.01500|2.00000,0.10000,1.60000|0.00000,0.00000,0.00000|0.5000;cage-D-floor|0.60000,0.04000,0.40000|2.00000,0.04000,-2.00000|0.00000,0.00000,0.00000|0.8000;cage-D-wall-x-|0.02000,0.40000,0.40000|1.40000,0.40000,-2.00000|0.00000,0.00000,0.00000|0.5000;cage-D-wall-x+|0.02000,0.40000,0.40000|2.60000,0.40000,-2.00000|0.00000,0.00000,0.00000|0.5000;cage-D-wall-back|0.60000,0.40000,0.02000|2.00000,0.40000,-2.40000|0.00000,0.00000,0.00000|0.5000;cage-D-entry-sill|0.60000,0.02000,0.01500|2.00000,0.10000,-1.60000|0.00000,0.00000,0.00000|0.5000" + } +} \ No newline at end of file diff --git a/docs/stage2c_cad_visual/screenshots/01-current-before.png b/docs/stage2c_cad_visual/screenshots/01-current-before.png new file mode 100644 index 0000000..1dffd44 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/01-current-before.png differ diff --git a/docs/stage2c_cad_visual/screenshots/02-cad-contact-sheet.png b/docs/stage2c_cad_visual/screenshots/02-cad-contact-sheet.png new file mode 100644 index 0000000..9913c94 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/02-cad-contact-sheet.png differ diff --git a/docs/stage2c_cad_visual/screenshots/03-single-conveyor-overview.png b/docs/stage2c_cad_visual/screenshots/03-single-conveyor-overview.png new file mode 100644 index 0000000..488d195 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/03-single-conveyor-overview.png differ diff --git a/docs/stage2c_cad_visual/screenshots/04-motor-correct-position.png b/docs/stage2c_cad_visual/screenshots/04-motor-correct-position.png new file mode 100644 index 0000000..dbcdfc0 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/04-motor-correct-position.png differ diff --git a/docs/stage2c_cad_visual/screenshots/05-volumetric-rollers.png b/docs/stage2c_cad_visual/screenshots/05-volumetric-rollers.png new file mode 100644 index 0000000..8193df4 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/05-volumetric-rollers.png differ diff --git a/docs/stage2c_cad_visual/screenshots/06-volumetric-brackets.png b/docs/stage2c_cad_visual/screenshots/06-volumetric-brackets.png new file mode 100644 index 0000000..80da638 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/06-volumetric-brackets.png differ diff --git a/docs/stage2c_cad_visual/screenshots/07-real-sorter-mechanism.png b/docs/stage2c_cad_visual/screenshots/07-real-sorter-mechanism.png new file mode 100644 index 0000000..273d2f7 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/07-real-sorter-mechanism.png differ diff --git a/docs/stage2c_cad_visual/screenshots/08-mechanism-contact.png b/docs/stage2c_cad_visual/screenshots/08-mechanism-contact.png new file mode 100644 index 0000000..42ac5e2 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/08-mechanism-contact.png differ diff --git a/docs/stage2c_cad_visual/screenshots/09-no-measurement-panel.png b/docs/stage2c_cad_visual/screenshots/09-no-measurement-panel.png new file mode 100644 index 0000000..488d195 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/09-no-measurement-panel.png differ diff --git a/docs/stage2c_cad_visual/screenshots/10-full-height-layout.png b/docs/stage2c_cad_visual/screenshots/10-full-height-layout.png new file mode 100644 index 0000000..488d195 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/10-full-height-layout.png differ diff --git a/docs/stage2c_cad_visual/screenshots/11-box-scale.png b/docs/stage2c_cad_visual/screenshots/11-box-scale.png new file mode 100644 index 0000000..7b0cfd1 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/11-box-scale.png differ diff --git a/docs/stage2c_cad_visual/screenshots/12-bottle-scale.png b/docs/stage2c_cad_visual/screenshots/12-bottle-scale.png new file mode 100644 index 0000000..6660608 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/12-bottle-scale.png differ diff --git a/docs/stage2c_cad_visual/screenshots/13-plate-scale.png b/docs/stage2c_cad_visual/screenshots/13-plate-scale.png new file mode 100644 index 0000000..6660608 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/13-plate-scale.png differ diff --git a/docs/stage2c_cad_visual/screenshots/14-pen-close-up.png b/docs/stage2c_cad_visual/screenshots/14-pen-close-up.png new file mode 100644 index 0000000..6660608 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/14-pen-close-up.png differ diff --git a/docs/stage2c_cad_visual/screenshots/15-route-b-final.png b/docs/stage2c_cad_visual/screenshots/15-route-b-final.png new file mode 100644 index 0000000..6be327d Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/15-route-b-final.png differ diff --git a/docs/stage2c_cad_visual/screenshots/16-route-c-final.png b/docs/stage2c_cad_visual/screenshots/16-route-c-final.png new file mode 100644 index 0000000..7d1cd30 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/16-route-c-final.png differ diff --git a/docs/stage2c_cad_visual/screenshots/17-route-d-final.png b/docs/stage2c_cad_visual/screenshots/17-route-d-final.png new file mode 100644 index 0000000..a6d7d48 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/17-route-d-final.png differ diff --git a/docs/stage2c_cad_visual/screenshots/18-mobile-svg.png b/docs/stage2c_cad_visual/screenshots/18-mobile-svg.png new file mode 100644 index 0000000..007f565 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/18-mobile-svg.png differ diff --git a/docs/stage2c_cad_visual/screenshots/cad-contact-sheet.png b/docs/stage2c_cad_visual/screenshots/cad-contact-sheet.png new file mode 100644 index 0000000..9913c94 Binary files /dev/null and b/docs/stage2c_cad_visual/screenshots/cad-contact-sheet.png differ diff --git a/docs/stage2c_cad_visual/videos/README.md b/docs/stage2c_cad_visual/videos/README.md new file mode 100644 index 0000000..66c822c --- /dev/null +++ b/docs/stage2c_cad_visual/videos/README.md @@ -0,0 +1,16 @@ +# Stage 2C runtime videos + +Recorded WebM files are **not** shipped in this drop. + +Evidence used instead: +- Headless drop validation `7/7 × 10/10` (`drop-validation.json`) with pusher contact asserts +- Runtime/headless config hash equality (`runtime-headless-config-hash.json`) +- Still screenshots `07-real-sorter-mechanism.png`, `08-mechanism-contact.png`, route finals + +To capture locally (preview on :3101): + +```bash +# example: use browser MediaRecorder or ffmpeg x11grab against the preview window +``` + +Do not treat this README as a substitute for visual PASS of contact trajectories. diff --git a/docs/stage2c_cad_visual/visual-physics-review.md b/docs/stage2c_cad_visual/visual-physics-review.md new file mode 100644 index 0000000..6f69cd7 --- /dev/null +++ b/docs/stage2c_cad_visual/visual-physics-review.md @@ -0,0 +1,27 @@ +# Visual Physics Review — Stage 2C + +## Layout + +- Canvas heightRatio = 1.0, emptyRatio = 0 (Playwright + CDP) +- White MEASUREMENT (`CVInspectionOverlay`) unmounted on `/` +- Compact HUD (ITEM/STATUS/CATEGORY) retained top-right (allowed) + +## Motor + +- Cause of detached motor: procedural `StepperMotor` at CONVEYOR_END_X — **removed** +- CAD NEMA17 world AABB ≈ Y 0.56–0.61 (on frame under belt drive) — not floor-hidden + +## Mechanism + +- Junction paddle: SPEC_DERIVED (CAD lacks junction paddle solid) +- Contact-based C/D handoff unchanged; headless 7/7×10/10 with pusher contact + +## Parity + +- runtimeHash == headlessHash: 7ee15ad3e879a44a + +## Remaining visual debt + +- Entry/exit belt still SPEC_DERIVED (modular, not scaled CAD) +- CAD belt segment looks brighter/different from extensions +- Runtime WebM trajectories not recorded in this drop diff --git a/e2e/stage2c-layout.spec.ts b/e2e/stage2c-layout.spec.ts new file mode 100644 index 0000000..5e73839 --- /dev/null +++ b/e2e/stage2c-layout.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; + +test.setTimeout(90_000); + +test.describe('Stage 2C layout + measurement panel', () => { + test('default / has no Measurement panel and canvas fills viewport height', async ({ page }) => { + await page.goto('/?playback=paused'); + await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 }); + await page.waitForFunction(() => !document.body.innerText.includes('Loading 3D'), { + timeout: 60_000, + }); + await page.waitForTimeout(500); + + await expect(page.locator('.cv-overlay')).toHaveCount(0); + await expect(page.getByTestId('measurement-compact')).toHaveCount(0); + + const metrics = await page.evaluate(() => { + const vh = window.innerHeight; + const canvas = document.querySelector('canvas'); + const svg = document.querySelector('[data-testid="main-svg-fallback"]'); + const scene = canvas ?? svg; + if (!scene) return { ok: false as const, reason: 'no scene' }; + const r = scene.getBoundingClientRect(); + const heightRatio = r.height / vh; + const emptyBelow = Math.max(0, vh - (r.top + r.height)); + const emptyRatio = emptyBelow / vh; + return { + ok: true as const, + heightRatio, + emptyRatio, + sceneTop: r.top, + sceneHeight: r.height, + vh, + }; + }); + + expect(metrics.ok).toBe(true); + if (!metrics.ok) return; + expect(metrics.heightRatio).toBeGreaterThanOrEqual(0.85); + expect(metrics.emptyRatio).toBeLessThanOrEqual(0.2); + }); + + test('full Measurement panel only with debug=1&measurement=full', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 800 }); + await page.goto('/?debug=1&playback=paused'); + await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 }); + await expect(page.locator('.cv-overlay:not(.cv-overlay-compact)')).toHaveCount(0); + }); +}); diff --git a/src/components/ThreeD/ConveyorCadModel.tsx b/src/components/ThreeD/ConveyorCadModel.tsx index ee61a2b..b0f13fa 100644 --- a/src/components/ThreeD/ConveyorCadModel.tsx +++ b/src/components/ThreeD/ConveyorCadModel.tsx @@ -35,7 +35,16 @@ function materialFor(slot: string): THREE.MeshStandardMaterial { 'dark-mechanical': { color: '#3a4148', metalness: 0.6, roughness: 0.5 }, 'safety-yellow': { color: '#f5b301', metalness: 0.2, roughness: 0.55 }, }; - MATERIALS[slot] = new THREE.MeshStandardMaterial(defs[slot] ?? defs['painted-metal']); + const def = defs[slot] ?? defs['painted-metal']; + // Stage 2C §9.1 — opaque machine metals: no accidental alpha / DoubleSide bleed. + MATERIALS[slot] = new THREE.MeshStandardMaterial({ + ...def, + transparent: false, + opacity: 1, + depthWrite: true, + depthTest: true, + side: THREE.FrontSide, + }); } return MATERIALS[slot]; } @@ -102,6 +111,11 @@ export function ConveyorCadModel({ mesh.material = materialFor(slotFor(name)); mesh.castShadow = shadows; mesh.receiveShadow = shadows; + // Force opaque even if a future GLB embeds transparent materials. + const mat = mesh.material as THREE.MeshStandardMaterial; + mat.transparent = false; + mat.opacity = 1; + mat.depthWrite = true; if (name.startsWith('stop-gate/') || name.startsWith('rollers/Ролик') || name.startsWith('rollers/Вал')) { toPivot.push(mesh); } diff --git a/src/components/ThreeD/SorterDigitalTwinContinuous.tsx b/src/components/ThreeD/SorterDigitalTwinContinuous.tsx index b20ddaa..27d8c8d 100644 --- a/src/components/ThreeD/SorterDigitalTwinContinuous.tsx +++ b/src/components/ThreeD/SorterDigitalTwinContinuous.tsx @@ -42,14 +42,9 @@ import { BELT_TOP_Y, BELT_THICKNESS_M, CONVEYOR_WIDTH_M, - ROLLER_RADIUS_M, - ROLLER_SPACING_M, FRAME_HEIGHT_M, SIDE_GUARD_HEIGHT_M, LEG_WIDTH_M, - MOTOR_WIDTH_M, - MOTOR_HEIGHT_M, - MOTOR_DEPTH_M, DRIVE_ROLLER_RADIUS_M, ZONES, CAMERA_RIG, @@ -61,6 +56,11 @@ import { CAGE_FLOOR_Y, CONVEYOR_SPEED_MPS, } from '../../domain/physicalLayout'; +import { + CAD_ROLLER_PITCH_M, + CAD_ROLLER_RADIUS_M, + CAD_SUPPORT_SPACING_M, +} from '../../domain/cadAssemblyParams'; import { CHUTE_PITCH, CHUTE_LENGTH, CHUTE_MID_Y, CHUTE_MID_Z, CHUTE_X, CHUTE_HALF_W } from '../../domain/physicsWorldLayout'; import PerfCollector from './PerfCollector'; import PerfOverlay from './PerfOverlay'; @@ -133,8 +133,8 @@ function getItemMaterial(itemId: string) { // Physical layout constants const BELT_Y = BELT_TOP_Y; // 0.7m - top of belt where items ride const BELT_UNDERSIDE_Y = BELT_TOP_Y - BELT_THICKNESS_M; // 0.685m -const ROLLER_Y = BELT_UNDERSIDE_Y - ROLLER_RADIUS_M; // ~0.645m - roller center -const FRAME_TOP_Y = ROLLER_Y - ROLLER_RADIUS_M - 0.02; // Top of frame structure +const ROLLER_Y = BELT_UNDERSIDE_Y - CAD_ROLLER_RADIUS_M; +const FRAME_TOP_Y = ROLLER_Y - CAD_ROLLER_RADIUS_M - 0.02; const CONVEYOR_START_X = -4.2; const CONVEYOR_END_X = B_RECEIVER.transferEndX + 0.15; // main belt ends at short B spur tip /** Max items rendered simultaneously (perf cap). */ @@ -267,12 +267,19 @@ function MotionTrail({ ); } -/** Static roller cylinder — no per-frame rotation (belt stripes show movement). */ +/** Static roller — CAD pitch/radius for modular extensions (no non-uniform scale of CAD module). */ function StaticRoller({ position }: { position: [number, number, number] }) { return ( - - + + ); } @@ -302,56 +309,7 @@ function BeltStripe({ baseOffset, elapsedMs }: { baseOffset: number; elapsedMs: ); } -/** Stepper motor drive unit with pulse indicator */ -function StepperMotor({ position, pulseActive }: { position: [number, number, number]; pulseActive: boolean }) { - const pulseRef = useRef(null); - const rotationRef = useRef(0); - - useFrame((_, delta) => { - if (pulseRef.current && pulseActive) { - rotationRef.current += delta * 8; - pulseRef.current.rotation.z = rotationRef.current; - } - }); - - return ( - - {/* Motor body */} - - - - - {/* Motor shaft - rotates when active */} - - - - - {/* Drive pulley */} - - - - - {/* Belt to drive roller */} - - - - - {/* Pulse indicator LED */} - - - - - - ); -} +/** StepperMotor removed in Stage 2C — CAD motor-and-drive/NEMA17 is the sole drive unit. */ /** Laser beam from rangefinder to item — static opacity, no useFrame. */ function LaserBeam({ active, itemY }: { active: boolean; itemY: number }) { @@ -467,26 +425,27 @@ function SupportLeg({ x }: { x: number }) { * Belt top surface at 0.7m (BELT_TOP_Y) */ /** - * Belt extension segment (SPEC_DERIVED) — entry/exit sections flanking the - * CAD conveyor module. Same 500 mm width / 700 mm belt top as the real unit. + * Belt extension segment (SPEC_DERIVED modular) — entry/exit flanking the CAD + * module. Uses CAD roller pitch (500 mm) and radius (25 mm); never scales the GLB. */ function BeltSection({ startX, endX, simplified, shadows }: { startX: number; endX: number; simplified: boolean; shadows: boolean }) { - const spacing = simplified ? ROLLER_SPACING_M * 2 : ROLLER_SPACING_M; + const spacing = simplified ? CAD_ROLLER_PITCH_M * 2 : CAD_ROLLER_PITCH_M; const length = endX - startX; const centerX = (startX + endX) / 2; const rollerCount = Math.max(1, Math.floor(length / spacing)); + const rollerY = BELT_Y - BELT_THICKNESS_M - CAD_ROLLER_RADIUS_M - 0.01; const rollerPositions = useMemo(() => { const positions: [number, number, number][] = []; for (let i = 0; i < rollerCount; i++) { - positions.push([startX + spacing / 2 + i * spacing, ROLLER_Y, 0]); + positions.push([startX + spacing / 2 + i * spacing, rollerY, 0]); } return positions; - }, [rollerCount, spacing, startX]); + }, [rollerCount, spacing, startX, rollerY]); const legPositions = useMemo(() => { const positions: number[] = []; - for (let x = startX + 0.5; x < endX - 0.3; x += 2.0) { + for (let x = startX + 0.5; x < endX - 0.3; x += CAD_SUPPORT_SPACING_M) { positions.push(x); } return positions; @@ -496,23 +455,23 @@ function BeltSection({ startX, endX, simplified, shadows }: { startX: number; en - + - + - + - + - + {rollerPositions.map((pos, i) => ( @@ -525,17 +484,16 @@ function BeltSection({ startX, endX, simplified, shadows }: { startX: number; en } /** - * Conveyor: CAD module (REAL_CAD) + entry/exit belt extensions (SPEC_DERIVED). - * Gate/roller/belt motion is driven by the domain visual-state adapter. + * SorterCadAssembly (inline): sole conveyor geometry source — + * REAL_CAD module + modular SPEC_DERIVED entry/exit at CAD pitch. + * No procedural motor (CAD NEMA17 only). */ -function ConveyorBelt({ pulseActive, elapsedMs, simplified, shadows = false, gateOpen = false, beltVelocityMps = 0, rollerOmega = 0 }: { pulseActive: boolean; elapsedMs: number; simplified: boolean; shadows?: boolean; gateOpen?: boolean; beltVelocityMps?: number; rollerOmega?: number }) { +function ConveyorBelt({ pulseActive: _pulseActive, elapsedMs, simplified, shadows = false, gateOpen = false, beltVelocityMps = 0, rollerOmega = 0 }: { pulseActive: boolean; elapsedMs: number; simplified: boolean; shadows?: boolean; gateOpen?: boolean; beltVelocityMps?: number; rollerOmega?: number }) { const stripeOffsets = simplified ? [-2, 0, 2] : [-3, -1, 0.5, 2]; const [cadStart, cadEnd] = CONVEYOR_CAD_SPAN_X; return ( - - {/* CAD-derived conveyor module (real machine: frame, belt, rollers, - drive, metering gates, servo diverters, camera arch) */} + - {/* Entry extension: zone A feed into the CAD module */} - {/* Exit extension: CAD module discharge to the B spur */} - {/* Belt stripes — deterministic movement synced to item (offset = time * 1 m/s) */} {stripeOffsets.map((offset, i) => ( ))} - {/* Drive roller at end (larger) */} - + - + - {/* Tension roller at start (larger) */} - + - + - {/* Stepper motor at drive end */} - - - {/* End caps / guards */} - + - + ); diff --git a/src/components/ThreeD/SorterPhysics.tsx b/src/components/ThreeD/SorterPhysics.tsx index 9d05758..86acce9 100644 --- a/src/components/ThreeD/SorterPhysics.tsx +++ b/src/components/ThreeD/SorterPhysics.tsx @@ -17,8 +17,9 @@ import { useRef, type ReactNode } from 'react'; import { useFrame } from '@react-three/fiber'; import { Physics, RigidBody, CuboidCollider, useRapier } from '@react-three/rapier'; import { getStaticColliders } from '../../domain/physicsWorldLayout'; +import { PHYSICS_TIMESTEP_SEC } from '../../domain/physicsTimestep'; -export const PHYSICS_DT = 1 / 60; +export const PHYSICS_DT = PHYSICS_TIMESTEP_SEC; const MAX_SUBSTEPS = 4; /** diff --git a/src/domain/cadAssemblyParams.ts b/src/domain/cadAssemblyParams.ts new file mode 100644 index 0000000..822b8b1 --- /dev/null +++ b/src/domain/cadAssemblyParams.ts @@ -0,0 +1,45 @@ +/** + * Stage 2C — modular CAD conveyor assembly parameters. + * + * Derived from 3d_models/conveer.FCStd (manifest Body and Link bbox mm) and + * OZON Track 3 layout (500 mm belt / 700 mm height). Extensions flanking the + * ~2.01 m CAD module use the same pitch/width/height — never non-uniform scale. + */ + +/** Author CAD module length after bake+placement (m). */ +export const CAD_MODULE_LENGTH_M = 2.01; +/** CAD roller assembly pitch along the line (profiles / Link005 spacing ~500 mm). */ +export const CAD_ROLLER_PITCH_M = 0.5; +/** CAD roller outer radius — Body002 roller diameter 50 mm. */ +export const CAD_ROLLER_RADIUS_M = 0.025; +/** Spec / CAD belt width. */ +export const CAD_CONVEYOR_WIDTH_M = 0.5; +/** Belt top height from floor. */ +export const CAD_BELT_HEIGHT_M = 0.7; +/** Support leg spacing along extensions. */ +export const CAD_SUPPORT_SPACING_M = 2.0; +/** Full domain line length (entry A to B spur tip), meters. */ +export const CAD_TOTAL_LINE_LENGTH_M = 8.5; + +export const CAD_ASSEMBLY_PARAMS = { + segmentLength: CAD_MODULE_LENGTH_M, + rollerPitch: CAD_ROLLER_PITCH_M, + rollerRadius: CAD_ROLLER_RADIUS_M, + conveyorWidth: CAD_CONVEYOR_WIDTH_M, + beltHeight: CAD_BELT_HEIGHT_M, + supportSpacing: CAD_SUPPORT_SPACING_M, + totalLineLength: CAD_TOTAL_LINE_LENGTH_M, +} as const; + +/** Structured correction transforms (bake is authoritative; JSX must not invent offsets). */ +export const CAD_TRANSFORM_MANIFEST = { + bakeFormula: '(x,y,z)_mm_Zup -> (-x, z, y+250)/1000 Y-up meters', + worldPlacement: [-2.02, 0.594, 0] as [number, number, number], + moduleSpanX: [-2.086, -0.076] as [number, number], + motorNode: 'motor-and-drive/NEMA17', + motorWorldAabbApprox: { + min: [-2.062, 0.564, 0.212], + max: [-2.02, 0.606, 0.284], + note: 'On frame at belt height — not under floor. Detached motor was procedural StepperMotor.', + }, +} as const; diff --git a/src/domain/physicsConfigHash.ts b/src/domain/physicsConfigHash.ts new file mode 100644 index 0000000..21b3cbd --- /dev/null +++ b/src/domain/physicsConfigHash.ts @@ -0,0 +1,89 @@ +/** + * Stage 2C §11.1 — machine-readable runtime ↔ headless physics config hash. + * Both paths must consume the same static colliders, pusher geometry, dt, gravity. + * Hash is pure-JS (no Node crypto) so the module stays isomorphic. + */ +import { getStaticColliders } from './physicsWorldLayout'; +import { PUSHER } from './pusherMotion'; +import { PHYSICS_TIMESTEP_SEC, PHYSICS_GRAVITY } from './physicsTimestep'; + +export { PHYSICS_TIMESTEP_SEC, PHYSICS_GRAVITY } from './physicsTimestep'; + +export interface PhysicsConfigSnapshot { + timestep: number; + gravity: [number, number, number]; + pusherHalfExtents: [number, number, number]; + pusherCenterY: number; + pusherEngage: { x: number; z: number }; + staticColliderCount: number; + staticColliderIds: string[]; + staticColliderFingerprint: string; +} + +function fingerprintColliders(): string { + const defs = getStaticColliders(); + return defs + .map((d) => + [ + d.id, + d.halfExtents.map((n) => n.toFixed(5)).join(','), + d.position.map((n) => n.toFixed(5)).join(','), + d.rotation.map((n) => n.toFixed(5)).join(','), + d.friction.toFixed(4), + ].join('|'), + ) + .join(';'); +} + +/** FNV-1a 64-bit (as hex) — deterministic, isomorphic. */ +function fnv1aHex(input: string): string { + let h = 0xcbf29ce484222325n; + const prime = 0x100000001b3n; + for (let i = 0; i < input.length; i++) { + h ^= BigInt(input.charCodeAt(i)); + h = (h * prime) & 0xffffffffffffffffn; + } + return h.toString(16).padStart(16, '0'); +} + +export function buildPhysicsConfigSnapshot(timestep: number): PhysicsConfigSnapshot { + const defs = getStaticColliders(); + return { + timestep, + gravity: PHYSICS_GRAVITY, + pusherHalfExtents: [...PUSHER.halfExtents] as [number, number, number], + pusherCenterY: PUSHER.centerY, + pusherEngage: { x: PUSHER.engageX, z: PUSHER.engageZ }, + staticColliderCount: defs.length, + staticColliderIds: defs.map((d) => d.id), + staticColliderFingerprint: fingerprintColliders(), + }; +} + +export function hashPhysicsConfig(snapshot: PhysicsConfigSnapshot): string { + return fnv1aHex(JSON.stringify(snapshot)); +} + +export function getRuntimePhysicsConfigHash(): string { + return hashPhysicsConfig(buildPhysicsConfigSnapshot(PHYSICS_TIMESTEP_SEC)); +} + +export function getHeadlessPhysicsConfigHash(headlessTimestep = PHYSICS_TIMESTEP_SEC): string { + return hashPhysicsConfig(buildPhysicsConfigSnapshot(headlessTimestep)); +} + +export function assertRuntimeHeadlessParity(headlessTimestep = PHYSICS_TIMESTEP_SEC): { + runtime: string; + headless: string; + equal: boolean; + timestepEqual: boolean; +} { + const runtime = getRuntimePhysicsConfigHash(); + const headless = getHeadlessPhysicsConfigHash(headlessTimestep); + return { + runtime, + headless, + equal: runtime === headless, + timestepEqual: headlessTimestep === PHYSICS_TIMESTEP_SEC, + }; +} diff --git a/src/domain/physicsDropSim.ts b/src/domain/physicsDropSim.ts index 87d8468..765c4a7 100644 --- a/src/domain/physicsDropSim.ts +++ b/src/domain/physicsDropSim.ts @@ -21,8 +21,9 @@ import { getPusherState, PUSHER } from './pusherMotion'; import { receiverContains, type ReceiverZone } from './receiverVolumes'; import { resolveItem } from '../data/resolveItem'; import { BELT_TOP_Y } from './physicalLayout'; +import { PHYSICS_TIMESTEP_SEC } from './physicsTimestep'; -export const SIM_DT = 1 / 60; +export const SIM_DT = PHYSICS_TIMESTEP_SEC; export const SIM_SETTLE_SECONDS = 6.0; let rapierReady = false; diff --git a/src/domain/physicsTimestep.ts b/src/domain/physicsTimestep.ts new file mode 100644 index 0000000..9a27bdd --- /dev/null +++ b/src/domain/physicsTimestep.ts @@ -0,0 +1,3 @@ +/** Shared fixed physics timestep — runtime Rapier and headless must match. */ +export const PHYSICS_TIMESTEP_SEC = 1 / 60; +export const PHYSICS_GRAVITY: [number, number, number] = [0, -9.81, 0]; diff --git a/src/domain/stage2cCadVisual.test.ts b/src/domain/stage2cCadVisual.test.ts new file mode 100644 index 0000000..b3b0e62 --- /dev/null +++ b/src/domain/stage2cCadVisual.test.ts @@ -0,0 +1,52 @@ +/** + * Stage 2C — unit tests for CAD assembly params + runtime/headless physics hash. + */ +import { describe, expect, it } from 'vitest'; +import { + CAD_ASSEMBLY_PARAMS, + CAD_TRANSFORM_MANIFEST, + CAD_ROLLER_PITCH_M, + CAD_CONVEYOR_WIDTH_M, + CAD_BELT_HEIGHT_M, +} from './cadAssemblyParams'; +import { + assertRuntimeHeadlessParity, + PHYSICS_TIMESTEP_SEC, + buildPhysicsConfigSnapshot, +} from './physicsConfigHash'; +import { SIM_DT } from './physicsDropSim'; + +describe('Stage 2C CAD assembly params', () => { + it('preserves official belt width and height', () => { + expect(CAD_CONVEYOR_WIDTH_M).toBe(0.5); + expect(CAD_BELT_HEIGHT_M).toBe(0.7); + expect(CAD_ASSEMBLY_PARAMS.rollerPitch).toBe(CAD_ROLLER_PITCH_M); + }); + + it('documents motor on-frame AABB (not under floor)', () => { + const aabb = CAD_TRANSFORM_MANIFEST.motorWorldAabbApprox; + expect(aabb.min[1]).toBeGreaterThan(0.4); + expect(aabb.max[1]).toBeLessThan(0.8); + }); +}); + +describe('Stage 2C runtime/headless physics parity', () => { + it('headless SIM_DT matches canonical timestep', () => { + expect(SIM_DT).toBe(PHYSICS_TIMESTEP_SEC); + }); + + it('hashes match for runtime and headless snapshots', () => { + const parity = assertRuntimeHeadlessParity(SIM_DT); + expect(parity.timestepEqual).toBe(true); + expect(parity.equal).toBe(true); + expect(parity.runtime).toBe(parity.headless); + expect(parity.runtime).toMatch(/^[a-f0-9]{16}$/); + }); + + it('snapshot includes static colliders and pusher geometry', () => { + const snap = buildPhysicsConfigSnapshot(PHYSICS_TIMESTEP_SEC); + expect(snap.staticColliderCount).toBeGreaterThan(5); + expect(snap.pusherHalfExtents[0]).toBe(0.5); + expect(snap.gravity).toEqual([0, -9.81, 0]); + }); +}); diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index c253ea8..9629955 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -241,7 +241,16 @@ export default function MainPage({ const caseProgress = getCaseProgress(playback); const measurementData = useMemo(() => getMeasurementData(playback), [playback]); + // Stage 2C §6: large white MEASUREMENT panel is debug-only. + // Default / — never mount. ?debug=1 — compact. ?debug=1&measurement=full — full panel. + const measurementMode = useMemo(() => { + if (typeof window === 'undefined') return 'off' as const; + const params = new URLSearchParams(window.location.search); + if (params.get('debug') !== '1') return 'off' as const; + return params.get('measurement') === 'full' ? 'full' as const : 'compact' as const; + }, []); const showMeasurement = + measurementMode !== 'off' && !presentationMode && shouldShowMeasurement(playback.currentPhase) && (isRunning || isPaused) && @@ -306,9 +315,29 @@ export default function MainPage({ )} - {width >= 768 && ( + {width >= 768 && measurementMode === 'full' && ( )} + {width >= 768 && measurementMode === 'compact' && showMeasurement && ( +
+
+ MEAS + {Math.round((measurementData.confidence ?? 0) * 100)}% +
+
+
+ L×W×H + + {measurementData.measuredLengthMm}×{measurementData.measuredWidthMm}×{measurementData.measuredHeightMm} + +
+
+ K + {measurementData.roundnessK.toFixed(2)} +
+
+
+ )} {!presentationMode && (!stage0.enabled || stage0.hud) && (
diff --git a/src/styles.css b/src/styles.css index b75681a..2680b45 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2046,9 +2046,12 @@ button:disabled { .main-page { position: relative; width: 100vw; - height: 100vh; + height: 100dvh; + height: 100vh; /* fallback */ + min-height: 100dvh; overflow: hidden; background: var(--bg); + padding-bottom: env(safe-area-inset-bottom, 0); } .main-demo-viewport { @@ -2057,11 +2060,26 @@ button:disabled { z-index: 1; } -.main-demo-viewport .digital-twin-wrap { +/* Stage 2C §7 — canvas-holder must fill viewport; % height of auto parent was collapsing Canvas. */ +.main-demo-viewport .canvas-holder { + position: absolute; + inset: 0; + width: 100%; height: 100%; + min-height: 100%; +} + +.main-demo-viewport .digital-twin-wrap { + position: absolute; + inset: 0; + height: 100%; + width: 100%; } .main-demo-viewport .digital-twin-canvas { + position: absolute; + inset: 0; + width: 100% !important; height: 100% !important; min-height: 100% !important; border: none; @@ -2544,7 +2562,7 @@ button:disabled { left: 16px; z-index: 15; width: 220px; - max-height: calc(100vh - 220px); + max-height: calc(100dvh - 220px); overflow-y: auto; border: 1px solid rgba(37, 99, 235, 0.2); border-radius: 10px; @@ -2553,6 +2571,16 @@ button:disabled { box-shadow: 0 2px 16px rgba(37, 99, 235, 0.12); font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 10px; + pointer-events: auto; +} + +.cv-overlay-compact { + width: 160px; + top: 88px; + left: auto; + right: 16px; + max-height: 120px; + pointer-events: none; } .cv-header {