fix: improve match3d tray preview readability
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user