Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npx playwright install chromium --with-deps
- name: Generate E2E MCAP fixture
run: node scripts/gen-test-mcap.mjs
- name: Generate E2E fixtures
run: npm run gen:e2e:fixtures
- run: npm run test:e2e
env:
CI: true
30 changes: 15 additions & 15 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@
| `npm run build:lib` | `tsc` + **npm package** build (`vite.lib.config.ts` → `dist-lib/`); used by `prepublishOnly` and embedders. |
| `npm run test:e2e` | Playwright (requires fixture MCAP; see below). |

## Fixtures (`public/examples/`)
## Fixtures

Place sample bags/MCAP files here for local dev and E2E. Expected names include:

- `test_5s.mcap` — minimal indexed MCAP for most Playwright cases (generated automatically before `test:e2e`; see `scripts/gen-test-mcap.mjs`)
- `episode_20260122_122345.hdf5` — optional HDF5 case

Regenerate the default small MCAP (also run as `pretest:e2e` via `npm run gen:e2e:fixtures`):
Committed sources live under **`test-fixtures/`** (layouts, minimal HDF5/BVH, H264/JPEG bytes). Playwright copies or generates runtime files into **`public/examples/`** (gitignored) via:

```bash
npm run gen:e2e:fixtures
```

Override paths when files live elsewhere:
This runs automatically as `pretest:e2e` before `npm run test:e2e`.

```bash
export ROSVIEW_TEST_MCAP=/absolute/path/to/test_5s.mcap
export ROSVIEW_TEST_HDF5=/absolute/path/to/episode.hdf5
npm run test:e2e
```
| Generated file (`public/examples/`) | Purpose |
|-------------------------------------|---------|
| `test_5s.mcap` | Basic MCAP playback, dockview, transport |
| `test_pose.mcap` | PoseStamped sidebar topics |
| `test_3cam.mcap` | Three-camera image grid layout |
| `test_h264.mcap` | H.264 CompressedImage decode |
| `test_minimal.hdf5` | ALOHA-schema HDF5 (~7 KB) |
| `test_minimal.bvh` | Minimal BVH skeleton |

Vitest layout round-trip tests import JSON directly from `test-fixtures/layouts/`.

For sample deep links (`?url=sample://…`), set `VITE_SAMPLE_DATASETS_MANIFEST_URL` in `.env` to a reachable JSON manifest (see `src/services/sampleDatasets.ts`).

Expand Down Expand Up @@ -62,7 +62,7 @@ Prefer main-thread rendering and subscription tuning before MCAP-parse WASM. Con
**Prerequisites**

1. `npm install` and (first time) `npx playwright install`.
2. Put `test_5s.mcap` under `public/examples/` or set `ROSVIEW_TEST_MCAP`.
2. `npm run gen:e2e:fixtures` (also runs automatically before `test:e2e`).
3. `npm run dev` → `http://localhost:5173`.

**Playwright**
Expand All @@ -80,4 +80,4 @@ npm run test:e2e
3. Switch to **Data** if multiple sources are present; the successfully loaded row is highlighted.
4. Open `/`, upload or drag a local `.mcap`; confirm load succeeds.

If no local sample is available, smoke-test routing and sidebar shell only; full Range behavior needs same-origin static assets and correct CORS/Range headers.
Full E2E coverage requires `npm run gen:e2e:fixtures` so `public/examples/` is populated; no files outside the repo are needed.
22 changes: 11 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta name="author" content="IO-AI Tech — ROSView" />

<link rel="alternate" hreflang="en" href="https://io-ai.tech/rosview/?lang=en" />
<link rel="alternate" hreflang="zh-CN" href="https://io-ai.tech/rosview/?lang=zh-CN" />
<link rel="alternate" hreflang="ja" href="https://io-ai.tech/rosview/?lang=ja" />
<link rel="alternate" hreflang="x-default" href="https://io-ai.tech/rosview/?lang=en" />
<link rel="alternate" hreflang="en" href="https://rosview.com/?lang=en" />
<link rel="alternate" hreflang="zh-CN" href="https://rosview.com/?lang=zh-CN" />
<link rel="alternate" hreflang="ja" href="https://rosview.com/?lang=ja" />
<link rel="alternate" hreflang="x-default" href="https://rosview.com/?lang=en" />

<link rel="canonical" href="https://io-ai.tech/rosview/?lang=en" />
<link rel="canonical" href="https://rosview.com/?lang=en" />

