feat(web-platform): release V0.8.1 常规地图优化
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled

This commit is contained in:
2026-09-03 13:18:35 +08:00
parent deead17a9a
commit a9b07e0abf
49 changed files with 4987 additions and 569 deletions
+256 -8
View File
@@ -481,14 +481,14 @@ test('认证资产可点击创建场景并拖到画布落位', async ({ page })
await expect(page.getByText('模型加载完成')).toBeVisible({ timeout: 30_000 });
await page.getByRole('tab', { name: '地图' }).click();
const library = page.getByLabel('认证资产');
const library = page.getByLabel('地图资产库');
await expect(library.getByText('点击添加,或按住资产拖到画布落位。')).toBeVisible();
await library.getByRole('button', { name: '添加基础方盒' }).click();
await expect(page.getByText('正在加载 MuJoCo 与模型…')).toHaveCount(0);
await expect(page.getByLabel('地图来源')).toHaveValue('project:maps/scene_1/map.json', {
timeout: 30_000,
});
await expect(page.getByText('V3 地图编辑器')).toBeVisible();
await expect(page.getByText('认证资产与场景对象属性')).toBeVisible();
await expect(page.getByLabel('地图对象列表').getByText('基础方盒 · 方盒')).toBeVisible();
await page.locator('[data-map-asset="ramp"]').dragTo(page.locator('main canvas'));
@@ -510,6 +510,240 @@ test('认证资产可点击创建场景并拖到画布落位', async ({ page })
await page.mouse.move(canvasBox!.x + 104, canvasBox!.y + 50, { steps: 8 });
await page.mouse.up({ button: 'left' });
await expect.poll(() => gizmoLine.getAttribute('x2')).not.toBe(beforeRotation);
await expect(page.getByLabel('对象名称')).toHaveCount(0);
await expect(page.getByLabel('地图对象列表').getByText('基础方盒 · 方盒')).toBeVisible();
});
test('认证资产自动打开地图属性并与参数地形一次编译', async ({ page }) => {
await page.goto('/');
await page.setViewportSize({ width: 1440, height: 900 });
await page
.locator('input[type="file"]')
.first()
.setInputFiles({
name: 'unified-map-model.xml',
mimeType: 'text/xml',
buffer: Buffer.from(SIMPLE_MODEL),
});
await expect(page.getByText('模型加载完成')).toBeVisible({ timeout: 30_000 });
await page.getByRole('tab', { name: '地图' }).click();
const library = page.getByLabel('地图资产库');
await page.getByRole('tab', { name: '属性' }).click();
await library.getByRole('button', { name: '添加基础方盒' }).click();
await expect(page.getByRole('tab', { name: '地图' })).toHaveAttribute('aria-selected', 'true');
await expect(page.getByText('认证资产与场景对象属性')).toBeVisible();
await expect(page.getByText('1 项场景更改待应用')).toBeVisible();
const sceneTree = page.getByLabel('场景资产树');
await expect(sceneTree.getByText('基础方盒')).toBeVisible();
await page.getByRole('tab', { name: '属性' }).click();
await sceneTree.getByRole('button', { name: /基础方盒/ }).click();
await expect(page.getByRole('tab', { name: '地图' })).toHaveAttribute('aria-selected', 'true');
await library.getByRole('button', { name: '添加随机粗糙地形' }).click();
await page.getByLabel('位置 X(m)').fill('4');
await expect(page.getByText('2 项场景更改待应用')).toBeVisible();
// 切换到参数地形后,尚未编译的认证资产仍保留在画布中并可直接选回。
const draftCanvasBox = await page.locator('main canvas').first().boundingBox();
expect(draftCanvasBox).not.toBeNull();
let selectedDraftAsset = false;
for (let y = 0.15; y <= 0.85 && !selectedDraftAsset; y += 0.05) {
for (let x = 0.05; x <= 0.95; x += 0.05) {
await page.mouse.click(
draftCanvasBox!.x + draftCanvasBox!.width * x,
draftCanvasBox!.y + draftCanvasBox!.height * y,
);
await page.waitForTimeout(20);
if (await page.getByLabel('对象名称').count()) {
selectedDraftAsset = true;
break;
}
}
}
expect(selectedDraftAsset).toBe(true);
await expect(page.getByLabel('地图来源')).toHaveValue('project:maps/scene_1/map.json');
await sceneTree.getByRole('button', { name: '选择地图实例 随机粗糙地形' }).click();
await expect(page.getByLabel('地图来源')).toHaveValue('builtin');
await page.getByRole('button', { name: '一次编译应用' }).click();
await expect(page.getByText('2 项场景更改待应用')).toHaveCount(0, { timeout: 30_000 });
await expect(page.getByText(/已加载工程地图“场景 1”(1 个物理几何/)).toBeVisible();
await expect(page.getByText(/已加载随机粗糙地形物理地图/)).toBeVisible();
await expect(page.getByLabel('地图来源')).toHaveValue('builtin');
// 当前仍选中参数地形时,直接点已编译的认证资产也必须反查场景与对象,首次点击即挂载操纵器。
const canvasBox = await page.locator('main canvas').first().boundingBox();
expect(canvasBox).not.toBeNull();
let selectedCompiledAsset = false;
for (let y = 0.15; y <= 0.85 && !selectedCompiledAsset; y += 0.05) {
for (let x = 0.05; x <= 0.95; x += 0.05) {
await page.mouse.click(
canvasBox!.x + canvasBox!.width * x,
canvasBox!.y + canvasBox!.height * y,
);
await page.waitForTimeout(20);
if (await page.getByLabel('对象名称').count()) {
selectedCompiledAsset = true;
break;
}
}
}
expect(selectedCompiledAsset).toBe(true);
await expect(page.getByLabel('地图来源')).toHaveValue('project:maps/scene_1/map.json');
await expect(page.getByLabel('对象名称')).toHaveValue('基础方盒');
await expect(page.getByLabel('地图对象列表').getByText('基础方盒 · 方盒')).toBeVisible();
await expect(page.getByText('地图草稿尚未应用')).toHaveCount(0);
});
test('放弃首次认证资产会完整回滚临时场景和工程文件', async ({ page }) => {
await page.goto('/');
await page.setViewportSize({ width: 1440, height: 900 });
await page
.locator('input[type="file"]')
.first()
.setInputFiles({
name: 'discard-map-model.xml',
mimeType: 'text/xml',
buffer: Buffer.from(SIMPLE_MODEL),
});
await expect(page.getByText('模型加载完成')).toBeVisible({ timeout: 30_000 });
await page.getByRole('tab', { name: '地图' }).click();
const library = page.getByLabel('地图资产库');
await library.getByRole('button', { name: '添加基础方盒' }).click();
await expect(page.getByText('1 项场景更改待应用')).toBeVisible();
await expect(page.getByLabel('场景资产树')).toContainText('场景 1');
await page.getByRole('button', { name: '放弃场景更改' }).click();
await expect(page.getByText('1 项场景更改待应用')).toHaveCount(0);
await expect(page.getByLabel('场景资产树')).toContainText('0 个实例');
await expect(library.getByText('场景 1')).toHaveCount(0);
// 文件和 map id 也必须回滚;再次创建应复用 scene_1,而不是泄漏出 scene_2。
await library.getByRole('button', { name: '添加基础方盒' }).click();
await expect(page.getByLabel('场景资产树')).toContainText('场景 1');
await expect(page.getByLabel('场景资产树')).not.toContainText('场景 2');
});
test('工程地图与参数地形共享放置草稿、实例变换和回滚入口', async ({ page }) => {
const mapJson = JSON.stringify({
schemaVersion: 1,
id: 'warehouse-draft',
name: '草稿仓库',
coordinateSystem: { units: 'm', up: 'Z', forward: '+X' },
physics: { source: 'physics/world.xml' },
spawnPoints: [],
});
const project = zipSync({
'model.xml': Buffer.from(SIMPLE_MODEL),
'maps/warehouse/map.json': Buffer.from(mapJson),
'maps/warehouse/physics/world.xml': Buffer.from(
'<mujoco><worldbody><geom name="wall" type="box" pos="0 0 .5" size=".1 1 .5"/></worldbody></mujoco>',
),
});
await page.goto('/');
await page.setViewportSize({ width: 1440, height: 900 });
await page
.locator('input[type="file"]')
.first()
.setInputFiles({
name: 'unified-project-map.zip',
mimeType: 'application/zip',
buffer: Buffer.from(project),
});
await expect(page.getByText('模型加载完成')).toBeVisible({ timeout: 30_000 });
await page.getByRole('tab', { name: '地图' }).click();
const library = page.getByLabel('地图资产库');
await library.getByRole('button', { name: '放置工程地图 草稿仓库' }).click();
await expect(page.getByText('1 项场景更改待应用')).toBeVisible();
await expect(page.getByLabel('场景资产树')).toContainText('待应用');
await expect(page.getByText('正在准备仿真')).toHaveCount(0);
await page.getByLabel('位置 X(m)').fill('3');
await page.getByLabel('位置 Y(m)').fill('-2');
await expect(page.getByText('1 项场景更改待应用')).toBeVisible();
await library.getByRole('button', { name: '添加波浪地形' }).click();
await expect(page.getByText('2 项场景更改待应用')).toBeVisible();
await page.getByRole('button', { name: '一次编译应用' }).click();
await expect(page.getByText('2 项场景更改待应用')).toHaveCount(0, { timeout: 30_000 });
await expect(page.getByText(/已加载工程地图“草稿仓库”/)).toBeVisible();
await expect(page.getByText(/已加载波浪地形物理地图/)).toBeVisible();
await page.getByRole('button', { name: '删除地图实例 草稿仓库' }).click();
await expect(page.getByText('1 项场景更改待应用')).toBeVisible();
await expect(page.getByLabel('场景资产树')).not.toContainText('草稿仓库');
await page.getByRole('button', { name: '放弃场景更改' }).click();
await expect(page.getByText('1 项场景更改待应用')).toHaveCount(0);
await expect(page.getByLabel('场景资产树')).toContainText('草稿仓库');
});
test('认证资产重力放置使用参数地形的真实承载高度', async ({ page }) => {
await page.goto('/');
await page.setViewportSize({ width: 1440, height: 900 });
await page
.locator('input[type="file"]')
.first()
.setInputFiles({
name: 'terrain-surface-model.xml',
mimeType: 'text/xml',
buffer: Buffer.from(SIMPLE_MODEL),
});
await expect(page.getByText('模型加载完成')).toBeVisible({ timeout: 30_000 });
await page.getByRole('tab', { name: '地图' }).click();
const library = page.getByLabel('地图资产库');
await library.getByRole('button', { name: '添加深坑地形' }).click();
await page.getByLabel('地形难度(0–1)').fill('1');
await library.getByLabel('新增资产放置方式').selectOption('gravity');
await library.getByRole('button', { name: '添加基础方盒' }).click();
await expect(page.getByLabel('对象放置方式')).toHaveValue('gravity');
await expect(page.getByLabel('对象位置Z')).toHaveValue('-0.3');
await expect(page.getByText('2 项场景更改待应用')).toBeVisible();
});
test('参数化地形可连续拖到画布并一次性编译', async ({ page }) => {
await page.goto('/');
await page.setViewportSize({ width: 1440, height: 900 });
await page
.locator('input[type="file"]')
.first()
.setInputFiles({
name: 'terrain-draft-model.xml',
mimeType: 'text/xml',
buffer: Buffer.from(SIMPLE_MODEL),
});
await expect(page.getByText('模型加载完成')).toBeVisible({ timeout: 30_000 });
await page.getByRole('tab', { name: '地图' }).click();
await page.locator('[data-system-terrain="rough"]').dragTo(page.locator('main canvas'));
await expect(page.getByText('1 项场景更改待应用')).toBeVisible();
await expect(page.getByText('正在准备仿真')).toHaveCount(0);
await page.locator('[data-system-terrain="wave"]').dragTo(page.locator('main canvas'));
await expect(page.getByText('2 项场景更改待应用')).toBeVisible();
await expect(page.getByLabel('场景资产树')).toContainText('随机粗糙地形');
await expect(page.getByLabel('场景资产树')).toContainText('波浪地形');
await page.getByRole('tab', { name: '属性' }).click();
const canvasBox = await page.locator('main canvas').first().boundingBox();
expect(canvasBox).not.toBeNull();
await page.mouse.click(
canvasBox!.x + canvasBox!.width * 0.5,
canvasBox!.y + canvasBox!.height * 0.78,
);
await expect(page.getByRole('tab', { name: '地图' })).toHaveAttribute('aria-selected', 'true');
await expect(page.getByLabel('视口选择与变换')).toBeVisible();
await expect(page.getByText('地图与对象属性')).toBeVisible();
await page.getByRole('button', { name: '一次编译应用' }).click();
await expect(page.getByText('2 项场景更改待应用')).toHaveCount(0, { timeout: 30_000 });
await expect(page.getByText(/已加载随机粗糙地形物理地图/)).toBeVisible({
timeout: 30_000,
});
await expect(page.getByText(/已加载波浪地形物理地图/)).toBeVisible();
});
test('应用内置 MJCF 楼梯物理地图', async ({ page }) => {
@@ -638,7 +872,7 @@ test('将受支持的只读物理地图转换为可编辑副本', async ({ page
await page.getByRole('tab', { name: '地图' }).click();
await page.getByLabel('地图来源').selectOption({ label: '旧版基础场景' });
await page.getByRole('button', { name: '应用并重新编译' }).click();
const editor = page.getByText('V3 地图编辑器').locator('..');
const editor = page.getByText('认证资产与场景对象属性').locator('..');
await expect(editor.getByText(/保持只读/)).toBeVisible({ timeout: 30_000 });
await editor.getByRole('button', { name: '创建可编辑副本' }).click();
await expect(page.getByText('已创建可编辑地图副本')).toBeVisible({ timeout: 30_000 });
@@ -684,8 +918,8 @@ test('编辑 V3 地图对象并事务式应用', async ({ page }) => {
await page.getByRole('tab', { name: '地图' }).click();
await page.getByLabel('地图来源').selectOption({ label: '可编辑场景' });
await page.getByRole('button', { name: '应用并重新编译' }).click();
await expect(page.getByText('V3 地图编辑器')).toBeVisible({ timeout: 30_000 });
const editor = page.getByText('V3 地图编辑器').locator('..');
await expect(page.getByText('认证资产与场景对象属性')).toBeVisible({ timeout: 30_000 });
const editor = page.getByText('认证资产与场景对象属性').locator('..');
await expect(editor.getByRole('button', { name: '移动工具 W' })).toHaveAttribute(
'aria-pressed',
'true',
@@ -735,15 +969,29 @@ test('工程地图编译失败时保留上一仿真会话', async ({ page }) =>
buffer: Buffer.from(project),
});
await expect(page.getByText('WASM 已加载')).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: '▶ 播放' }).click();
await page.waitForTimeout(200);
const runningTime = Number(
(await page.locator('footer').innerText()).match(/时间 ([\d.]+) s/)?.[1] ?? 0,
);
await page.getByRole('tab', { name: '地图' }).click();
await page.getByLabel('地图来源').selectOption({ label: '动态错误地图' });
await page.getByRole('button', { name: '应用并重新编译' }).click();
await expect(page.getByRole('alert')).toContainText('模型编译失败', { timeout: 30_000 });
await expect(page.getByLabel('地图来源')).toHaveValue('none');
await expect(page.getByLabel('地图来源')).toHaveValue('project:maps/invalid/map.json');
await expect(page.getByText('1 项场景更改待应用')).toBeVisible();
await expect(page.getByLabel('场景资产树')).toContainText('待应用');
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeVisible();
await page.getByRole('button', { name: '关闭错误' }).click();
await page.getByRole('button', { name: '▶ 播放' }).click();
await page.waitForTimeout(300);
await expect(page.locator('footer')).not.toContainText('时间 0.000 s');
await expect
.poll(async () =>
Number((await page.locator('footer').innerText()).match(/时间 ([\d.]+) s/)?.[1] ?? 0),
)
.toBeGreaterThan(runningTime);
await page.getByRole('button', { name: '放弃场景更改' }).click();
await expect(page.getByText('1 项场景更改待应用')).toHaveCount(0);
await expect(page.getByLabel('地图来源')).toHaveValue('none');
});
test('无效模型显示中文诊断且保留工程树', async ({ page }) => {