Skip to content

Commit fedf518

Browse files
authored
Feat: upgrade hdf5 to 1.0.0 and e2e fixtures
Feat: upgrade hdf5 to 1.0.0 and e2e fixtures
2 parents a3a269c + 98d318f commit fedf518

37 files changed

Lines changed: 908 additions & 284 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ jobs:
3030
cache: 'npm'
3131
- run: npm ci
3232
- run: npx playwright install chromium --with-deps
33-
- name: Generate E2E MCAP fixture
34-
run: node scripts/gen-test-mcap.mjs
33+
- name: Generate E2E fixtures
34+
run: npm run gen:e2e:fixtures
3535
- run: npm run test:e2e
3636
env:
3737
CI: true

docs/DEVELOPMENT.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,26 @@
1111
| `npm run build:lib` | `tsc` + **npm package** build (`vite.lib.config.ts``dist-lib/`); used by `prepublishOnly` and embedders. |
1212
| `npm run test:e2e` | Playwright (requires fixture MCAP; see below). |
1313

14-
## Fixtures (`public/examples/`)
14+
## Fixtures
1515

16-
Place sample bags/MCAP files here for local dev and E2E. Expected names include:
17-
18-
- `test_5s.mcap` — minimal indexed MCAP for most Playwright cases (generated automatically before `test:e2e`; see `scripts/gen-test-mcap.mjs`)
19-
- `episode_20260122_122345.hdf5` — optional HDF5 case
20-
21-
Regenerate the default small MCAP (also run as `pretest:e2e` via `npm run gen:e2e:fixtures`):
16+
Committed sources live under **`test-fixtures/`** (layouts, minimal HDF5/BVH, H264/JPEG bytes). Playwright copies or generates runtime files into **`public/examples/`** (gitignored) via:
2217

2318
```bash
2419
npm run gen:e2e:fixtures
2520
```
2621

27-
Override paths when files live elsewhere:
22+
This runs automatically as `pretest:e2e` before `npm run test:e2e`.
2823

29-
```bash
30-
export ROSVIEW_TEST_MCAP=/absolute/path/to/test_5s.mcap
31-
export ROSVIEW_TEST_HDF5=/absolute/path/to/episode.hdf5
32-
npm run test:e2e
33-
```
24+
| Generated file (`public/examples/`) | Purpose |
25+
|-------------------------------------|---------|
26+
| `test_5s.mcap` | Basic MCAP playback, dockview, transport |
27+
| `test_pose.mcap` | PoseStamped sidebar topics |
28+
| `test_3cam.mcap` | Three-camera image grid layout |
29+
| `test_h264.mcap` | H.264 CompressedImage decode |
30+
| `test_minimal.hdf5` | ALOHA-schema HDF5 (~7 KB) |
31+
| `test_minimal.bvh` | Minimal BVH skeleton |
32+
33+
Vitest layout round-trip tests import JSON directly from `test-fixtures/layouts/`.
3434

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

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

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

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

83-
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.
83+
Full E2E coverage requires `npm run gen:e2e:fixtures` so `public/examples/` is populated; no files outside the repo are needed.

index.html

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@
1818
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
1919
<meta name="author" content="IO-AI Tech — ROSView" />
2020

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

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

2828
<meta property="og:type" content="website" />
2929
<meta property="og:site_name" content="ROSView" />
30-
<meta property="og:url" content="https://io-ai.tech/rosview/?lang=en" />
30+
<meta property="og:url" content="https://rosview.com/?lang=en" />
3131
<meta
3232
property="og:title"
3333
content="ROSView — MCAP, ROS bag, ROS 2 db3, HDF5 &amp; BVH visualization in the browser"
@@ -39,7 +39,7 @@
3939
<meta property="og:locale" content="en_US" />
4040
<meta property="og:locale:alternate" content="zh_CN" />
4141
<meta property="og:locale:alternate" content="ja_JP" />
42-
<meta property="og:image" content="https://io-ai.tech/rosview/og-image.png" />
42+
<meta property="og:image" content="https://rosview.com/og-image.png" />
4343
<meta property="og:image:width" content="1200" />
4444
<meta property="og:image:height" content="630" />
4545
<meta
@@ -48,7 +48,7 @@
4848
/>
4949