<meta property="og:type" content="website" />
<meta property="og:site_name" content="ROSView" />
<meta property="og:url" content="https://io-ai.tech/rosview/?lang=en" />
<meta property="og:url" content="https://rosview.com/?lang=en" />
<meta
property="og:title"
content="ROSView — MCAP, ROS bag, ROS 2 db3, HDF5 &amp; BVH visualization in the browser"
Expand All @@ -39,7 +39,7 @@
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
<meta property="og:locale:alternate" content="ja_JP" />
<meta property="og:image" content="https://io-ai.tech/rosview/og-image.png" />
<meta property="og:image" content="https://rosview.com/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
Expand All @@ -48,7 +48,7 @@
/>

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://io-ai.tech/rosview/og-image.png" />
<meta name="twitter:image" content="https://rosview.com/og-image.png" />
<meta
name="twitter:title"
content="ROSView — MCAP, ROS bag, ROS 2 db3, HDF5 &amp; BVH visualization in the browser"
Expand All @@ -63,7 +63,7 @@
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "ROSView",
"url": "https://io-ai.tech/rosview/?lang=en",
"url": "https://rosview.com/?lang=en",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Any",
"browserRequirements": "Requires JavaScript. Modern evergreen browser recommended.",
Expand All @@ -79,8 +79,8 @@
</script>
<script>
(function () {
var SEO_ORIGIN = 'https://io-ai.tech';
var SEO_PATH = '/rosview/';
var SEO_ORIGIN = 'https://rosview.com';
var SEO_PATH = '/';
var SEO_BASE = SEO_ORIGIN + SEO_PATH;
var OG_IMAGE = SEO_BASE + 'og-image.png';

Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ioai/rosview",
"version": "1.3.1",
"version": "1.3.2",
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
"keywords": [
"ros",
Expand Down Expand Up @@ -63,7 +63,7 @@
"lint": "eslint \"src/**/*.{ts,tsx}\" \"tests/**/*.ts\"",
"test": "vitest run",
"preview": "npm run build && vite preview",
"gen:e2e:fixtures": "node scripts/gen-test-mcap.mjs",
"gen:e2e:fixtures": "node scripts/gen-e2e-fixtures.mjs",
"pretest:e2e": "npm run gen:e2e:fixtures",
"test:e2e": "playwright test"
},
Expand Down Expand Up @@ -96,7 +96,7 @@
"@foxglove/rosmsg": "^5.0.5",
"@foxglove/rosmsg-serialization": "^2.0.4",
"@foxglove/rosmsg2-serialization": "^3.0.3",
"@ioai/hdf5": "^0.1.4",
"@ioai/hdf5": "^1.0.0",
"@mcap/core": "^2.0.2",
"@playwright/test": "^1.59.1",
"@radix-ui/react-collapsible": "^1.1.12",
Expand Down
35 changes: 35 additions & 0 deletions scripts/gen-e2e-fixtures.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Generate all Playwright E2E fixtures into public/examples/.
*/
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

/** @param {string} script */
function runNode(script) {
const scriptPath = path.join(__dirname, script);
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' });
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

/** @param {string} script */
function runPython(script) {
const scriptPath = path.join(__dirname, script);
const result = spawnSync('python3', [scriptPath], { stdio: 'inherit' });
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

runNode('gen-test-mcap.mjs');
runNode('gen-test-mcap-pose.mjs');
runNode('gen-test-mcap-3cam.mjs');
runNode('gen-test-mcap-h264.mjs');
runPython('gen-test-hdf5.py');
runNode('gen-test-bvh.mjs');

console.log('[gen-e2e-fixtures] all fixtures ready');
12 changes: 12 additions & 0 deletions scripts/gen-test-bvh.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copy minimal BVH fixture into public/examples/ for E2E.
*/
import fs from 'node:fs';
import path from 'node:path';
import { FIXTURES_DIR, EXAMPLES_DIR } from './mcap-fixture-utils.mjs';

const src = path.join(FIXTURES_DIR, 'media/minimal.bvh');
const dest = path.join(EXAMPLES_DIR, 'test_minimal.bvh');
fs.mkdirSync(EXAMPLES_DIR, { recursive: true });
fs.copyFileSync(src, dest);
console.log('Wrote', dest, `(${fs.statSync(dest).size} bytes)`);
56 changes: 56 additions & 0 deletions scripts/gen-test-hdf5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Generate or copy minimal ALOHA-schema HDF5 fixture into public/examples/."""
from __future__ import annotations

import shutil
import sys
from pathlib import Path

try:
import h5py
import numpy as np
except ImportError:
h5py = None # type: ignore
np = None # type: ignore

ROOT = Path(__file__).resolve().parent.parent
FIXTURE_SRC = ROOT / 'test-fixtures' / 'media' / 'minimal-aloha.h5'
OUT = ROOT / 'public' / 'examples' / 'test_minimal.hdf5'


def generate() -> None:
assert h5py is not None and np is not None
n, k, h, w, c = 4, 3, 2, 2, 3
FIXTURE_SRC.parent.mkdir(parents=True, exist_ok=True)
with h5py.File(FIXTURE_SRC, 'w') as h5:
h5.create_dataset('/action', data=np.arange(n * k, dtype=np.float32).reshape(n, k))
h5.create_dataset('/observations/qpos', data=(np.arange(n * k, dtype=np.float32).reshape(n, k) * 2))
h5.create_dataset('/observations/qvel', data=np.zeros((n, k), dtype=np.float32))
h5.create_dataset('/observations/tau_J', data=np.zeros((n, k), dtype=np.float32))
h5.create_dataset('/observations/ee_pos_t', data=np.zeros((n, 3), dtype=np.float32))
h5.create_dataset(
'/observations/ee_pos_q',
data=np.tile([0, 0, 0, 1], (n, 1)).astype(np.float32),
)
h5.create_dataset(
'/observations/images/ext1',
data=np.arange(n * h * w * c, dtype=np.uint8).reshape(n, h, w, c),
)
h5.create_dataset('/tm', data=np.full((n, 1), 0.125, dtype=np.float32))
print(f'Generated {FIXTURE_SRC} ({FIXTURE_SRC.stat().st_size} bytes)')


def main() -> int:
if not FIXTURE_SRC.exists():
if h5py is None:
print('h5py not installed and committed fixture missing', file=sys.stderr)
return 1
generate()
OUT.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(FIXTURE_SRC, OUT)
print(f'Wrote {OUT} ({OUT.stat().st_size} bytes)')
return 0


if __name__ == '__main__':
raise SystemExit(main())
42 changes: 42 additions & 0 deletions scripts/gen-test-mcap-3cam.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Minimal MCAP with three compressed JPEG camera topics for ros-image-grid E2E.
*/
import {
createIndexedMcapWriter,
encodeCompressedImageCdr,
readFixture,
registerCompressedImageChannel,
writeExample,
} from './mcap-fixture-utils.mjs';

const jpegBytes = readFixture('media/jpeg-1x1.bin');

const { writer, writable } = await createIndexedMcapWriter();

const topics = [
'/camera/left/color/image_raw/compressed',
'/camera/top/color/image_raw/compressed',
'/camera/right/color/image_raw/compressed',
];

const channelIds = [];
for (const topic of topics) {
channelIds.push(await registerCompressedImageChannel(topic, writer));
}

const messageTimes = [1_000_000_000n, 3_000_000_000n, 5_000_000_000n];
for (const [idx, ts] of messageTimes.entries()) {
const stamp = { sec: Number(ts / 1_000_000_000n), nsec: Number(ts % 1_000_000_000n) };
for (const channelId of channelIds) {
await writer.addMessage({
channelId,
sequence: idx + 1,
logTime: ts,
publishTime: ts,
data: encodeCompressedImageCdr(stamp, 'jpeg', jpegBytes),
});
}
}

await writer.end();
writeExample('test_3cam.mcap', writable.getBuffer());
43 changes: 43 additions & 0 deletions scripts/gen-test-mcap-h264.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Minimal MCAP with H.264 CompressedImage messages for image-h264 E2E.
*/
import {
createIndexedMcapWriter,
encodeCompressedImageCdr,
readFixture,
registerCompressedImageChannel,
writeExample,
} from './mcap-fixture-utils.mjs';

const keyBytes = readFixture('media/h264-key.bin');
const deltaBytes = readFixture('media/h264-delta.bin');

const { writer, writable } = await createIndexedMcapWriter();

const channelId = await registerCompressedImageChannel(
'/camera/head/color/image_raw/compressed',
writer,
);

const frames = [
{ ts: 1_000_000_000n, data: keyBytes },
{ ts: 1_100_000_000n, data: deltaBytes },
{ ts: 1_200_000_000n, data: deltaBytes },
{ ts: 3_000_000_000n, data: keyBytes },
{ ts: 3_100_000_000n, data: deltaBytes },
{ ts: 5_000_000_000n, data: keyBytes },
];

for (const [idx, { ts, data }] of frames.entries()) {
const stamp = { sec: Number(ts / 1_000_000_000n), nsec: Number(ts % 1_000_000_000n) };
await writer.addMessage({
channelId,
sequence: idx + 1,
logTime: ts,
publishTime: ts,
data: encodeCompressedImageCdr(stamp, 'h264', data),
});
}

await writer.end();
writeExample('test_h264.mcap', writable.getBuffer());
Loading
Loading