From f2ce6113304d3650abc71e462d8c8d0dc686c86f Mon Sep 17 00:00:00 2001 From: root Date: Sun, 2 Aug 2026 22:51:41 +0200 Subject: [PATCH] fix: align CV rules and refresh final documentation Co-authored-by: Cursor --- README.md | 1 + cv/README.md | 4 +- cv/classify.py | 9 +- cv/stabilize.py | 13 +- cv/test_classify.py | 42 ++++- docs/ENGINEERING.md | 4 +- e2e/routes.spec.ts | 5 +- src/data/productionStatusSummary.ts | 133 ++++++++++++-- src/pages/DocumentationPage.tsx | 271 ++++++++++++++++------------ src/styles.css | 171 ++++++++++++++---- 10 files changed, 462 insertions(+), 191 deletions(-) diff --git a/README.md b/README.md index c196810..7b74e05 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ OWL PRIME — инженерный контур предварительной 2. Если габариты OK и **K > 0.8** (круг) → **D**. 3. Иначе → **B**. 4. **C-priority:** негабарит + круг → только **C**. +5. Граница: **K = 0.8 не круг** (→ B, не D). Одинаково в web и CV. ## Архитектура diff --git a/cv/README.md b/cv/README.md index 64f4f78..c2a55b2 100644 --- a/cv/README.md +++ b/cv/README.md @@ -36,8 +36,8 @@ Primary modules: `main.py` (live), `demo.py` (HUD), `classify.py` (rules), `meas ## Classification rules (Track 3) 1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C** -2. Else if circularity exceeds the official threshold **0.8** → **D** - (web twin uses strict `K > 0.8`; this prototype uses `circle_ratio >= 0.8`) +2. Else if `circle_ratio > 0.8` → **D** + (`K == 0.8` is **not** circular — same strict rule as web `classifier.ts`) 3. Else → **B** Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames. diff --git a/cv/classify.py b/cv/classify.py index 2776575..492bcec 100644 --- a/cv/classify.py +++ b/cv/classify.py @@ -79,13 +79,14 @@ def classify( """ Порядок ТЗ: 1) габариты → иначе C (приоритет над кругом) - 2) если r_in/r_out >= 0.8 в любом сечении → D + 2) если r_in/r_out > 0.8 в любом сечении → D + (K == 0.8 НЕ круг — как в web classifier.ts) 3) иначе → B """ dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm) passes = check_size(dims, min_mm, max_mm) ratio = float(measurement.circle_ratio) - circular = ratio >= float(circle_ratio_threshold) + circular = ratio > float(circle_ratio_threshold) clipped = bool(getattr(measurement, "clipped_by_frame", False)) if not passes or clipped: @@ -112,7 +113,7 @@ def classify( circle_ratio=ratio, passes_size=True, is_circular=True, - reason=f"круг в сечении: r_in/r_out={ratio:.3f} >= {circle_ratio_threshold}", + reason=f"круг в сечении: r_in/r_out={ratio:.3f} > {circle_ratio_threshold}", ) return ClassificationResult( @@ -121,7 +122,7 @@ def classify( circle_ratio=ratio, passes_size=True, is_circular=False, - reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} < {circle_ratio_threshold}", + reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} <= {circle_ratio_threshold}", ) diff --git a/cv/stabilize.py b/cv/stabilize.py index 6d326cc..9b73501 100644 --- a/cv/stabilize.py +++ b/cv/stabilize.py @@ -113,12 +113,13 @@ class DecisionStabilizer: top_m = _median(self._tops) sec_m = _median(self._secs) ratio_p75 = _percentile(self._ratios, 75) - # устойчивый круг: медиана >= 0.8 ИЛИ (медиана сечений >= 0.8 и ≥ половины окна сильные) + # устойчивый круг: медиана > 0.8 ИЛИ (медиана сечений > 0.8 и ≥ половины окна сильные) + # K == 0.8 официально НЕ круг (строгое > threshold) sec_strong = ( - sum(1 for x in self._secs if x >= self.enter_circle) / max(1, len(self._secs)) + sum(1 for x in self._secs if x > self.enter_circle) / max(1, len(self._secs)) ) - circular = ratio >= self.enter_circle or ( - sec_m >= self.enter_circle and sec_strong >= 0.55 + circular = ratio > self.enter_circle or ( + sec_m > self.enter_circle and sec_strong >= 0.55 ) ratio_show = max(ratio, sec_m) if circular else ratio @@ -153,7 +154,7 @@ class DecisionStabilizer: reason=( f"круг: med={ratio:.3f} p75={ratio_p75:.3f} " f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} " - f">= {self.enter_circle}" + f"> {self.enter_circle}" ), ) else: @@ -165,7 +166,7 @@ class DecisionStabilizer: is_circular=False, reason=( f"не круг: med={ratio:.3f} sec={sec_m:.3f} " - f"strong={sec_strong:.0%} < {self.enter_circle}" + f"strong={sec_strong:.0%} <= {self.enter_circle}" ), ) diff --git a/cv/test_classify.py b/cv/test_classify.py index c18645b..eda5c83 100755 --- a/cv/test_classify.py +++ b/cv/test_classify.py @@ -43,11 +43,40 @@ def test_lying_bottle_must_be_D_not_B(): assert r.category.zone == "D" -def test_border_circle(): - r = classify_from_dims(100, 50, 50, circle_ratio=0.8) +def test_strict_circle_boundary(): + """Официально: круг только при K > 0.8. K == 0.8 → B (не D).""" + below = classify_from_dims(100, 50, 50, circle_ratio=0.799999) + assert below.category == Category.SUITABLE + assert below.category.zone == "B" + assert below.is_circular is False + + exact = classify_from_dims(100, 50, 50, circle_ratio=0.8) + assert exact.category == Category.SUITABLE + assert exact.category.zone == "B" + assert exact.is_circular is False + + above = classify_from_dims(100, 50, 50, circle_ratio=0.800001) + assert above.category == Category.NEED_PACK + assert above.category.zone == "D" + assert above.is_circular is True + + +def test_normal_non_circular_B(): + r = classify_from_dims(120, 80, 40, circle_ratio=0.5) + assert r.category == Category.SUITABLE + assert r.category.zone == "B" + + +def test_normal_circular_above_threshold_D(): + r = classify_from_dims(100, 50, 50, circle_ratio=0.81) assert r.category == Category.NEED_PACK - r2 = classify_from_dims(100, 50, 50, circle_ratio=0.799) - assert r2.category == Category.SUITABLE + assert r.category.zone == "D" + + +def test_oversized_circular_still_C(): + r = classify_from_dims(500, 100, 100, circle_ratio=0.95) + assert r.category == Category.OVERSIZE + assert r.category.zone == "C" def test_border_dims_strict(): @@ -80,7 +109,10 @@ if __name__ == "__main__": test_too_small() test_need_pack_cylinder() test_lying_bottle_must_be_D_not_B() - test_border_circle() + test_strict_circle_boundary() + test_normal_non_circular_B() + test_normal_circular_above_threshold_D() + test_oversized_circular_still_C() test_border_dims_strict() test_dims_order_independent() print("OK: all classification tests passed") diff --git a/docs/ENGINEERING.md b/docs/ENGINEERING.md index a41a308..c9b5c49 100644 --- a/docs/ENGINEERING.md +++ b/docs/ENGINEERING.md @@ -20,7 +20,7 @@ Companion to `README.md` and `/documentation`. Not a stage changelog. | `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same | | Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge | | Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` | -| Roundness | K > 0.8 (web); CV code uses ≥ 0.8 | `classifier.ts` / `cv/classify.py` | +| Roundness | K > 0.8 (K = 0.8 not circular); web + CV aligned | `classifier.ts` / `cv/classify.py` | | Belt speed (sim) | 1.0 m/s | runtime / physics helpers | | Physics timestep | 1/60 s | Rapier world step | | `CONVEYOR_CAD_URL` | `/models/sorter/conveyor-clean.glb` | `ConveyorCadModel.tsx` | @@ -73,7 +73,7 @@ Grounded only in present sources + current `main` code/tests (not a full officia | B/C/D bounds & roundness | `official_sources/doc-1783095831.pdf` | web `classifier.ts`, CV `cv/classify.py` | unit tests | PARTIAL (PDF not re-parsed each pass) | | Digital twin demo | workspace PDFs in `input_info/` | `/` + `/documentation` | e2e smoke/routes, production | PASS (demo present) | | CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in `/documentation` | PASS (assets present) | -| Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV | `cv/README.md`, `test_classify.py` | PARTIAL (prototype, not live web) | +| Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV on `main` | `cv/README.md`, `test_classify.py` | PARTIAL (WORKING_PROTOTYPE, not live web) | | Physical industrial line | scoring/workspace PDFs | web physics + optional MQTT CV | code; contact not fully validated | PARTIAL | | Presentation | platform rules | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10-page PDF in repo | PASS (file present) | diff --git a/e2e/routes.spec.ts b/e2e/routes.spec.ts index 23d4c43..1572ae6 100644 --- a/e2e/routes.spec.ts +++ b/e2e/routes.spec.ts @@ -10,7 +10,10 @@ test.describe('routes', () => { await page.getByTestId('nav-documentation').first().click(); await expect(page).toHaveURL(/\/documentation\/?$/); await expect(page.getByTestId('documentation-page')).toBeVisible(); - await expect(page.getByTestId('docs-production-status')).toContainText('DATA_ACQUISITION_PACK_READY'); + await expect(page.getByTestId('docs-production-status')).toContainText( + 'FINAL_ENGINEERING_PROTOTYPE_READY', + ); + await expect(page.getByTestId('docs-production-status')).toContainText('WORKING_PROTOTYPE'); await expect(page.getByTestId('nav-documentation').first()).toHaveClass(/active/); await page.reload(); diff --git a/src/data/productionStatusSummary.ts b/src/data/productionStatusSummary.ts index e1b8402..68a246c 100644 --- a/src/data/productionStatusSummary.ts +++ b/src/data/productionStatusSummary.ts @@ -4,16 +4,29 @@ * Unverified / planned items are labeled explicitly — never implied complete. */ -/** Kept for e2e (`docs-production-status`) and acquisition-pack provenance. */ export const PRODUCTION_STATUS = { - acquisitionPackStatus: 'DATA_ACQUISITION_PACK_READY', - webTwinStatus: 'BASELINE_PRESERVED', + projectStatus: 'FINAL_ENGINEERING_PROTOTYPE_READY', + webTwinStatus: 'PRODUCTION_LIVE', + cvStatus: 'WORKING_PROTOTYPE', + cvLiveIntegrated: false, unitTests: '196/196', + cvTests: 'PASS (no camera)', productionBuild: 'PASS', + productionUrl: 'https://arhipovdan.ru', contactPhysics: 'NOT_FULLY_VALIDATED', officialCompliance: 'PARTIAL_SOURCES_PRESENT', + canonicalBranch: 'main', } as const; +export const SOLUTION_COMPONENTS = [ + 'Web digital twin (https://arhipovdan.ru)', + 'Real CV working prototype (cv/, RealSense D415 + OpenCV)', + 'Physical experimental conveyor stand', + 'Author CAD (3d_models/conveer.FCStd)', + 'Official B/C/D classifier (web + CV, K > 0.8)', + 'Engineering documentation (/documentation)', +] as const; + export const CONFIRMED_LAYOUT = { workspaceMm: { length: 10000, width: 6000 }, conveyorWidthMm: 500, @@ -23,7 +36,7 @@ export const CONFIRMED_LAYOUT = { } as const; export const CLASSIFIER_BOUNDS = { - status: 'CURRENT_IMPLEMENTATION_VERIFIED_IN_CODE' as const, + status: 'ALIGNED_WEB_AND_CV' as const, officialSourceReference: 'official_sources/doc-1783095831.pdf', officialSourceParsedThisPass: false, minExclusiveMm: { width: 10, depth: 10, height: 10 }, @@ -33,8 +46,63 @@ export const CLASSIFIER_BOUNDS = { min: '> 10×10×10 мм', max: '< 450×320×320 мм', roundness: 'K > 0.8', + exactBoundary: 'K = 0.8 → B (not circular), not D', }, - checkOrder: 'dimensions→C, else circular→D, else B' as const, + checkOrder: 'dimensions→C, else circular (K>0.8)→D, else B' as const, +} as const; + +export const CV_PROTOTYPE = { + path: 'cv/', + status: 'WORKING_PROTOTYPE', + hardware: 'Intel RealSense D415', + software: 'Python + OpenCV (+ optional MQTT)', + liveIntegrated: false, + pipeline: [ + 'depth frame', + 'segmentation', + 'object contour', + 'L×W×H', + 'roundness K', + 'B/C/D', + 'optional MQTT', + ] as const, + notes: [ + 'CV works as a separate prototype under cv/ on main.', + 'CV is not connected directly to the live public website.', + 'Hardware live validation requires RealSense D415.', + 'No-camera unit tests and compileall run in CI/local verification.', + ], +} as const; + +export const PHYSICAL_STAND = { + status: 'EXPERIMENTAL_PROTOTYPE', + elements: [ + 'Physical belt conveyor', + 'Camera mounting structure above the belt', + 'Intel RealSense D415', + 'Electronics / control nodes', + 'Experimental actuators and printed components', + ] as const, + purpose: 'Measurements, calibration, and hardware validation — not claimed as industrial end-to-end certified sorting.', +} as const; + +export const ARCHITECTURE = { + realPath: [ + 'physical product', + 'RealSense D415', + 'OpenCV measurement', + 'classifier (K > 0.8)', + 'B/C/D result', + 'optional MQTT/controller', + ] as const, + digitalPath: [ + 'digital product', + 'simulated measurement', + 'same classifier rules', + 'route command', + 'digital twin', + 'B/C/D receiver', + ] as const, } as const; export const ROUTE_MAPPING = [ @@ -72,6 +140,7 @@ export const PHYSICS_STATUS = { 'Synchronized CAD diverter visual / kinematic targets', 'CCD enabled for light/thin SKUs in runtime and headless sim', 'Visual/physics spawn gating via product asset preload', + 'Belt speed target 1.0 m/s; physics timestep 1/60 s', ], notFullyValidated: [ 'Complete contact-only routing through CAD diverters', @@ -83,18 +152,26 @@ export const PHYSICS_STATUS = { planned: [ 'Visual full belt loop with surface-velocity coupling', 'Controlled tangential friction at 1 m/s', - 'Dynamic rigid bodies for divert segment with fixed timestep', 'Per-SKU collider, damping, and friction profiles', ], } as const; +export const MOBILE_BEHAVIOR = { + desktop: 'Interactive WebGL 3D digital twin', + mobile: + 'Capability-based tier: WebGL when viable; SVG/2D lite fallback on low FPS / missing WebGL (not claimed as full 3D)', +} as const; + export const VALIDATION_BOARD = [ - { item: 'Unit tests', status: '196/196 PASS' }, + { item: 'Web unit tests', status: '196/196 PASS' }, { item: 'Production build', status: 'PASS' }, - { item: 'Active routes / + /documentation', status: 'PASS' }, + { item: 'Focused E2E (/ + /documentation)', status: 'PASS' }, + { item: 'CV compileall', status: 'PASS' }, + { item: 'CV classify/geometry tests', status: 'PASS (no camera)' }, + { item: 'Production / and /documentation', status: 'PASS' }, { item: 'conveyor-clean.glb checksum', status: 'PASS' }, { item: 'Author FCStd checksum', status: 'PASS' }, - { item: 'Frozen diverter angles / 0.50 s', status: 'PASS' }, + { item: 'Classifier K > 0.8 (web + CV)', status: 'ALIGNED' }, { item: 'Full contact physics', status: 'NOT_FULLY_VALIDATED' }, ] as const; @@ -146,7 +223,14 @@ export const OFFICIAL_SOURCE_MATRIX = [ purpose: 'Classifier bounds authority cited by code', present: true, canonical: true, - usage: 'Referenced by classifier.ts; not re-parsed this pass', + usage: 'Referenced by classifier.ts and cv/classify.py', + }, + { + source: 'presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf', + purpose: 'Final presentation (10 slides)', + present: true, + canonical: true, + usage: 'Single presentation PDF in repository', }, { source: 'input_info/extracted/Постановка_Задача_3_сжато_2.pdf', @@ -169,15 +253,24 @@ export const RUNTIME_FLOW = [ 'RECEIVER', ] as const; -export const CURRENT_LIMITATIONS = [ - 'Full physical contact sorting through CAD diverters is not fully validated.', - 'Belt surface-velocity drive at exactly 1 m/s is planned, not complete.', - 'Per-SKU physical parameters still require profiling/calibration.', - 'Author CAD horn / transmission incomplete in active GLB.', - 'Official compliance is partial: missing extracted task PDF; scoring PDF not re-parsed this pass.', - 'Real CV prototype lives in cv/ (WORKING_PROTOTYPE) and is not connected to the live website.', - 'Live CV validation requires Intel RealSense D415 hardware.', +export const REPOSITORY_LAYOUT = [ + 'src/ — web digital twin', + 'cv/ — RealSense + OpenCV prototype', + '3d_models/ — author CAD', + 'public/ — runtime GLB/STL/draco', + 'docs/ — engineering notes', + 'presentation/ — final PDF', + 'e2e/ + Vitest — tests', + 'Docker / nginx — deployment', ] as const; -/** @deprecated alias — historical Gate wording retained for acquisition-pack note */ -export const DATA_ACQUISITION_SEQUENCE = ['MEASURE', 'VERIFY', 'FREEZE', 'DESIGN'] as const; +export const CURRENT_LIMITATIONS = [ + 'Engineering prototype, not an industrial-certified PAK.', + 'Real CV prototype is not live-integrated into the public website.', + 'Live camera mode requires Intel RealSense D415 hardware.', + 'Physical stand parameters still require calibration against the digital twin.', + 'Full physical contact sorting through CAD diverters is not fully validated.', + 'Belt surface-velocity drive at exactly 1 m/s is not fully validated.', + 'Author CAD horn / transmission incomplete in active GLB.', + 'Cloud presentation/video links for the platform form are provided separately by the team.', +] as const; diff --git a/src/pages/DocumentationPage.tsx b/src/pages/DocumentationPage.tsx index f07d292..720fd49 100644 --- a/src/pages/DocumentationPage.tsx +++ b/src/pages/DocumentationPage.tsx @@ -1,30 +1,37 @@ import AppNav from '../components/AppNav'; import { + ARCHITECTURE, CAD_PROVENANCE, CLASSIFIER_BOUNDS, CONFIRMED_LAYOUT, CURRENT_LIMITATIONS, + CV_PROTOTYPE, DIVERTER_KINEMATICS, + MOBILE_BEHAVIOR, OFFICIAL_SOURCE_MATRIX, + PHYSICAL_STAND, PHYSICS_STATUS, PRODUCTION_STATUS, + REPOSITORY_LAYOUT, ROUTE_MAPPING, RUNTIME_FLOW, + SOLUTION_COMPONENTS, VALIDATION_BOARD, } from '../data/productionStatusSummary'; const SECTIONS = [ - { id: 'about', title: '1. Цель проекта' }, - { id: 'runtime-flow', title: '2. Runtime flow' }, - { id: 'layout', title: '3. Активная линия' }, - { id: 'classifier', title: '4. Классификация' }, - { id: 'routes', title: '5. Маршруты B/C/D' }, - { id: 'diverter', title: '6. Diverter state machine' }, - { id: 'cad', title: '7. CAD provenance' }, - { id: 'physics', title: '8. Физика' }, - { id: 'validation', title: '9. Validation status' }, - { id: 'official', title: '10. Official-source matrix' }, - { id: 'limits', title: '11. Known limitations' }, + { id: 'about', title: '1. Обзор проекта' }, + { id: 'production-web', title: '2. Production web' }, + { id: 'cv', title: '3. Real CV prototype' }, + { id: 'classifier', title: '4. Официальная классификация' }, + { id: 'stand', title: '5. Физический стенд' }, + { id: 'twin', title: '6. Digital twin' }, + { id: 'architecture', title: '7. Архитектура' }, + { id: 'repository', title: '8. Репозиторий' }, + { id: 'validation', title: '9. Validation' }, + { id: 'limits', title: '10. Ограничения' }, + { id: 'routes', title: '11. Маршруты B/C/D' }, + { id: 'official', title: '12. Official sources' }, ] as const; function TocList({ onNavigate }: { onNavigate?: () => void }) { @@ -55,15 +62,15 @@ export default function DocumentationPage() { O
-
Ozon Tech Sorter
-
Цифровой двойник линии сортировки
+
OWL PRIME
+
Ozon Tech Track 3 — документация
- Раздел - Документация + Ветка + {PRODUCTION_STATUS.canonicalBranch}
@@ -75,7 +82,7 @@ export default function DocumentationPage() {

Документация

-

Текущее состояние веб-симуляции и инженерный статус

+

Финальный инженерный статус решения OWL PRIME

@@ -99,144 +106,158 @@ export default function DocumentationPage() {
Текущий статус
-
{PRODUCTION_STATUS.acquisitionPackStatus}
+
{PRODUCTION_STATUS.projectStatus}

- Web twin: {PRODUCTION_STATUS.webTwinStatus}. Unit tests {PRODUCTION_STATUS.unitTests}, - build {PRODUCTION_STATUS.productionBuild}. Contact physics:{' '} - {PRODUCTION_STATUS.contactPhysics}. Official compliance:{' '} - {PRODUCTION_STATUS.officialCompliance}. + Web: {PRODUCTION_STATUS.webTwinStatus}. CV: {PRODUCTION_STATUS.cvStatus} (live + integrated: {PRODUCTION_STATUS.cvLiveIntegrated ? 'YES' : 'NO'}). Unit tests{' '} + {PRODUCTION_STATUS.unitTests}, CV tests {PRODUCTION_STATUS.cvTests}, build{' '} + {PRODUCTION_STATUS.productionBuild}. Domain: {PRODUCTION_STATUS.productionUrl}. Contact + physics: {PRODUCTION_STATUS.contactPhysics}.

{SECTIONS[0].title}

- Цифровой двойник линии сортировки Ozon Tech: конвейер, камера/классификация, физическая - маршрутизация и три приёмные категории (B / C / D). Страница / — рабочая - веб-симуляция; эта страница — каноническая сводка текущего состояния. Реальный - CV-прототип (RealSense D415) лежит в cv/ и не подключён к live-сайту. + OWL PRIME — инженерный прототип предварительной сортировки товаров для Ozon Tech Track + 3: веб-цифровой двойник, реальный CV-прототип, физический экспериментальный стенд, + авторский CAD и единые правила классификации B/C/D.

+
    + {SOLUTION_COMPONENTS.map((item) => ( +
  • {item}
  • + ))} +
-
+

{SECTIONS[1].title}

-

{RUNTIME_FLOW.join(' → ')}

-
- -
-

{SECTIONS[2].title}

+

+ Production: {PRODUCTION_STATUS.productionUrl} +

  • - Рабочая зона (чертёж): {CONFIRMED_LAYOUT.workspaceMm.length} ×{' '} - {CONFIRMED_LAYOUT.workspaceMm.width} мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм / - высота верха {CONFIRMED_LAYOUT.conveyorHeightMm} мм. + / — непрерывная симуляция конвейера
  • -
  • Модули CAD: {CONFIRMED_LAYOUT.modules.join(', ')}.
  • +
  • + /documentation — эта инженерная документация +
  • +
+

+ На desktop — интерактивный WebGL 3D twin: CAD-конвейер, цифровое измерение, + классификация B/C/D, CAD-дивертеры и физика Rapier. Mobile: {MOBILE_BEHAVIOR.mobile}. +

+
    +
  • + Рабочая зона: {CONFIRMED_LAYOUT.workspaceMm.length} × {CONFIRMED_LAYOUT.workspaceMm.width}{' '} + мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм / высота{' '} + {CONFIRMED_LAYOUT.conveyorHeightMm} мм. +
  • +
  • Модули: {CONFIRMED_LAYOUT.modules.join(', ')}.
  • Приёмники: {CONFIRMED_LAYOUT.receivers.join(', ')}.
  • +
  • Runtime flow: {RUNTIME_FLOW.join(' → ')}
  • +
+
+ +
+

{SECTIONS[2].title}

+

+ Path: {CV_PROTOTYPE.path}. Status:{' '} + {CV_PROTOTYPE.status}. Hardware: {CV_PROTOTYPE.hardware}. Software:{' '} + {CV_PROTOTYPE.software}. +

+

Pipeline: {CV_PROTOTYPE.pipeline.join(' → ')}

+
    + {CV_PROTOTYPE.notes.map((n) => ( +
  • {n}
  • + ))}

{SECTIONS[3].title}

- Status: {CLASSIFIER_BOUNDS.status} + Status: {CLASSIFIER_BOUNDS.status}. Source:{' '} + {CLASSIFIER_BOUNDS.officialSourceReference}.

  • Min: {CLASSIFIER_BOUNDS.display.min}
  • Max: {CLASSIFIER_BOUNDS.display.max}
  • Roundness: {CLASSIFIER_BOUNDS.display.roundness}
  • +
  • + Boundary: {CLASSIFIER_BOUNDS.display.exactBoundary} +
  • Order: {CLASSIFIER_BOUNDS.checkOrder}
  • +
  • Oversized circular product still goes to C (C priority).

- OFFICIAL_SOURCE_REFERENCE: {CLASSIFIER_BOUNDS.officialSourceReference} - {CLASSIFIER_BOUNDS.officialSourceParsedThisPass - ? '' - : ' — PDF not re-parsed in this documentation pass; bounds match current code/tests.'} + Web (src/domain/classifier.ts) and CV (cv/classify.py) both use + strict K > 0.8.

-
+

{SECTIONS[4].title}

-
- - - - - - - - - - - {ROUTE_MAPPING.map((row) => ( - - - - - - - ))} - -
CategoryPhysical routeCAD diverterAngle
{row.category} - {row.physicalRoute} - {row.activeDiverter}{row.signedAngleDeg}°
-
+

+ Status: {PHYSICAL_STAND.status}. {PHYSICAL_STAND.purpose} +

+
    + {PHYSICAL_STAND.elements.map((el) => ( +
  • {el}
  • + ))} +
-
+

{SECTIONS[5].title}

-

{DIVERTER_KINEMATICS.phases.join(' → ')}

-
    -
  • Rotation duration: {DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s
  • -
  • Safety margin: {DIVERTER_KINEMATICS.openingSafetyMarginSec.toFixed(2)} s
  • -
  • Contact plane S: {DIVERTER_KINEMATICS.contactPlaneS}
  • -
  • Clear plane S: {DIVERTER_KINEMATICS.clearPlaneS.toFixed(4)}
  • -
  • Route command bound to productId; one active product at a time
  • -
  • Close after rear-clear of clear plane
  • -
  • - Invented sorter drive mesh active:{' '} - {DIVERTER_KINEMATICS.generatedMechanismActive ? 'YES' : 'NO'} -
  • -
-
- -
-

{SECTIONS[6].title}

  • - Author: {CAD_PROVENANCE.authorFcstd} -
    - SHA-256: {CAD_PROVENANCE.authorSha256} + Author CAD: {CAD_PROVENANCE.authorFcstd}
  • - Runtime: {CAD_PROVENANCE.runtimeGlb} -
    - SHA-256: {CAD_PROVENANCE.runtimeSha256} + Runtime conveyor: {CAD_PROVENANCE.runtimeGlb}
  • -
  • Author servo/holder nodes used in sorter module only.
  • -
  • Invented sorter drive mesh inactive; author CAD diverters only.
  • -
  • Horn / transmission in GLB: {CAD_PROVENANCE.hornTransmissionInGlb}.
  • +
  • Product STL models in public/models/
  • +
  • Same B/C/D classifier rules as CV
  • +
  • Rapier physics + CAD diverters (−45° / +45°)
  • +
  • Synchronized digital routing into B/C/D receivers
-
- -
-

{SECTIONS[7].title}

-

IMPLEMENTED

+

Physics

+

IMPLEMENTED

    {PHYSICS_STATUS.implemented.map((x) => (
  • {x}
  • ))}
-

NOT FULLY VALIDATED

+

NOT FULLY VALIDATED

    {PHYSICS_STATUS.notFullyValidated.map((x) => (
  • {x}
  • ))}
-

PLANNED

+

+ Diverter phases: {DIVERTER_KINEMATICS.phases.join(' → ')} ( + {DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s rotation). +

+
+ +
+

{SECTIONS[6].title}

+

REAL PATH

+

{ARCHITECTURE.realPath.join(' → ')}

+

DIGITAL PATH

+

{ARCHITECTURE.digitalPath.join(' → ')}

+
+ +
+

{SECTIONS[7].title}

+

+ Canonical branch: {PRODUCTION_STATUS.canonicalBranch}. Other branches are + historical and not required to run the solution. +

    - {PHYSICS_STATUS.planned.map((x) => ( -
  • {x}
  • + {REPOSITORY_LAYOUT.map((line) => ( +
  • {line}
  • ))}
@@ -265,8 +286,45 @@ export default function DocumentationPage() {
-
+

{SECTIONS[9].title}

+
    + {CURRENT_LIMITATIONS.map((b) => ( +
  • {b}
  • + ))} +
+
+ +
+

{SECTIONS[10].title}

+
+ + + + + + + + + + + {ROUTE_MAPPING.map((row) => ( + + + + + + + ))} + +
CategoryPhysical routeCAD diverterAngle
{row.category} + {row.physicalRoute} + {row.activeDiverter}{row.signedAngleDeg}°
+
+
+ +
+

{SECTIONS[11].title}

@@ -294,15 +352,6 @@ export default function DocumentationPage() {
- -
-

{SECTIONS[10].title}

-
    - {CURRENT_LIMITATIONS.map((b) => ( -
  • {b}
  • - ))} -
-
diff --git a/src/styles.css b/src/styles.css index 7968c46..3ecb81c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -3312,17 +3312,24 @@ button:disabled { } /* ========================================================= - Documentation page + Documentation page — light readable surface (Ozon blue) ========================================================= */ .docs-page { + --docs-bg: #f5f8fc; + --docs-panel: #ffffff; + --docs-text: #101828; + --docs-muted: #344054; + --docs-heading: #0b1f3a; + --docs-blue: #005bff; + --docs-magenta: #f91155; + --docs-line: #d0d5dd; + --docs-status-bg: #e8f1ff; min-height: 100dvh; max-width: 100vw; overflow-x: hidden; - background: - radial-gradient(circle at 12% 0%, rgba(56, 189, 248, 0.12), transparent 32%), - linear-gradient(180deg, #07111d 0%, #050910 100%); - color: var(--text); + background: var(--docs-bg); + color: var(--docs-text); } .docs-header { @@ -3336,9 +3343,8 @@ button:disabled { gap: 12px 20px; min-height: 56px; padding: 14px 20px; - border-bottom: 1px solid var(--line); - background: rgba(5, 9, 16, 0.92); - backdrop-filter: blur(12px); + border-bottom: 1px solid var(--docs-line); + background: #ffffff; } .docs-brand { @@ -3354,9 +3360,9 @@ button:disabled { width: 36px; height: 36px; border-radius: 10px; - background: rgba(56, 189, 248, 0.15); - border: 1px solid rgba(56, 189, 248, 0.35); - color: var(--cyan); + background: rgba(0, 91, 255, 0.1); + border: 1px solid rgba(0, 91, 255, 0.35); + color: var(--docs-blue); font-weight: 800; flex: 0 0 auto; } @@ -3364,11 +3370,13 @@ button:disabled { .docs-title { font-size: 1.25rem; line-height: 1.2; + color: var(--docs-heading); } .docs-subtitle { - color: var(--muted); + color: var(--docs-muted); font-size: 0.85rem; + opacity: 1; } .docs-layout { @@ -3385,9 +3393,9 @@ button:disabled { top: 84px; align-self: start; padding: 12px; - border: 1px solid var(--line); + border: 1px solid var(--docs-line); border-radius: 12px; - background: rgba(10, 19, 31, 0.7); + background: var(--docs-panel); font-size: 0.85rem; } @@ -3405,16 +3413,17 @@ button:disabled { .docs-toc-desktop a, .docs-toc-mobile a { - color: var(--muted); + color: var(--docs-muted); text-decoration: none; display: inline-flex; min-height: 36px; align-items: center; + opacity: 1; } .docs-toc-desktop a:hover, .docs-toc-mobile a:hover { - color: var(--cyan); + color: var(--docs-blue); } .docs-main { @@ -3432,31 +3441,39 @@ button:disabled { } .docs-status-banner { - border: 1px solid rgba(74, 222, 128, 0.35); - background: rgba(20, 48, 32, 0.45); + border: 1px solid rgba(0, 91, 255, 0.28); + background: var(--docs-status-bg); } .docs-status-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--muted); + color: var(--docs-blue); + opacity: 1; } .docs-status-value { margin-top: 6px; font-size: 1.15rem; font-weight: 700; - color: var(--green); + color: var(--docs-heading); overflow-wrap: anywhere; word-break: break-word; } .docs-status-note { margin-top: 10px; - color: var(--muted); + color: var(--docs-muted); font-size: 0.92rem; line-height: 1.5; + opacity: 1; +} + +.docs-section h2, +.docs-section h3, +.docs-section h4 { + color: var(--docs-heading); } .docs-section h2 { @@ -3464,9 +3481,19 @@ button:disabled { margin-bottom: 10px; } +.docs-section a { + color: var(--docs-blue); +} + +.docs-section strong { + color: var(--docs-magenta); + font-weight: 700; +} + .docs-section p, .docs-section li { - color: #c9d8e8; + color: var(--docs-text); + opacity: 1; font-size: 0.98rem; line-height: 1.5; } @@ -3480,19 +3507,22 @@ button:disabled { .docs-section code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.88em; - color: var(--cyan-hover); + color: var(--docs-blue); + background: rgba(0, 91, 255, 0.08); + padding: 0.1em 0.35em; + border-radius: 4px; overflow-wrap: anywhere; word-break: break-word; } .docs-conflict { margin-top: 12px; - border: 1px solid rgba(245, 158, 11, 0.4); - background: rgba(48, 32, 10, 0.4); + border: 1px solid rgba(249, 17, 85, 0.35); + background: #fff5f8; } .docs-blocker-list li { - color: #ffd0d5; + color: var(--docs-text); } .docs-table-scroll { @@ -3507,20 +3537,24 @@ button:disabled { min-width: 420px; border-collapse: collapse; font-size: 0.92rem; + background: var(--docs-panel); } .docs-table th, .docs-table td { padding: 8px 10px; - border: 1px solid var(--line); + border: 1px solid var(--docs-line); text-align: left; vertical-align: top; - white-space: nowrap; + white-space: normal; + color: var(--docs-text); + overflow-wrap: anywhere; + word-break: break-word; } .docs-table th { - background: rgba(12, 23, 37, 0.9); - color: var(--muted); + background: #eef4ff; + color: var(--docs-heading); font-weight: 600; } @@ -3689,8 +3723,7 @@ button:disabled { background: #070d16; } - .main-page--product .product-topbar, - .docs-page .product-topbar { + .main-page--product .product-topbar { position: relative; z-index: 30; flex: 0 0 auto; @@ -3706,6 +3739,41 @@ button:disabled { backdrop-filter: blur(14px); } + .docs-page .product-topbar { + position: relative; + z-index: 30; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + height: 68px; + max-height: 72px; + padding: 0 24px; + border-bottom: 1px solid var(--docs-line); + background: #ffffff; + backdrop-filter: none; + } + + .docs-page .product-brand-mark { + border-color: rgba(0, 91, 255, 0.35); + background: rgba(0, 91, 255, 0.1); + color: var(--docs-blue); + } + + .docs-page .product-brand-title { + color: var(--docs-heading); + } + + .docs-page .product-brand-sub, + .docs-page .product-tele-label { + color: var(--docs-muted); + } + + .docs-page .product-tele-value { + color: var(--docs-text); + } + .product-brand { display: flex; align-items: center; @@ -3742,8 +3810,7 @@ button:disabled { color: #8aa0b8; } - .main-page--product .app-nav-solid, - .docs-page .product-topbar .app-nav-solid { + .main-page--product .app-nav-solid { flex: 0 0 auto; display: inline-flex; gap: 4px; @@ -3753,14 +3820,38 @@ button:disabled { background: rgba(8, 16, 26, 0.7); } - .main-page--product .app-nav-link, - .docs-page .product-topbar .app-nav-link { + .docs-page .product-topbar .app-nav-solid, + .docs-page .docs-header .app-nav-solid { + flex: 0 0 auto; + display: inline-flex; + gap: 4px; + padding: 3px; + border-radius: 10px; + border: 1px solid var(--docs-line); + background: #f2f6ff; + } + + .main-page--product .app-nav-link { min-height: 34px; padding: 0 16px; font-size: 13px; border-radius: 8px; } + .docs-page .product-topbar .app-nav-link, + .docs-page .docs-header .app-nav-link { + min-height: 34px; + padding: 0 16px; + font-size: 13px; + border-radius: 8px; + color: var(--docs-heading); + } + + .docs-page .app-nav-link.active { + background: var(--docs-blue); + color: #ffffff; + } + .product-telemetry { display: flex; align-items: stretch; @@ -3940,11 +4031,9 @@ button:disabled { white-space: normal; } - /* Documentation desktop grid aligned to product chrome */ + /* Documentation desktop grid — keep light readable surface */ .docs-page { - background: - radial-gradient(circle at 10% 0%, rgba(56, 189, 248, 0.1), transparent 34%), - linear-gradient(180deg, #07111d 0%, #050910 100%); + background: var(--docs-bg); } .docs-layout { @@ -3960,6 +4049,8 @@ button:disabled { top: 88px; padding: 14px 12px; font-size: 0.84rem; + background: var(--docs-panel); + border-color: var(--docs-line); } .docs-main {