601f584181
Панель за фазы A–C разрослась до ~14 всегда-раскрытых секций (длинный скролл, тяжело ориентироваться). Сделал её удобнее: - _stereoInitPanel() (вызов из _openStereo, идемпотентно) оборачивает контролы каждой секции в .st-acc-body; заголовки .gp-section-title → кликабельные .st-acc-hdr с шевроном; состояние секций в localStorage. - Тройку фигурных секций (Многогранники/Правильные/Тела вращения) слил в одну «Фигуры» (под-метки .st-sublabel). По умолчанию открыты «Фигуры» и «Параметры», остальное свёрнуто. - Кнопки «Развернуть всё / Свернуть всё» (stereoAccAll), клавиатура (Enter/Space на заголовке), role=button/tabindex. - Только раскладка: ни один контрол/обработчик не изменён (узлы лишь перемещены в тело секции). Затронуты stereo.js + lab.css. Верификация: node --check OK; headless DOM-смоук (мини-DOM + реальный stereo.js в vm) 22/22: 12 сворачиваемых секций, тройка фигур слита (2 под-метки внутри «Фигуры»), пары заголовок→тело, дефолт-открытие, тоггл+персист, развернуть/свернуть всё, идемпотентная переинициализация, ни одна строка контролов не потеряна. Эмодзи/eval/new Function — 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
134 lines
18 KiB
Markdown
134 lines
18 KiB
Markdown
# План улучшения симуляции «Стереометрия 3D»
|
||
|
||
Файлы: `frontend/js/labs/stereo.js` (StereoSim, ~3720 строк), панель в `frontend/lab.html` (`#sim-stereo`), роутинг `frontend/js/labs/lab-init.js`.
|
||
|
||
Статус-легенда: [ ] не начато · [~] в работе · [x] готово
|
||
|
||
---
|
||
|
||
## Фаза 0 — Производительность и гигиена (быстрый эффект, низкий риск) — ГОТОВО
|
||
|
||
- [x] 0.1 Останавливать loop при переключении симуляций.
|
||
Добавлен `_pauseAllSims()` в `lab-init.js`, вызывается в начале `openSim()` — все rAF-симы (включая стерео) паузятся при переходе. Раньше предыдущий сим рендерил невидимый canvas вечно.
|
||
- [x] 0.2 Pointer/touch-слушатели перенесены с `window` на `renderer.domElement`, с pointer-capture для драга вне холста; все слушатели трекаются и снимаются в `dispose()`. Canvas получил `tabindex`/`role`/`aria-label`.
|
||
- [x] 0.3 Render-on-demand: `_invalidate()` + dirty-флаг `_needsRender`; loop засыпает (`_rafId=null`), просыпается по взаимодействию/изменению сцены. Хук в `_clearGroup()` покрывает все rebuild/clear; защита от двойного rAF.
|
||
- [x] 0.4 Обработка `webglcontextlost`/`webglcontextrestored` (пересборка сцены); метод `dispose()` (renderer, ResizeObserver, слушатели, текстуры). Бонус: `_clearGroup` стал рекурсивным — устранена утечка вложенных групп (измерения и т.п.).
|
||
|
||
## Фаза 1 — Камера и навигация — ГОТОВО
|
||
|
||
- [x] 1.1 Инерция орбиты (плавное затухание после отпускания) + панорамирование: правая/средняя кнопка или Shift+ЛКМ на десктопе, два пальца на тач. Орбита вокруг сдвигаемого таргета (`_panOffset`). _(zoom-to-cursor отложен — pan покрывает рецентрирование; перенесён в бэклог Фазы 2.)_
|
||
- [x] 1.2 Overlay-тулбар в правом верхнем углу viewport: сброс вида + пресеты ракурса (Изо / Спереди / Сбоку / Сверху). Пресет «держит» вид (спин выключается).
|
||
- [x] 1.3 Тумблер авто-вращения (с реальным засыпанием loop при выключении), fullscreen (по `.graph-canvas-outer`), снимок PNG (`preserveDrawingBuffer` + синхронный рендер → download). a11y: `aria-pressed`/`aria-label` на кнопках.
|
||
|
||
## Фаза 2 — Геометрия и пикинг — ГОТОВО
|
||
|
||
- [x] 2.1 Аналитические сечения кривых тел `_sliceCurvedByNormal()`: шар → точная окружность (плоскость∩сфера), цилиндр/конус/усеч.конус → гладкая кривая через точное решение y(θ) для образующей `r(y)=r0+k·y`. Старый пороговый сэмплинг оставлен как fallback (почти вертикальные плоскости). Проверено численно (диапазон y цилиндра и радиус окружности шара совпадают с формулами).
|
||
- [x] 2.2 Общий хелпер `_edgePickNDC()` (расстояние точка-отрезок в NDC); `_pickNearestEdge` и `_pickNearestEdgeIdx` переведены с «по середине ребра» на корректный пикинг по всей длине.
|
||
- [x] 2.3 HiDPI-метки: `_makeTextSprite` рендерит на canvas с учётом DPR, корректный аспект по ширине текста, тёмная обводка для читаемости, анизотропная фильтрация. Тип сечения для кривых = «окружность»/«эллипс», вершинные маркеры не плодятся (cap ≤12 точек).
|
||
|
||
Бэклог: zoom-to-cursor (перенесён из 1.1); SDF-шрифт и пул текстур (текущая резкость достаточна).
|
||
|
||
## Фаза 3 — Педагогика сечений — ГОТОВО (частично, см. бэклог)
|
||
|
||
- [x] 3.1 Подписи вершин сечения буквами (K, L, M…) в наклонном/произвольном сечении и сечении-по-3-точкам (для настоящих многоугольников ≤12 вершин). Пошаговый режим сечения-по-3-точкам уже был. _(Полное «построение по следам» — в бэклоге, крупная отдельная фича.)_
|
||
- [x] 3.2 Точки на произвольной точке грани: `_raycastFace()` — в режиме «точки» клик по грани (не у ребра/вершины) ставит точку на поверхности. Через произвольное сечение (3 кастомные точки) это даёт плоскость через внутренние точки граней.
|
||
- [x] 3.3 Live-readout overlay (`#stereo-readout`, низ-слева): тип сечения, площадь S, периметр P, последнее измерение. Обновляется через `info().readout` при любом изменении сечения/измерения.
|
||
|
||
Бэклог: полное «построение сечения по следам» (продление рёбер, след на грани); readout углов (двугранный/линия-плоскость) — сейчас угол только в 3D-метке.
|
||
|
||
## Фаза 4 — Визуал — ГОТОВО (см. бэклог)
|
||
|
||
- [x] 4.1 Свечение вершин (мягкий additive-ореол без текстур, безопасно с `_clearGroup`); рёбра контрастнее (opacity 0.9, `renderOrder` поверх полупрозрачного тела), вершины поверх рёбер. _(Подсветка грани по ховеру отложена — текущий centroid-пикинг граней грубоват для плавного hover.)_
|
||
- [x] 4.2 Подписи осей X/Y/Z, цвета совпадают с AxesHelper (X крас., Y зел., Z син.).
|
||
|
||
Бэклог: подсветка грани по ховеру (нужен точный raycast по логическим граням); градиентный/бумажный фон (учесть захват в скриншоте).
|
||
|
||
## Фаза 5 — Интеграция и архитектура — ГОТОВО (без дробления файла, по решению пользователя)
|
||
|
||
- [~] 5.1 Дробление `stereo.js` на модули — **отложено по решению пользователя** (риск регрессий, не видно пользователю). Остаётся в бэклоге.
|
||
- [x] 5.2 Deep-link фигуры из учебников без изменения общего hash-роутера: `openSim('stereo:<figure>')` и `/lab?stereofig=<figure>#sim/stereo`. `_openStereo(figureType)` применяет фигуру и подсвечивает кнопку. Допустимые: cube, parallelepiped, prism, pyramid, truncpyramid, tetrahedron, octahedron, icosahedron, dodecahedron, cylinder, cone, trunccone, sphere.
|
||
- [x] 5.3 a11y: клавиатурная навигация по сфокусированному canvas — стрелки (орбита), +/− (зум), R/Home (сброс); `tabindex`/`role`/`aria-label` на canvas (Фаза 0), `aria-pressed`/`aria-label` на кнопках вида (Фаза 1), `aria-live` на readout.
|
||
|
||
Бэклог Фазы 5: модульное дробление файла; deep-link конкретного сечения/инструмента (не только фигуры).
|
||
|
||
## Фаза 6 — Построение сечения «по следам» (метод следов) — ГОТОВО (путь b)
|
||
|
||
Реализован гибрид: финальный полигон считается надёжно (Фаза 2), а след и вспомогательные точки выводятся аналитически — без риска несходимости конструктивного алгоритма. Scope: тела с основанием (куб, параллелепипед, призма, пирамида, усеч. пирамида, тетраэдр).
|
||
|
||
- [x] 6.1 `_hasBase()` + `_traceLine(data)` — след = π ∩ плоскость основания (y=0): `n.x·x + n.z·z + D = 0`; возвращает `{p0, dir}` или null (плоскость параллельна основанию). Проверено численно (точка следа на плоскости, остаток 0).
|
||
- [x] 6.2 `_auxiliaryPoints(polygon)` — продление боковых сторон сечения до y=0; точка пересечения лежит ровно на следе (численно dist=0). Сортировка по «дальности продления», берём 2 ближайшие.
|
||
- [x] 6.3 Настоящий пошаговый `_drawSection3PStep` (заменил бутафорию): 6 подписанных шагов — 3 точки → стороны в одной грани (`_sameFace`) → след → вспом. точки T₁,T₂ → вершины+замыкание → итог. В step-режиме финальное сечение скрыто до шага 5 (`showFull`), пиковые линии тоже скрыты. Подписи шагов в `#sect3p-hint` через `_stepCaption`. Для тел без основания — деградация к простым шагам с пояснением.
|
||
- bump stereo.js?v=9
|
||
|
||
Бэклог Фазы 6: «честный» конструктивный алгоритм (шаг по граням через след для поиска каждой новой вершины); анимация перехода между шагами; ветка для плоскости, параллельной основанию.
|
||
|
||
---
|
||
|
||
## Раунд «Конструктор» (2026-06-17) — упор на ученика-самоучку (песочница)
|
||
|
||
Цель: превратить отличный **визуализатор** в полноценный **конструктор** для самостоятельных
|
||
построений. Приоритеты, выбранные пользователем: **Фаза A (конструкторное ядро)** и
|
||
**Фаза C (сечения+)**. A — фундамент C (сечение через прямую+точку, параллельно прямой/плоскости
|
||
опираются на объекты-прямые/плоскости).
|
||
|
||
### Фаза A — Конструкторное ядро
|
||
|
||
Прямые и плоскости как объекты первого класса + пересечения + параллели/перпендикуляры +
|
||
общий undo/redo + дерево именованных объектов.
|
||
|
||
- [x] A1 — **Объектная модель + базовые построения.** `_lines[]` (имена a,b,c…), `_planes[]`
|
||
(имена α,β,γ…), группа `_constructGroup`, сериализуемое хранение `{x,y,z}`. Инструменты
|
||
«Прямая по 2 точкам» и «Плоскость по 3 точкам» (пикинг вершин/точек). Плоскость рисует
|
||
полупрозрачный квад + пунктирную рамку + **сечение тела этой плоскостью** (через `_sliceByPlane`,
|
||
делает плоскость осмысленной сразу). Панель «Построения», список объектов с уравнением плоскости.
|
||
- [x] A2 — **Пересечения** (прямая∩плоскость → точка `_cpoints` имена M,N,K…; плоскость∩плоскость
|
||
→ прямая; прямая∩прямая → точка или «скрещиваются») — выбор 2 объектов в дереве (`setIntersectMode`/
|
||
`pickConstructObject`). **Интерактивное дерево**: видимость (глаз)/удаление (×) по объекту, выбор
|
||
для пересечения. Точки-пересечения пикабельны → по ним строятся новые прямые/плоскости.
|
||
- [x] A3 — **Параллели/перпендикуляры** через точку (`setRelMode`/`_onRelClick`): `lpar` прямая ∥
|
||
прямой; `lperp` прямая ⟂ плоскости; `ppar` плоскость ∥ плоскости; `pperp` плоскость ⟂ прямой
|
||
(= «плоскость по точке и нормали» через `_createPlaneFromPointNormal` — мост к Фазе C). Поток:
|
||
кнопка op → выбор опоры в дереве → клик точки. **Общий undo/redo** конструкторного слоя (JSON-
|
||
снапшот `_undoStack`/`_redoStack`, кап 60; хуки в create/remove/clear; Ctrl+Z / Ctrl+Shift+Z /
|
||
Ctrl+Y + кнопки «Отменить»/«Вернуть»). Видимость — не шаг истории (намеренно).
|
||
|
||
### UX панели управления (2026-06-17)
|
||
|
||
- [x] Панель `.stereo-panel` разрослась за фазы A–C до ~14 всегда-раскрытых секций (длинный скролл) →
|
||
**сворачиваемый аккордеон**: `_stereoInitPanel()` (вызывается из `_openStereo`, идемпотентно) оборачивает
|
||
контролы каждой секции в `.st-acc-body`, заголовки `.gp-section-title` → кликабельные `.st-acc-hdr`
|
||
с шевроном; состояние каждой секции в localStorage (`stereo-acc-<имя>`). Тройка фигурных секций
|
||
(Многогранники/Правильные/Тела вращения) слита в одну «Фигуры» (под-метки `.st-sublabel`). По
|
||
умолчанию открыты «Фигуры» и «Параметры». Кнопки «Развернуть всё / Свернуть всё» (`stereoAccAll`).
|
||
Сами контролы/обработчики не тронуты — только раскладка. Только `stereo.js` + `lab.css`.
|
||
|
||
### Фаза B — Умные точки
|
||
|
||
- [x] B1 — **Деление отрезка m:n** (`setDivideMode`/`setDivideRatio`): задаёшь m,n → кликаешь 2 точки A,B
|
||
→ точка делит AB как AM:MB = m:n (`t = m/(m+n)`), создаётся как `_cpoints` (M,N,K…).
|
||
- [x] B2 — **Точка по координатам** (`addPointAt(x,y,z)`): поля x/y/z + кнопка → точка-построение.
|
||
- [x] B3 — **Перетаскивание точек** (`setDragPointMode`): pointerdown по точке-построению (приоритет над
|
||
орбитой) → drag в плоскости, обращённой к камере (нормаль = направление камеры, фикс. на старте);
|
||
`_beginCPointDrag`/`_dragCPointWithRay`/`_rayPlaneHit`/`_endCPointDrag`; снапшот истории на старте
|
||
(undo откатывает весь drag). Точки-пересечения/деления непараметрические (downstream-объекты
|
||
копируют позицию при создании и за перетаскиванием НЕ следуют — параметрический граф = бэклог).
|
||
|
||
### Фаза C — Сечения+
|
||
|
||
- [x] C1 — Сечение **плоскостью-объектом** (из Фазы A): клик по плоскости в дереве (нормальный режим)
|
||
→ `setSectionPlane` показывает её заливкой + подписи вершин K,L,M… + площадь/периметр в readout
|
||
(`_activeSectionPolygon`, `getReadout`). Удаление/очистка/смена фигуры сбрасывают сечение.
|
||
- [x] C2 — **Покрыто Фазой A** (отдельный код не нужен): сечение через прямую+точку = плоскость по
|
||
3 точкам (2 с прямой + 1); сечение ∥ плоскости через точку = rel-операция `ppar` → затем клик
|
||
как сечение. Дополнительный UI признан избыточным.
|
||
- [x] C3 — **«Натуральная величина» сечения** (`getTrueShape`): разворот многоугольника в его
|
||
плоскость (ортонормированный базис от нормали) с сохранением истинных длин → 2D-мини-панель
|
||
(SVG, штриховка `<pattern>`, подписи вершин, длины сторон, S/P). Проверено сохранение длин и
|
||
площади для прямого и наклонного сечений.
|
||
- [ ] C4 — Честный конструктивный алгоритм следов с анимацией (из бэклога Ф6) — **отложено**
|
||
(крупная отдельная фича; текущий гибрид Ф6 + сечения-объекты Фазы A/C закрывают практику).
|
||
|
||
---
|
||
|
||
История: создан 2026-05-30. Фаза 6 добавлена 2026-05-30. Раунд «Конструктор» (Фазы A,C) — 2026-06-17.
|