5050
<meta name="twitter:card" content="summary_large_image" />
51-
<meta name="twitter:image" content="https://io-ai.tech/rosview/og-image.png" />
51+
<meta name="twitter:image" content="https://rosview.com/og-image.png" />
5252
<meta
5353
name="twitter:title"
5454
content="ROSView — MCAP, ROS bag, ROS 2 db3, HDF5 &amp; BVH visualization in the browser"
@@ -63,7 +63,7 @@
6363
"@context": "https://schema.org",
6464
"@type": "WebApplication",
6565
"name": "ROSView",
66-
"url": "https://io-ai.tech/rosview/?lang=en",
66+
"url": "https://rosview.com/?lang=en",
6767
"applicationCategory": "DeveloperApplication",
6868
"operatingSystem": "Any",
6969
"browserRequirements": "Requires JavaScript. Modern evergreen browser recommended.",
@@ -79,8 +79,8 @@
7979
</script>
8080
<script>
8181
(function () {
82-
var SEO_ORIGIN = 'https://io-ai.tech';
83-
var SEO_PATH = '/rosview/';
82+
var SEO_ORIGIN = 'https://rosview.com';
83+
var SEO_PATH = '/';
8484
var SEO_BASE = SEO_ORIGIN + SEO_PATH;
8585
var OG_IMAGE = SEO_BASE + 'og-image.png';
8686

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ioai/rosview",
3-
"version": "1.3.1",
3+
"version": "1.3.2",
44
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
55
"keywords": [
66
"ros",
@@ -63,7 +63,7 @@
6363
"lint": "eslint \"src/**/*.{ts,tsx}\" \"tests/**/*.ts\"",
6464
"test": "vitest run",
6565
"preview": "npm run build && vite preview",
66-
"gen:e2e:fixtures": "node scripts/gen-test-mcap.mjs",
66+
"gen:e2e:fixtures": "node scripts/gen-e2e-fixtures.mjs",
6767
"pretest:e2e": "npm run gen:e2e:fixtures",
6868
"test:e2e": "playwright test"
6969
},
@@ -96,7 +96,7 @@
9696
"@foxglove/rosmsg": "^5.0.5",
9797
"@foxglove/rosmsg-serialization": "^2.0.4",
9898
"@foxglove/rosmsg2-serialization": "^3.0.3",
99-
"@ioai/hdf5": "^0.1.4",
99+
"@ioai/hdf5": "^1.0.0",
100100
"@mcap/core": "^2.0.2",
101101
"@playwright/test": "^1.59.1",
102102
"@radix-ui/react-collapsible": "^1.1.12",

scripts/gen-e2e-fixtures.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Generate all Playwright E2E fixtures into public/examples/.
3+
*/
4+
import { spawnSync } from 'node:child_process';
5+
import path from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
9+
10+
/** @param {string} script */
11+
function runNode(script) {
12+
const scriptPath = path.join(__dirname, script);
13+
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' });
14+
if (result.status !== 0) {
15+
process.exit(result.status ?? 1);
16+
}
17+
}
18+
19+
/** @param {string} script */
20+
function runPython(script) {
21+
const scriptPath = path.join(__dirname, script);
22+
const result = spawnSync('python3', [scriptPath], { stdio: 'inherit' });
23+
if (result.status !== 0) {
24+
process.exit(result.status ?? 1);
25+
}
26+
}
27+
28+
runNode('gen-test-mcap.mjs');
29+
runNode('gen-test-mcap-pose.mjs');
30+
runNode('gen-test-mcap-3cam.mjs');
31+
runNode('gen-test-mcap-h264.mjs');
32+
runPython('gen-test-hdf5.py');
33+
runNode('gen-test-bvh.mjs');
34+
35+
console.log('[gen-e2e-fixtures] all fixtures ready');

scripts/gen-test-bvh.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copy minimal BVH fixture into public/examples/ for E2E.
3+
*/
4+
import fs from 'node:fs';
5+
import path from 'node:path';
6+
import { FIXTURES_DIR, EXAMPLES_DIR } from './mcap-fixture-utils.mjs';
7+
8+
const src = path.join(FIXTURES_DIR, 'media/minimal.bvh');
9+
const dest = path.join(EXAMPLES_DIR, 'test_minimal.bvh');
10+
fs.mkdirSync(EXAMPLES_DIR, { recursive: true });
11+
fs.copyFileSync(src, dest);
12+
console.log('Wrote', dest, `(${fs.statSync(dest).size} bytes)`);

scripts/gen-test-hdf5.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env python3
2+
"""Generate or copy minimal ALOHA-schema HDF5 fixture into public/examples/."""
3+
from __future__ import annotations
4+
5+
import shutil
6+
import sys
7+
from pathlib import Path
8+
9+
try:
10+
import h5py
11+
import numpy as np
12+
except ImportError:
13+
h5py = None # type: ignore
14+
np = None # type: ignore
15+
16+
ROOT = Path(__file__).resolve().parent.parent
17+
FIXTURE_SRC = ROOT / 'test-fixtures' / 'media' / 'minimal-aloha.h5'
18+
OUT = ROOT / 'public' / 'examples' / 'test_minimal.hdf5'
19+
20+
21+
def generate() -> None:
22+
assert h5py is not None and np is not None
23+
n, k, h, w, c = 4, 3, 2, 2, 3
24+
FIXTURE_SRC.parent.mkdir(parents=True, exist_ok=True)
25+
with h5py.File(FIXTURE_SRC, 'w') as h5:
26+
h5.create_dataset('/action', data=np.arange(n * k, dtype=np.float32).reshape(n, k))
27+
h5.create_dataset('/observations/qpos', data=(np.arange(n * k, dtype=np.float32).reshape(n, k) * 2))
28+
h5.create_dataset('/observations/qvel', data=np.zeros((n, k), dtype=np.float32))
29+
h5.create_dataset('/observations/tau_J', data=np.zeros((n, k), dtype=np.float32))
30+
h5.create_dataset('/observations/ee_pos_t', data=np.zeros((n, 3), dtype=np.float32))
31+
h5.create_dataset(
32+
'/observations/ee_pos_q',
33+
data=np.tile([0, 0, 0, 1], (n, 1)).astype(np.float32),
34+
)
35+
h5.create_dataset(
36+
'/observations/images/ext1',
37+
data=np.arange(n * h * w * c, dtype=np.uint8).reshape(n, h, w, c),
38+
)
39+
h5.create_dataset('/tm', data=np.full((n, 1), 0.125, dtype=np.float32))
40+
print(f'Generated {FIXTURE_SRC} ({FIXTURE_SRC.stat().st_size} bytes)')
41+
42+
43+
def main() -> int:
44+
if not FIXTURE_SRC.exists():
45+
if h5py is None:
46+
print('h5py not installed and committed fixture missing', file=sys.stderr)
47+
return 1
48+
generate()
49+
OUT.parent.mkdir(parents=True, exist_ok=True)
50+
shutil.copy2(FIXTURE_SRC, OUT)
51+
print(f'Wrote {OUT} ({OUT.stat().st_size} bytes)')
52+
return 0
53+
54+
55+
if __name__ == '__main__':
56+
raise SystemExit(main())

scripts/gen-test-mcap-3cam.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Minimal MCAP with three compressed JPEG camera topics for ros-image-grid E2E.
3+
*/
4+
import {
5+
createIndexedMcapWriter,
6+
encodeCompressedImageCdr,
7+
readFixture,
8+
registerCompressedImageChannel,
9+
writeExample,
10+
} from './mcap-fixture-utils.mjs';
11+
12+
const jpegBytes = readFixture('media/jpeg-1x1.bin');
13+
14+
const { writer, writable } = await createIndexedMcapWriter();
15+
16+
const topics = [
17+
'/camera/left/color/image_raw/compressed',
18+
'/camera/top/color/image_raw/compressed',
19+
'/camera/right/color/image_raw/compressed',
20+
];
21+
22+
const channelIds = [];
23+
for (const topic of topics) {
24+
channelIds.push(await registerCompressedImageChannel(topic, writer));
25+
}
26+
27+
const messageTimes = [1_000_000_000n, 3_000_000_000n, 5_000_000_000n];
28+
for (const [idx, ts] of messageTimes.entries()) {
29+
const stamp = { sec: Number(ts / 1_000_000_000n), nsec: Number(ts % 1_000_000_000n) };
30+
for (const channelId of channelIds) {
31+
await writer.addMessage({
32+
channelId,
33+
sequence: idx + 1,
34+
logTime: ts,
35+
publishTime: ts,
36+
data: encodeCompressedImageCdr(stamp, 'jpeg', jpegBytes),
37+
});
38+
}
39+
}
40+
41+
await writer.end();
42+
writeExample('test_3cam.mcap', writable.getBuffer());

scripts/gen-test-mcap-h264.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Minimal MCAP with H.264 CompressedImage messages for image-h264 E2E.
3+
*/
4+
import {
5+
createIndexedMcapWriter,
6+
encodeCompressedImageCdr,
7+
readFixture,
8+
registerCompressedImageChannel,
9+
writeExample,
10+
} from './mcap-fixture-utils.mjs';
11+
12+
const keyBytes = readFixture('media/h264-key.bin');
13+
const deltaBytes = readFixture('media/h264-delta.bin');
14+
15+
const { writer, writable } = await createIndexedMcapWriter();
16+
17+
const channelId = await registerCompressedImageChannel(
18+
'/camera/head/color/image_raw/compressed',
19+
writer,
20+
);
21+
22+
const frames = [
23+
{ ts: 1_000_000_000n, data: keyBytes },
24+
{ ts: 1_100_000_000n, data: deltaBytes },
25+
{ ts: 1_200_000_000n, data: deltaBytes },
26+
{ ts: 3_000_000_000n, data: keyBytes },
27+
{ ts: 3_100_000_000n, data: deltaBytes },
28+
{ ts: 5_000_000_000n, data: keyBytes },
29+
];
30+
31+
for (const [idx, { ts, data }] of frames.entries()) {
32+
const stamp = { sec: Number(ts / 1_000_000_000n), nsec: Number(ts % 1_000_000_000n) };
33+
await writer.addMessage({
34+
channelId,
35+
sequence: idx + 1,
36+
logTime: ts,
37+
publishTime: ts,
38+
data: encodeCompressedImageCdr(stamp, 'h264', data),
39+
});
40+
}
41+
42+
await writer.end();
43+
writeExample('test_h264.mcap', writable.getBuffer());

0 commit comments

Comments
 (0)