Repo had no license file and no description metadata. Adds MIT LICENSE,
a license badge and section in the README, and description/license/
repository fields in package.json.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(physics): add TMS Dynamics golden validation suite
Cross-engine validation of Ymir force models against the TMS Dynamics 3.0
golden dataset (vessel1 VLCC). Strategy is state-in -> force-out: the golden
motion trajectory is fed row by row into the matching Ymir force model and the
result is compared against the golden per-force decomposition. This is
integrator-independent, so it validates the force formula, not the time step.
- GoldenCsv.h: header-only CSV reader (column access by name)
- Vessel1Params.h: force configs mapped from vessel1.json, with the tonne->SI
x1000 unit conversion documented (verified empirically to ~3e-5 on Fr_z)
- TestGolden08Restoring.cpp: restoring (Fr_z, Mr_y) + heave damping (Fd_z)
- Downsampled golden fixtures under tests/fixtures/golden/08_restoring/
- ymir_golden_tests target wired into tests/CMakeLists.txt
Tolerances are set empirically and the residual causes are documented: the
restoring divergence (up to ~6.5% Fr_z / ~13.5% Mr_y as pitch grows) is the
off-diagonal hydrostatic coupling Ymir omits by design. Findings on squat
(diverges ~4 orders — model gap) and the body-frame-flow forces (need TMS
heading-convention mapping) are captured under docs/specs/features/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(tests): run C++ suite through the turbo monorepo
Make core/ a pnpm workspace package so `pnpm test` (turbo run test) runs the
C++ ctest suite alongside the JS vitest suites — one command for everything.
- Add "core" to pnpm-workspace.yaml so turbo discovers @ymir/core
- core/package.json: add a `build` script (cmake configure + compile)
- core/turbo.json: override `test` to dependsOn ["build"] instead of the root's
build:wasm, so native ctest does not require Emscripten; cache off (native)
- scripts/run-all-tests.sh: standalone runner (cpp/js/e2e flags) for running
the C++ suite without touching the wasm/emcc toolchain
@ymir/core#build -> @ymir/core#test: 270/270 tests pass via turbo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(physics): add wind golden validation + reconstruct flow convention
Extends the cross-engine golden suite to wind drag (scenario 02_wind) and adds
the earth->body flow conversion needed by flow-dependent forces.
- GoldenFrame.h: nautToBodyFrame mirroring NavalDomain — rotates the scenario
environment (speed + nautical direction) into the body frame using the golden
heading, so wind/current vectors can be reconstructed from controlDict + the
golden trajectory
- Vessel1Params.h: vessel1Wind() config with the full Cd(angle) table
- TestGolden02Wind.cpp: Fwd_x validated to ~4% across the trajectory (residual
is the rho_air constant: Ymir 1.225 vs reference ~1.27); Fwd_y/Mwd_z ~0 bow-on
- Fixtures for 02_wind
Documents the remaining divergences with measured numbers: current OBOKATA
disagrees in sign and magnitude (Fc_x -83kN vs golden +172kN — sectional-area
approximation + sign convention), squat off by ~4 orders. See
docs/specs/features/golden-validation/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(graphify): refresh knowledge graph after golden validation work
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(physics): align rho_air, squat sinkage, and damping norm to reference
Cross-engine alignment against the TMS Dynamics 3.0 golden dataset (see
docs/planning/force-model-alignment/). Verified against the reference engine
sources in dynamics/nashville/src/*Forces/.
- rho_air 1.225 -> 1.275 (PhysicalConstants.h): matches PhysicalProps.cpp of the
reference and closes the ~4% wind gap. Wind golden test tightened to <3%.
- SquatForces: fix double division by (rho*g) — the sinkage numerator is now
volumetricWeight/(rho*g*L^2) (single division, matching the reference), not
nabla_/(rho*g*L^2). Also use heave relative to equilibrium (z + draft) for the
depth/seabed clamp, since the reference q[2] is waterline-referenced. Squat
goes from ~4 orders of magnitude off to the correct order (partial: <=5% still
blocked by the artificial 07 scenario depth dynamics).
- DampingForces: the exponential-decay velocity norm now uses all 6 DOF (matching
dampingForces/DampingForces.cpp); the surge/sway sign was already correct given
Ymir's speedToWater convention.
Restoring needs no change: the reference restoring is also diagonal-only, so the
residual is a zero-crossing artifact, not a missing off-diagonal term.
Full C++ suite: 271/271 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(physics): add force-model-alignment spec/design/tasks + findings
TLC spec-driven planning for aligning all force models to the golden dataset:
- spec.md: 11 requirements (FMA-01..11), tolerances, decisions
- design.md: reference formula per model cross-checked with Ymir; root causes
- tasks.md: atomic tasks + execution status of the first pass
Records the key research corrections: restoring reference is diagonal (no
off-diagonal), squat is a double-division bug, current alignment conflicts with
self-propelled physics (reverted, needs speedToWater-convention resolution).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(graphify): refresh graph after force-model alignment
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engine ships as WASM-in-Node (covered by the WASM + JS workspaces job);
there is no native Windows target and CGAL/SUNDIALS aren't provisioned on
the Windows runner, so the job only ever failed at configure. Keep the
Linux + macOS native builds, which validate the C++ that compiles to WASM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- wasm-web: bump Node 20 -> 22 so the global WebSocket used by the /ws
integration test exists (it was ReferenceError on Node 20).
- build-and-test: point cmake at core/ (cmake -S core); there is no root
CMakeLists.txt, so Configure failed with 'does not contain CMakeLists.txt'
(pre-existing failure on main).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reset() now calls socket.clearSimId() so the next play() starts a fresh
simulation (CreateSimulation) instead of re-attaching to the old, reset
sim still alive within its TTL. Test asserts the post-reset play emits
CreateSimulation, not AttachSimulation.
Task: server-side-simulation review round 2, issue 001.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setup installs deps, builds @ymir/types and @ymir/wasm, and compiles the
Node WASM engine (best-effort if Emscripten is present). Run scripts: dev
(api+web via concurrently, wired to CONDUCTOR_PORT), api, web, test, wasm.
Concurrent-safe (per-worktree SQLite, per-workspace ports). Adds
concurrently devDependency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- manager: surface worker_thread errors/exit to clients and tear the sim
down (issue 001); cache last status/state and replay on attach so a
reconnecting client sees current state even when paused (issue 002)
- web socket: capped exponential reconnect backoff + bounded command queue
(issue 003); derive ws/wss URL from page origin (issue 004)
- runner: build replacement engine before freeing the old one (issue 005)
- remove dead VesselConfig message path across types/api/web (issue 006)
Tests added for each fix. Full monorepo suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fan-out and router roundtrip tests waited a fixed 180ms for the 20 Hz
loop to emit a State, which was flaky under CPU contention (full turbo
run). Poll for the State event with vi.waitFor instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single engine now lives on the server. Delete the simulation Web Worker,
the JS mock (mock-wasm), apps/web/public/wasm, and retire WorkerMessageDTO
and the wasm|mock SimulationEngine union from @ymir/types. Drop the
COOP/COEP headers and worker.format from vite config (were only for
in-browser WASM).
Task: task_08. Verified: web typecheck + unit suite green (83%), types
100%, api build clean, no dangling references. E2E against a live server
is validated separately (requires the running apps/api + web).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the local WASM Web Worker with the WebSocket client. Public store
API (play/pause/reset/loadScenario/applyEnvironment) is preserved; add
sendActuator + hasSession and a connection state (replacing the wasm/mock
engine badge). Update VesselPanel, AppShell, SimulationControls and their
tests; stub WebSocket in the jsdom test setup.
Task: task_07. Tests: full web suite green; store 95.8%, socket 93.9%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client transport to the server-side simulation: manages the WebSocket
lifecycle with auto-reconnect, (de)serializes ClientMessage/ServerMessage,
persists sim_id in localStorage, and re-attaches to the running sim on
reopen. Injectable WebSocket/storage for testing.
Task: task_06. Tests: 9 passing (FakeWebSocket), simulation-socket.ts
93.9% coverage; full web suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add @fastify/websocket (v8, Fastify 4) and a GET /ws endpoint. MessageRouter
validates inbound ClientMessages (TypeBox), dispatches to SimulationManager,
binds each connection to its sim_id on Create/Attach, and maps worker events
to ServerMessage frames. REST routes coexist unchanged.
Task: task_05. Tests: 31 passing (real Fastify + WebSocket roundtrip,
malformed-message handling, REST coexistence); branches 82.5%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owns every live simulation on the Fastify main thread: spawns one
worker_thread per sim (ADR-002), maps sim_id -> worker + connections,
fans events out to connected clients, enforces a max-sim limit, and
tears orphaned sims down after a configurable TTL (ADR-006). Make
SimulationRunner.dispose idempotent.
Task: task_04. Tests: 19 passing incl. a real worker_thread booting the
engine; coverage 93.6% stmts / 88% branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per ADR-005 tests run against the real WASM engine, so CI must build it
first. Add a wasm-web job: pinned emsdk (cached) -> pnpm build:wasm ->
build -> lint -> test. Wire turbo so 'test' dependsOn build:wasm and
^build:wasm, and point build:wasm outputs at the @ymir/wasm runtime.
Task: task_09. Verified turbo ordering locally (rm runtime -> turbo test
rebuilds it); full pipeline runs on GitHub Actions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server-side simulation runtime: SimulationRunner owns one real WASM
engine and its 20 Hz tick loop, applying commands (loadScenario,
play/pause/reset, actuators, environment) and producing snapshots.
attachRunnerToPort bridges it to a worker_thread port; the thread
entrypoint lives in simulation-worker.entry.ts. Adds vitest to apps/api.
Task: task_03. Tests: 12 passing (real engine + MessageChannel bridge),
coverage 98.6% stmts / 89.5% branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Package the physics engine as an internal @ymir/wasm workspace package
that runs in Node (worker_threads). Parametrize the Emscripten build
(YMIR_WASM_ENVIRONMENT=node, output dir override) so the same C++ core
builds for the server. Expose SimulationEngine interface +
createSimulationEngine() loader mirroring YmirBindings.cpp.
Task: task_01. Tests: 8 passing against the real WASM engine, 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add-only network contract for the server-side simulation: ClientMessage
(commands) and ServerMessage (events) TypeBox unions, reusing
SimulationStateDTO/VesselConfigDTO. Adds ScenarioDraftVesselDTO and
ScenarioInputDTO. WorkerMessageDTO left intact (removed in a later task).
Task: task_02. Tests: 45 passing, 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan to move physics from the browser (WASM Web Worker) to the server.
Client becomes a thin remote terminal; single engine; foundation for
persistence. Includes 6 ADRs and a 9-task breakdown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat blue water plane with a Gerstner-wave ShaderMaterial.
Wave components are derived from the environment store's active wave
keyframe (Hs, Tp, dirNaut): wavelength from the deep-water dispersion
relation, amplitude anchored to Hs/2, direction from the nautical
bearing. The detail patch follows the camera (snapped to its grid) with
a flat far-sea plane behind for the horizon; phase animates off the
render clock so the surface moves regardless of sim play/pause.
Add a SceneErrorBoundary around the GLTF-loading area mesh and each
vessel so a missing/4xx .glb (assets are gitignored) renders nothing
instead of throwing up the tree and blanking the whole Canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vessel mesh's bow points along local -Z, but the heading was applied with no
offset, so a heading of 0 (East/+X) left the hull beam-on to its motion ("anda de
lado"). Apply a -90° yaw offset so the bow aligns with the travel direction, to
both the model and the onboard cameras (their offsets are in the mesh's -Z-forward
frame). Verified in-browser: surging East now moves bow-first.
Also start the free-fly camera from a closer 3/4 aerial that frames the vessel.
Note: the bay terrain/islands/bridge are placed on consistent axes but the vessel
at sim origin sits in open water far from the coast — that's the geographic
mesh-origin↔sim-origin alignment, still pending calibration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Waves loaded into the timeline but no force model consumed them in the WASM
build, so vessels only translated (current → surge/sway/yaw). Wire the 6-DOF wave
excitation:
- core: WaveForces::setSpectrum() to swap the sea state without rebuilding (the
domain has no model-replace), keeping the RAO tables.
- core/src/wasm/WaveRaoData.h: embed the default vessel's excitation-force RAO
(vessel1.json waves, 6 DOF × 60 freq × 25 dir) so the WASM build has wave data
without file I/O.
- YmirBindings: add a WaveForces (RAO + calm spectrum) per vessel and a
setWaveConditions(Hs,Tp,dir,gamma,spectrum) that drives every vessel's spectrum.
- worker: parse the scenario wave keyframe and call setWaveConditions on env load.
- VesselPanel telemetry: show live φ (roll) / θ (pitch) / heave.
Verified in-browser: beam seas (Hs 6, Tp 14) → roll oscillates ±~1.3°, pitch ±~0.9°,
heave around equilibrium; head seas → near-zero roll (correct). Small angles because
the default hull is a 450,000 t VLCC. Core 257 tests + web 246 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 3D vessel only applied yaw, so even when the engine produced roll/pitch/heave
the ship rendered as a rigid block. Apply the full attitude: yaw about scene up,
then pitch about the beam axis and roll about the longitudinal axis in the
heading-aligned frame, plus heave bobbing on the waterline. Signs/axes documented
as calibratable. (Necessary half of the wave-motion fix; the wasm wave force is
wired separately.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The celso_furtado mesh origin is at the keel (world Y 0→46 = keel→masthead), so
placing it at the sea plane (Y=0) floated the whole ship above the water — and
near the shallow origin you saw the seabed around/under it. Sink the model by its
design waterline (~4.45 m, from vessel_config waterInteraction points) so the
waterline sits on the sea: hull partly submerged, superstructure above. Verified
in browser (Livre cam): vessel floats correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vessel appeared to float because AreaScene recentred the area group on the
combined bounding box, and ilhas.glb contains a stray vertex ~millions of units
out, so the box centre was garbage and shoved the terrain/water away from the
vessel. Verified against the uncompressed source: the meshes are Z-up with the
water surface at Z=0, modelled about the area's reference origin (= sim 0,0), so
the -90° X rotation already lands the waterline at Y=0 — no recentre needed.
- Remove the bbox recentre; trust the native origin (matches the sim frame).
- Enlarge the sea plane (200km) with a proper sea colour at Y=0 so the vessel
reads as floating on water and the bay sits in it.
Verified in browser (Livre cam): vessel on the sea, not in the air.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Livre" option to the onboard-camera selector. When active, the onboard
CameraRig is bypassed and FlyControls drives the camera: WASD to move, R/F up-down,
drag the mouse to look — go anywhere. On entry it positions above/behind the
selected vessel for an external overview, then hands control to the user.
viewStore CameraId gains 'Free'; CAMERAS map narrowed to the onboard ids with a
guard in CameraRig. CameraSelector + tests updated. 246 web tests green, tsc clean.
Verified in browser: external overview on select, keyboard fly confirmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Proa/Ponte/Popa/Boreste/Bombordo selector (the five celso_furtado onboard
cameras from vessel_config) shown over the 3D scene. Each camera uses its authored
local offset + yaw relative to the selected vessel; switching moves the view.
viewStore tracks the active camera; CameraSelector + tests added. 245 web tests
green, tsc clean. Verified in browser: switching cameras changes the viewpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New 3D background that swaps with the Leaflet map via a top-left Mapa/3D toggle —
only the background changes; all panels (top bar, sidebar, vessel panel, Play/Pause,
info) stay shared and live.
- Area3DView (react-three-fiber): loads the compressed Baía subset (batimetria +
ilhas + ponte, Z-up meshes rotated to Y-up and recentred on the sim origin) plus
a water plane and lighting; every vessel uses the celso_furtado mesh placed at
its live/draft pose.
- Camera uses the vessel's onboard "Front" camera (from vessel_config), following
the selected vessel; switching the selected vessel moves the view.
- geo3d util maps sim (x=East, y=North, ψ) → scene (X, -Z, yaw); viewStore holds
the mode; ViewToggle switches it. MapActions (Leaflet zoom) hidden in 3D.
Tests: geo3d + ViewToggle + viewStore; Area3DView excluded from coverage (WebGL,
e2e-only) like the Leaflet views. 243 web tests green, tsc clean. Verified in
browser: toggle both ways, onboard bridge camera, panels unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add three, @react-three/fiber, @react-three/drei (runtime) and gltf-transform /
gltfpack (dev) plus apps/web/scripts/build-3d-assets.sh, which compresses the
local Baía de Guanabara + celso_furtado source packs into web-ready GLBs
(meshopt geometry + WebP@1024 textures — no KTX2 transcoder needed under our
COOP/COEP). v1 subset = batimetria + ilhas + ponte + vessel (~21MB, down from
~640MB); topografia excluded (pathological per-tile textures). Generated GLBs are
gitignored — run the script once locally before using the 3D view.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vessel DivIcon was rebuilt on every render. While a simulation runs the map
re-renders ~20 Hz, so Leaflet replaced each marker's DOM element every ~50 ms —
a click's mousedown and mouseup landed on different elements and the click was
swallowed, so the vessel panel only opened when stopped. Memoize the icon on the
rounded heading + selection so the element is stable; clicks (and switching
between vessels) now work at any point during the run. Verified in-browser: panel
opens mid-run with live telemetry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Context-driven UI pass (not a blind Figma copy):
- Top-bar "+" now opens a VesselPicker popover to add vessels (it was a dead
button; adding only worked from the sidebar). Removed the stale center
actionsDisabled gating that greyed "+" out before the first vessel existed.
- Scenario name is edited inline in the breadcrumb; dropped the duplicate name
field from the sidebar (still editable in the info panel).
- VesselList rows are a readable two-line card (name + remove, then x/y + heading)
instead of a cramped single line; adding moved to the top bar.
- Sidebar is collapsible so the map can go full-width (map-first focus).
New VesselPicker (+ tests); ScenarioForm/VesselList/Sidebar/AppShell updated.
236 web tests green, web+api tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the "Build simulation" step — the floating control is now just Play/Pause.
Pause freezes the simulation in place (worker keeps the body + elapsed time) and
Play resumes from there instead of restarting, so the instructor can pause, add an
environmental condition or another vessel, and continue. On resume the current
environment is (re)applied; if the vessel set changed, the engine is reseeded with
existing vessels at their live positions so the scene doesn't jump back to t=0.
- simulationStore: start/stop → play/pause; status 'ready' → 'paused'; add
applyEnvironment(); 'ready' worker msg no longer flips to a transient status.
- SimulationControl primitive reduced to Play/Pause; status strip shows Pausado.
- Remove dead legacy TelemetryPanel. Tests updated; web 237 green, web+api tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The legacy-row backfill spread `Record<string,number>` which tsc could not narrow
to InitialCondition, breaking `pnpm --filter @ymir/api build` (and the docker api
image). Construct each InitialCondition explicitly with defaults. Verified
`apps/api` tsc + full `docker compose build` + up (api/web healthy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ScenarioInfoPanel (Figma "Informações do cenário"): a right-docked Panel with
editable scenario name, area metadata (gravity, water density) and vessel count,
toggled by the top-bar info (ⓘ) button which reflects open state. New panel + tests;
240 web tests green. (Vessel AIS/RIPEAM tab deferred — no source data/spec yet.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the environment editors in the Tabs primitive per the Figma "Condições
ambientais" layout: the current/wind/wave keyframe editors live under the main
tab, and a Visibilidade tab adds Chuva/Neblina sliders (UI-only, not yet wired to
the physics engine — noted in the panel). Test added; 236 web tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>