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}
-
-
-
-
- | Category |
- Physical route |
- CAD diverter |
- Angle |
-
-
-
- {ROUTE_MAPPING.map((row) => (
-
- | {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}
+
+
+
+
+ | Category |
+ Physical route |
+ CAD diverter |
+ Angle |
+
+
+
+ {ROUTE_MAPPING.map((row) => (
+
+ | {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 {