fix: improve match3d tray preview readability
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-05 17:17:27 +08:00
parent 06b8b46530
commit 2252afb080
8 changed files with 862 additions and 102 deletions

View File

@@ -12,9 +12,22 @@ import {
confirmLocalMatch3DClick,
startLocalMatch3DRun,
} from '../../services/match3d-runtime';
import {
MATCH3D_RENDER_ITEM_SCALE,
resolveRenderableItemFrame,
} from './match3dRuntimePresentation';
import {
MATCH3D_EXTRUDED_READABLE_SHAPES,
MATCH3D_TRAY_MODEL_MIN_RELATIVE_SIZE,
MATCH3D_TRAY_MODEL_TARGET_SIZE,
buildMatch3DPhysicsEntrySignature,
createMatch3DCannonShape,
createMatch3DThreeGeometry,
measureMatch3DItemPreviewDimension,
resolveMatch3DColliderBounds,
resolveMatch3DTrayPreviewRotation,
resolveMatch3DTrayPreviewReferenceDimension,
resolveMatch3DTrayPreviewScale,
} from './Match3DPhysicsBoard';
import { resolveGeometryAsset } from './match3dVisualAssets';
import { Match3DRuntimeShell } from './Match3DRuntimeShell';
@@ -38,6 +51,9 @@ vi.mock('./Match3DPhysicsBoard', async (importOriginal) => {
}, [onFallback]);
return <div data-testid="match3d-physics-board-fallback" />;
},
Match3DTrayPreviewBoard: () => (
<div data-testid="match3d-tray-model-board" />
),
};
});
@@ -91,6 +107,29 @@ test('展示圆形空间和 7 格备选栏', () => {
expect(screen.getAllByTestId('match3d-tray-slot')).toHaveLength(7);
});
test('显示层把可消除物整体半径放大 2 倍且保留相对比例', () => {
const run = startLocalMatch3DRun(25);
const firstItemByType = [...new Map(
run.items.map((item) => [item.itemTypeId, item]),
).values()];
const smallItem = firstItemByType.reduce((smallest, item) =>
item.radius < smallest.radius ? item : smallest,
);
const largeItem = firstItemByType.reduce((largest, item) =>
item.radius > largest.radius ? item : largest,
);
const smallFrame = resolveRenderableItemFrame(smallItem);
const largeFrame = resolveRenderableItemFrame(largeItem);
expect(smallFrame.radius).toBeCloseTo(
smallItem.radius * MATCH3D_RENDER_ITEM_SCALE,
);
expect(largeFrame.radius / smallFrame.radius).toBeCloseTo(
largeItem.radius / smallItem.radius,
);
});
test('点击可见物品后先乐观入槽再等待确认', async () => {
const run = startLocalMatch3DRun(4);
const clickableItem = run.items.find((item) => item.clickable);
@@ -103,6 +142,7 @@ test('点击可见物品后先乐观入槽再等待确认', async () => {
expect(onOptimisticRunChange).toHaveBeenCalled();
await waitFor(() => expect(onClickItem).toHaveBeenCalledTimes(1));
await waitFor(() => expect(onOptimisticRunChange).toHaveBeenCalledTimes(2));
});
test('3D 模式下备选栏使用共享模型预览层,避免挤占中心棋盘上下文', () => {
@@ -142,6 +182,25 @@ test('3D 模式下备选栏使用共享模型预览层,避免挤占中心棋
expect(screen.getByTestId('match3d-tray-model-board')).toBeTruthy();
});
test('3D 物理条目签名随 run 和视觉资源变化,避免旧模型复用到新局', () => {
const run = startLocalMatch3DRun(10);
const item = run.items[0]!;
const sameIdDifferentVisual = {
...item,
visualKey:
item.visualKey === 'block-red-2x4'
? 'block-blue-1x2'
: 'block-red-2x4',
};
expect(buildMatch3DPhysicsEntrySignature(run.runId, item)).not.toBe(
buildMatch3DPhysicsEntrySignature(`${run.runId}-next`, item),
);
expect(buildMatch3DPhysicsEntrySignature(run.runId, item)).not.toBe(
buildMatch3DPhysicsEntrySignature(run.runId, sameIdDifferentVisual),
);
});
test('本地试玩按消除次数生成类型并在 25 类封顶', () => {
const smallRun = startLocalMatch3DRun(12);
const largeRun = startLocalMatch3DRun(100);
@@ -280,6 +339,67 @@ test('同一视觉模型在复用时保持唯一尺寸', () => {
expect([...radiiByVisualKey.values()].every((radii) => radii.size === 1)).toBe(true);
});
test('托盘 3D 预览保留场内模型的相对尺寸比例', async () => {
const three = await import('three');
const run = startLocalMatch3DRun(25);
const firstItemByType = [...new Map(
run.items.map((item) => [item.itemTypeId, item]),
).values()];
const referenceDimension = resolveMatch3DTrayPreviewReferenceDimension(
three,
firstItemByType,
);
const previewRatios = new Set(
firstItemByType.map((item) =>
Math.round(
(measureMatch3DItemPreviewDimension(three, item) /
referenceDimension) *
1_000,
),
),
);
expect(previewRatios.size).toBeGreaterThan(1);
});
test('托盘 3D 预览放大模型并展示俯视 3/4 体积感', () => {
expect(MATCH3D_TRAY_MODEL_TARGET_SIZE).toBeGreaterThanOrEqual(0.85);
expect(MATCH3D_TRAY_MODEL_MIN_RELATIVE_SIZE).toBeGreaterThanOrEqual(0.9);
const brickRotation = resolveMatch3DTrayPreviewRotation('block-red-2x4');
const tileRotation = resolveMatch3DTrayPreviewRotation(
'block-lavender-tile-2x2',
);
const slopeRotation = resolveMatch3DTrayPreviewRotation(
'block-purple-slope-1x2',
);
expect(brickRotation.x).toBeLessThan(-0.28);
expect(brickRotation.z).toBeGreaterThan(0.2);
expect(brickRotation.y).toBeGreaterThan(0.6);
expect(tileRotation.x).toBeLessThan(-0.25);
expect(tileRotation.z).toBeGreaterThan(0.2);
expect(slopeRotation.x).toBeLessThan(-0.3);
expect(slopeRotation.z).toBeGreaterThan(0.22);
});
test('托盘 3D 预览为小模型保留最低可读显示尺寸', () => {
const smallDimension = 0.4;
const referenceDimension = 1;
const scale = resolveMatch3DTrayPreviewScale(
smallDimension,
referenceDimension,
);
expect(scale * smallDimension).toBeGreaterThanOrEqual(
MATCH3D_TRAY_MODEL_TARGET_SIZE *
MATCH3D_TRAY_MODEL_MIN_RELATIVE_SIZE,
);
expect(scale).toBeGreaterThan(
MATCH3D_TRAY_MODEL_TARGET_SIZE / referenceDimension,
);
});
test('积木 3D 资源可以为本局类型创建几何体', async () => {
const three = await import('three');
const run = startLocalMatch3DRun(15);
@@ -296,6 +416,37 @@ test('积木 3D 资源可以为本局类型创建几何体', async () => {
}
});
test('3D 物体碰撞体按同款视觉尺寸生成', async () => {
const cannon = await import('cannon-es');
const longBrick = resolveGeometryAsset('block-black-1x8');
const tile = resolveGeometryAsset('block-lavender-tile-2x2');
const cylinder = resolveGeometryAsset('block-green-cylinder');
const radius = 1;
const longBrickBounds = resolveMatch3DColliderBounds(longBrick, radius);
const longBrickShape = createMatch3DCannonShape(cannon, longBrick, radius);
expect(longBrickShape.type).toBe(cannon.Shape.types.BOX);
expect((longBrickShape as import('cannon-es').Box).halfExtents.x * 2).toBeCloseTo(
longBrickBounds.width,
);
expect((longBrickShape as import('cannon-es').Box).halfExtents.z * 2).toBeCloseTo(
longBrickBounds.depth,
);
const tileBounds = resolveMatch3DColliderBounds(tile, radius);
const tileShape = createMatch3DCannonShape(cannon, tile, radius);
expect((tileShape as import('cannon-es').Box).halfExtents.y * 2).toBeCloseTo(
tileBounds.height,
);
const cylinderBounds = resolveMatch3DColliderBounds(cylinder, radius);
const cylinderShape = createMatch3DCannonShape(cannon, cylinder, radius);
expect(cylinderShape.type).toBe(cannon.Shape.types.CYLINDER);
expect(
(cylinderShape as import('cannon-es').Cylinder).height,
).toBeCloseTo(cylinderBounds.height);
});
test('积木视觉键不会被统一兜底成红色苹字', () => {
const run = startLocalMatch3DRun(2);
run.items = run.items.slice(0, 2).map((item, index) => ({