87 lines
12 KiB
Markdown
87 lines
12 KiB
Markdown
# BQ-System — правила для Claude
|
||
|
||
## Поиск по коду
|
||
|
||
**ast-index — дефолт.** ВСЕГДА первым для «найти символ по имени / usages / callers / outline».
|
||
Grep/Read — только если ast-index вернул пустой результат.
|
||
|
||
**vex** — для поиска **по смыслу**, AST-паттернов, дубликатов, компактного тела символа:
|
||
`vex search "..." --semantic`, `vex similar`, `vex pattern`, `vex duplicates`, `vex show`.
|
||
Что и когда — подробно в `.claude/rules/search-tools.md`. (usages/callers по JS — только ast-index.)
|
||
|
||
```bash
|
||
# Найти класс/функцию/символ
|
||
ast-index class "ClassName"
|
||
ast-index symbol "functionName"
|
||
|
||
# Найти использования
|
||
ast-index usages "symbolName"
|
||
|
||
# Поиск по содержимому файла
|
||
ast-index search "keyword" --in-file "filename"
|
||
|
||
# Структура файла
|
||
ast-index outline "path/to/file.js"
|
||
|
||
# Универсальный поиск
|
||
ast-index search "query"
|
||
```
|
||
|
||
Grep использовать только для:
|
||
- Поиска строковых литералов (`"some text"`)
|
||
- Regex-паттернов
|
||
- Если ast-index вернул пустой результат
|
||
|
||
## Git — обновление репо
|
||
|
||
**После любых изменений** — коммит и push:
|
||
|
||
```bash
|
||
git add <изменённые файлы>
|
||
git commit -m "тип: описание"
|
||
git push origin master
|
||
```
|
||
|
||
- Коммитить только изменённые файлы (не `git add -A`)
|
||
- Сообщение коммита: `feat:` / `fix:` / `refactor:` / `style:` + краткое описание на русском или английском
|
||
- Push выполнять сразу после коммита
|
||
|
||
## Стек
|
||
|
||
- Node.js/Express backend, SQLite (better-sqlite3, sync)
|
||
- Frontend: vanilla JS, без бандлера
|
||
- ast-index проиндексирован: `ast-index rebuild` при добавлении новых файлов
|
||
|
||
## Feature: Конструктор симуляций (SimForge)
|
||
|
||
Движок авторинга интерактивных 2D-симуляций из JSON-спеки (данные, НЕ код). План: `plans/sim-builder/`.
|
||
|
||
### Phase 0 — Learnings
|
||
|
||
- **Спека = данные.** Любое числовое свойство объекта = число ИЛИ строка-выражение. Выражения шарятся между людьми → движок безопасный, ⛔ без `eval`/`new Function`.
|
||
- **`window.SimExpr`** (`frontend/js/labs/_sim_expr.js`): токенайзер → AST → evaluate. `compile(src)->{ast,fn,error}`; `fn(env)` НИКОГДА не бросает (NaN/∞/деление на 0 → 0). Whitelist: `+ - * / ^ %`, унарный `- + !`, сравнения `< <= > >= == !=`, логика `&& ||`, тернарник `?:`, функции `sin cos tan tg ctg cot asin..arctg sqrt abs exp ln log log2 log10 floor ceil round sign min max mod atan2 pow hypot`, константы `pi e tau`. Идентификаторы (вкл. точечные `obj.x`) — только из `env`. Парсер — расширение `y=f(x)` из `graph.js`; `-2^2 == 4` (парити). Также `evalSafe`, `compileValue`, `parse`, `tokenize`, `FUNCTIONS`, `CONSTANTS`.
|
||
- **`window.SimEngine.mount(host, spec)`** (`_sim_engine.js`) → `{ play, pause, reset, setParam, getParam, isRunning, destroy, el }`. Canvas (мир→экран, равные оси, Y вверх) + KaTeX-оверлей подписей (`katex.renderToString`, как graph.js) + слайдеры из `params[]`. Выражения компилируются 1 раз в mount; в rAF — только evaluate. `env = { t, <params>, w, h, xmin..ymax, <objId>.x, <objId>.y }`. Объекты: `point segment vector circle rect polyline path label`. **Формат спеки v1 — в шапке `_sim_engine.js`.**
|
||
- **`window.registerSpecSim(spec)`** (`_sim_adapter.js`): спека → манифест LabRegistry (ленивый хост `#sim-spec-host-<id>` в `#lab-sim`; `stop` прячет, `destroy` уничтожает). Так спек-сим открывается тем же путём, что рукописные ~40 (через `openSim` → реестр).
|
||
- Демо `customdemo` — `_sim_demo.js`, за флагом `?simdemo=1` / `?sim=customdemo` / `LAB_SHOW_SPEC_DEMO` / localStorage `lab-spec-demo=1` (ученикам не светится).
|
||
- Подключение: 3 каркасных `<script>` eager после `_graph_panel.js` в `lab.html`, демо — после `_register-all.js`. `_sim_deps.js` не трогать (каркас грузится до диспетчера).
|
||
|
||
### Phase 1 — Learnings
|
||
|
||
- **Новые типы объектов** (в `_sim_engine.js`, формат — в шапке файла):
|
||
- `plot` — график `f(var)` на canvas движка в мир-координатах (НЕ через `GraphPanelUI` — тот stacked time-series в фикс. оверлее, не `y=f(x)`). Поля: `expr`, `var` (деф.`x`), `range:[a,b]` (числа/выражения, деф. xmin..xmax), `samples` (клампится 2..2000, деф.200), `trace` (точка var=t пишется в trail; при trace без range статич. кривая не рисуется), `color/width`. Свободная переменная подставляется во временную копию env-ключа (восстанавливается после).
|
||
- `readout` — живой бейдж на DOM-оверлее (`_labelLayer`, как label). Поля: `expr`, `label`, `unit`, `precision` (0..8, деф.2), `x/y` (мир-коорд.; без них — авто-столбик верх-право, счётчик `_readoutSlot` сбрасывается на кадр). Ошибка — мягко через `SimExpr.evalSafe` (AST компилируется 1 раз в prepare), показывает «—».
|
||
- `vector` — новая форма `origin:[ox,oy]+dx/dy` (конец = origin + (dx,dy)); старая `x1/y1/x2/y2` сохранена; стрелка из Ф0.
|
||
- **Drag** (`point`/`circle` с `drag:{param,axis,min,max,paramY}`): pointer events на canvas (мышь+тач, `touchAction:none`); хит-тест в экранных px (допуск 16px, ближайшая ручка), приоритет ручек. `axis:'xy'` требует `paramY`. Курсор → мир через `_toWorld` (инверсия `_toPx`) → `_setParamClamped` (clamp по `drag.min/max` И по диапазону параметра из `_paramRange` — не полагаться на DOM-clamp слайдера). Слушатели снимаются в `destroy`. Drag только point/circle (вершины polyline/конец вектора — не реализовано).
|
||
- Тестировать движок headless: `vm.createContext` + ручной DOM/canvas-стаб (canvas-ctx через `Proxy` с noop). `_renderFrame` рано выходит при `_cw/_ch==0` — выставить вручную. `setParam`/drag используют `new Event('input')` (браузерно безопасно, в стабе нужен `Event`).
|
||
- ⛔ `lab.html`/`lab-glue.js` — зона параллельной сессии (Ф2 измерит. инструменты); Ф1 их НЕ трогала, работала только в `_sim_engine.js`/`_sim_demo.js`.
|
||
|
||
### Phase 2 — Learnings
|
||
|
||
- **Физический режим** (всё в `_sim_engine.js`, формат — в шапке файла): блок `physics:{ enabled, gravity:{x,y}, friction?, restitution?, dt?, walls?:[...], springs?:[...] }` + `body:{ mass, vx, vy, fixed }` на point/circle. gravity/friction/restitution/k/length/damping/mass/нач.позиция/vx/vy — число ИЛИ выражение от params (вычисляются на **reset**, не каждый кадр — для стабильности).
|
||
- **`window.SimPhysics`** — экспортированный интегратор (`step(state,dtFrame)`, `integrate`, `resolveCollisions`). Полу-неявный (симплектический) Эйлер `v+=a·dt; x+=v·dt` — та же математика, что `_fx_motion.spring`, обобщённая на N связанных тел. Фикс-шаг с накопителем (кламп dt 1/2000..1/30, кап подшагов 8, кламп скорости 1e4, вязкое трение `exp(-friction·dt)`) → энергия не «взрывается». Упругие столкновения круг-круг (импульс по нормали + позиционная коррекция по обратным массам) и круг-стена. Чистая функция над state, без DOM/eval — переиспользуемо headless. Отдельного файла `_sim_physics.js` НЕТ (нельзя подключить без правки lab.html — зона параллельной сессии); код внутри `_sim_engine.js`.
|
||
- **`_fx_motion` API не подходит** для спек-движка напрямую: `tween`/`springFactory` — rAF-замыкания, тянущие ОДНО значение к цели, не связанные тела с силами. Переиспользована только их интеграционная математика (формула спринга), а не сами функции.
|
||
- **env-поля тел**: `<id>.x/.y/.vx/.vy` берутся из СОСТОЯНИЯ интегратора и кладутся в `_buildEnv` ПЕРВЫМИ (до формульных центров) — это снимает forward-ref проблему однопроходного env для тел: формульный объект, ссылающийся на тело (`segment x2:'ball.x'`), видит актуальную позицию в том же кадре. point/circle с `body` рисуются из env-полей тела, а не из выражения x/y.
|
||
- **Drag тела**: тело (point/circle с `body`, не fixed) перетаскиваемо по умолчанию (без `drag`-конфига). Тащишь — `body.x/y = курсор`, тело временно `fixed` в `_stepPhysics`; отпускаешь — `body.vx/vy` из сглаженной оценки скорости курсора (кламп 40 м/с). Хит-тест тела — max(16px, экранный радиус). drag-ручки Ф1 и физ-тела сосуществуют.
|
||
- **Гочи**: (1) имя param **`e`** зарезервировано — это число Эйлера в SimExpr (parser проверяет CONSTANTS до env), выражение `e` даст 2.718, не значение param. Брать `el`/`elast`. (2) Радиус тела для коллизий: circle — мировой `r`; point — экранные px → мир через `_scale`, поэтому физика собирается в `reset()` ПОСЛЕ первого `_fit()`. (3) Слайдеры во время play меняют только env (readout/формулы), но НЕ силы/тела до reset (намеренно). На паузе при `t==0` — пересборка для предпросмотра старта.
|
||
- Headless-тест физики: виртуальные часы (`vclock`) синхронны с `performance.now()` и таймстампом rAF (иначе первый кадр получит огромный/отрицательный dt и ничего не сдвинется).
|