This commit is contained in:
2026-05-13 00:28:07 +08:00
parent ef4f91a75e
commit 01c5ab985a
101 changed files with 10635 additions and 2292 deletions

View File

@@ -6,7 +6,10 @@ import type {
} from '../../../packages/shared/src/contracts/match3dRuntime';
import type { Match3DGeneratedItemAsset } from '../../../packages/shared/src/contracts/match3dWorks';
import { isDebugMode } from '../../config/debugMode';
import { readAssetBytes } from '../../services/assetReadUrlService';
import {
readMatch3DGeneratedModelBytes,
resolveMatch3DGeneratedModelAssetSource,
} from '../../services/match3dGeneratedModelCache';
import {
isItemState,
resolveRenderableItemFrame,
@@ -111,6 +114,8 @@ type PhysicsRuntime = {
animationId: number | null;
camera: ThreeCamera;
entries: Map<string, PhysicsEntry>;
failedGeneratedModelTypeIds: Set<string>;
generatedModelByType: Map<string, Match3DGeneratedItemAsset>;
generatedModelTemplates: Match3DGeneratedModelTemplateMap;
pendingSpawns: Map<string, PendingPhysicsSpawn>;
raycaster: import('three').Raycaster;
@@ -170,7 +175,7 @@ export const MATCH3D_EXTRUDED_READABLE_SHAPES: ReadonlySet<Match3DGeometryShape>
function normalizeMatch3DGeneratedModelSource(
asset: Match3DGeneratedItemAsset,
) {
return asset.modelSrc?.trim() || asset.modelObjectKey?.trim() || '';
return resolveMatch3DGeneratedModelAssetSource(asset);
}
function compareMatch3DGeneratedTypeId(left: string, right: string) {
@@ -213,6 +218,7 @@ export function buildMatch3DGeneratedAssetTypeMap(
...resolved.asset,
modelSrc: resolved.source,
});
debugMatch3DGeneratedModelMapped(itemTypeId, resolved.source);
});
return assetMap;
@@ -237,12 +243,16 @@ function resolveGeneratedModelSourceForItemType(
return asset ? normalizeMatch3DGeneratedModelSource(asset) : '';
}
function shouldLogMatch3DGeneratedModelDiagnostics() {
return isDebugMode() && import.meta.env.MODE !== 'test';
}
function warnMatch3DGeneratedModelLoadFailure(
itemTypeId: string,
source: string,
error: unknown,
) {
if (!isDebugMode()) {
if (!shouldLogMatch3DGeneratedModelDiagnostics()) {
return;
}
const message =
@@ -254,6 +264,32 @@ function warnMatch3DGeneratedModelLoadFailure(
});
}
function debugMatch3DGeneratedModelLoaded(
itemTypeId: string,
source: string,
) {
if (!shouldLogMatch3DGeneratedModelDiagnostics()) {
return;
}
console.debug('[match3d] generated model loaded', {
itemTypeId,
source,
});
}
function debugMatch3DGeneratedModelMapped(
itemTypeId: string,
source: string,
) {
if (!shouldLogMatch3DGeneratedModelDiagnostics()) {
return;
}
console.debug('[match3d] generated model mapped', {
itemTypeId,
source,
});
}
async function loadMatch3DGeneratedModelTemplate(
templateMap: Match3DGeneratedModelTemplateMap,
three: ThreeModule,
@@ -265,11 +301,10 @@ async function loadMatch3DGeneratedModelTemplate(
if (cached?.source === source) {
return cached.scene;
}
const response = await readAssetBytes(source, {
const bytes = await readMatch3DGeneratedModelBytes(source, {
expireSeconds: 300,
signal,
});
const bytes = await response.arrayBuffer();
if (bytes.byteLength === 0) {
throw new Error('抓大鹅 3D 模型内容为空');
}
@@ -297,6 +332,7 @@ async function loadMatch3DGeneratedModelTemplate(
scene,
source,
});
debugMatch3DGeneratedModelLoaded(itemTypeId, source);
return scene;
}
@@ -327,7 +363,6 @@ function createGeneratedModelMesh(
}
const position = toWorldPosition(item);
const model = cloneThreeObjectWithMaterials(template);
markObjectForItem(model, item.itemInstanceId);
const bounds = new three.Box3().setFromObject(model);
const size = bounds.getSize(new three.Vector3());
const dimension = Math.max(size.x, size.y, size.z, 0.001);
@@ -341,10 +376,13 @@ function createGeneratedModelMesh(
model.position.sub(center);
const bottomY = scaledBounds.min.y - center.y;
model.position.y -= bottomY;
const pivot = new three.Group();
pivot.add(model);
markObjectForItem(pivot, item.itemInstanceId);
return {
lockReadableTop: false,
mesh: model,
mesh: pivot,
radius: position.radius,
shape: 'brick' as Match3DGeometryShape,
topRotationY: ((item.layer % 12) / 12) * Math.PI * 2,
@@ -1157,6 +1195,22 @@ function createItemMesh(
);
}
function shouldWaitForGeneratedModelTemplate(
generatedModelByType: Map<string, Match3DGeneratedItemAsset>,
templateMap: Match3DGeneratedModelTemplateMap,
failedTypeIds: ReadonlySet<string>,
itemTypeId: string,
) {
const source = resolveGeneratedModelSourceForItemType(
generatedModelByType,
itemTypeId,
);
// 中文注释:坏 GLB 或过期链接不能让整局空等模板;失败类型应立即走默认几何降级。
return Boolean(
source && !templateMap.has(itemTypeId) && !failedTypeIds.has(itemTypeId),
);
}
export function buildMatch3DPhysicsEntrySignature(
runId: string,
item: Match3DItemSnapshot,
@@ -1192,6 +1246,16 @@ function createPhysicsEntryFromPendingSpawn(
now: number,
templateMap?: Match3DGeneratedModelTemplateMap | null,
) {
if (
shouldWaitForGeneratedModelTemplate(
runtime.generatedModelByType,
runtime.generatedModelTemplates,
runtime.failedGeneratedModelTypeIds,
pendingSpawn.item.itemTypeId,
)
) {
return;
}
const visual = createItemMesh(runtime.three, pendingSpawn.item, templateMap);
const asset = resolveGeometryAsset(pendingSpawn.item.visualKey);
const colliderBounds = resolveMatch3DColliderBounds(asset, visual.radius);
@@ -1273,7 +1337,17 @@ function createPhysicsEntryFromPendingSpawn(
function flushPendingPhysicsSpawns(runtime: PhysicsRuntime, now: number) {
const readySpawns = [...runtime.pendingSpawns.entries()]
.filter(([, pendingSpawn]) => now >= pendingSpawn.spawnAtMs)
.filter(([, pendingSpawn]) => {
if (now < pendingSpawn.spawnAtMs) {
return false;
}
return !shouldWaitForGeneratedModelTemplate(
runtime.generatedModelByType,
runtime.generatedModelTemplates,
runtime.failedGeneratedModelTypeIds,
pendingSpawn.item.itemTypeId,
);
})
.sort((left, right) => {
if (left[1].spawnAtMs !== right[1].spawnAtMs) {
return left[1].spawnAtMs - right[1].spawnAtMs;
@@ -1317,6 +1391,7 @@ type TrayPreviewRuntime = {
animationId: number | null;
camera: ThreeCamera;
entries: Map<string, ThreeObject3D>;
failedGeneratedModelTypeIds: Set<string>;
generatedModelTemplates: Match3DGeneratedModelTemplateMap;
renderer: ThreeRenderer;
scene: ThreeScene;
@@ -1620,6 +1695,8 @@ export function Match3DTrayPreviewBoard({
animationId: null,
camera,
entries: runtimeRef.current?.entries ?? new Map(),
failedGeneratedModelTypeIds:
runtimeRef.current?.failedGeneratedModelTypeIds ?? new Set(),
generatedModelTemplates:
runtimeRef.current?.generatedModelTemplates ?? new Map(),
renderer,
@@ -1645,6 +1722,7 @@ export function Match3DTrayPreviewBoard({
animationId: window.requestAnimationFrame(animate),
camera,
entries: new Map(),
failedGeneratedModelTypeIds: new Set(),
generatedModelTemplates: new Map(),
renderer,
scene,
@@ -1687,6 +1765,7 @@ export function Match3DTrayPreviewBoard({
staleItemTypeIds.delete(itemTypeId);
const hadFreshTemplate =
runtime.generatedModelTemplates.get(itemTypeId)?.source === source;
runtime.failedGeneratedModelTypeIds.delete(itemTypeId);
void loadMatch3DGeneratedModelTemplate(
runtime.generatedModelTemplates,
runtime.three,
@@ -1721,6 +1800,8 @@ export function Match3DTrayPreviewBoard({
caughtError,
);
runtime.generatedModelTemplates.delete(itemTypeId);
runtime.failedGeneratedModelTypeIds.add(itemTypeId);
setTrayModelRevision((current) => current + 1);
});
});
staleItemTypeIds.forEach((itemTypeId) => {
@@ -1729,6 +1810,7 @@ export function Match3DTrayPreviewBoard({
disposeThreeObject(template.scene);
}
runtime.generatedModelTemplates.delete(itemTypeId);
runtime.failedGeneratedModelTypeIds.delete(itemTypeId);
});
return () => {
@@ -1785,7 +1867,9 @@ export function Match3DTrayPreviewBoard({
const preview = createItemMesh(
runtime.three,
item,
runtime.generatedModelTemplates,
runtime.failedGeneratedModelTypeIds.has(item.itemTypeId)
? null
: runtime.generatedModelTemplates,
);
const model = preview.mesh;
const rotation = resolveMatch3DTrayPreviewRotation(item.visualKey);
@@ -2050,6 +2134,8 @@ export function Match3DPhysicsBoard({
animationId: null,
camera,
entries: new Map(),
failedGeneratedModelTypeIds: new Set(),
generatedModelByType,
generatedModelTemplates: new Map(),
pendingSpawns: new Map(),
raycaster: new three.Raycaster(),
@@ -2157,6 +2243,7 @@ export function Match3DPhysicsBoard({
if (!runtime) {
return undefined;
}
runtime.generatedModelByType = generatedModelByType;
const abortController = new AbortController();
const staleItemTypeIds = new Set(runtime.generatedModelTemplates.keys());
generatedModelByType.forEach((asset, itemTypeId) => {
@@ -2167,6 +2254,7 @@ export function Match3DPhysicsBoard({
}
const hadFreshTemplate =
runtime.generatedModelTemplates.get(itemTypeId)?.source === source;
runtime.failedGeneratedModelTypeIds.delete(itemTypeId);
void loadMatch3DGeneratedModelTemplate(
runtime.generatedModelTemplates,
runtime.three,
@@ -2201,6 +2289,8 @@ export function Match3DPhysicsBoard({
caughtError,
);
runtime.generatedModelTemplates.delete(itemTypeId);
runtime.failedGeneratedModelTypeIds.add(itemTypeId);
setGeneratedModelRevision((current) => current + 1);
});
});
staleItemTypeIds.forEach((itemTypeId) => {
@@ -2209,6 +2299,7 @@ export function Match3DPhysicsBoard({
disposeThreeObject(template.scene);
}
runtime.generatedModelTemplates.delete(itemTypeId);
runtime.failedGeneratedModelTypeIds.delete(itemTypeId);
});
return () => {