Prune stale docs and update .hermes content
Delete a large set of outdated documentation (many files under docs/ and .hermes/plans/, including audits, design, prd, technical, planning, assets, and todos). Update and consolidate .hermes content: refresh shared-memory pages (decision-log, development-workflow, document-map, pitfalls, project-overview, team-conventions) and several skills/references under .hermes/skills. Also modify AGENTS.md, README.md, UI_CODING_STANDARD.md, docs/README.md and .encoding-check-ignore. Purpose: clean up stale planning/audit material and keep current hermes documentation and related top-level docs in sync.
This commit is contained in:
@@ -10,6 +10,10 @@ const MATCH3D_TRAY_SLOT_COUNT = 7;
|
||||
const MATCH3D_LOCAL_DURATION_MS = 600_000;
|
||||
const MATCH3D_MAX_ITEM_TYPE_COUNT = 25;
|
||||
const MATCH3D_LOCAL_BASE_RADIUS = 0.072;
|
||||
const MATCH3D_LOCAL_BOARD_CENTER = 0.5;
|
||||
const MATCH3D_LOCAL_BOARD_RADIUS = 0.5;
|
||||
const MATCH3D_LOCAL_BOARD_SAFE_MARGIN = 0.035;
|
||||
const MATCH3D_LOCAL_CONTAINER_MOUTH_RATIO = 0.78;
|
||||
|
||||
type Match3DSizeTier = 'XL' | 'L' | 'M' | 'XS' | 'S';
|
||||
|
||||
@@ -260,6 +264,45 @@ function createEmptyTray(): Match3DTraySlot[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveLocalMatch3DSpawnPoint(
|
||||
index: number,
|
||||
totalItemCount: number,
|
||||
radius: number,
|
||||
) {
|
||||
const safeRadius = Math.max(
|
||||
0,
|
||||
MATCH3D_LOCAL_BOARD_RADIUS - MATCH3D_LOCAL_BOARD_SAFE_MARGIN - radius,
|
||||
);
|
||||
const mouthRadius = safeRadius * MATCH3D_LOCAL_CONTAINER_MOUTH_RATIO;
|
||||
const normalizedIndex = Math.max(0, index);
|
||||
const normalizedTotal = Math.max(1, totalItemCount);
|
||||
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
|
||||
const distance =
|
||||
Math.sqrt((normalizedIndex + 0.5) / normalizedTotal) * mouthRadius;
|
||||
const angle = normalizedIndex * goldenAngle;
|
||||
const jitterRadius = Math.min(0.012, mouthRadius * 0.035);
|
||||
const jitterAngle = angle * 1.7 + 0.9;
|
||||
const x =
|
||||
MATCH3D_LOCAL_BOARD_CENTER +
|
||||
Math.cos(angle) * distance +
|
||||
Math.cos(jitterAngle) * jitterRadius;
|
||||
const y =
|
||||
MATCH3D_LOCAL_BOARD_CENTER +
|
||||
Math.sin(angle) * distance +
|
||||
Math.sin(jitterAngle) * jitterRadius;
|
||||
const dx = x - MATCH3D_LOCAL_BOARD_CENTER;
|
||||
const dy = y - MATCH3D_LOCAL_BOARD_CENTER;
|
||||
const currentDistance = Math.hypot(dx, dy);
|
||||
if (currentDistance <= safeRadius || currentDistance <= 0) {
|
||||
return { x, y };
|
||||
}
|
||||
const ratio = safeRadius / currentDistance;
|
||||
return {
|
||||
x: MATCH3D_LOCAL_BOARD_CENTER + dx * ratio,
|
||||
y: MATCH3D_LOCAL_BOARD_CENTER + dy * ratio,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRemainingMs(run: Match3DRunSnapshot, nowMs = Date.now()) {
|
||||
if (run.status !== 'Running') {
|
||||
return run;
|
||||
@@ -287,20 +330,16 @@ function buildItem(
|
||||
seed: Match3DSelectedVisualSeed,
|
||||
index: number,
|
||||
copyIndex: number,
|
||||
totalItemCount: number,
|
||||
): Match3DItemSnapshot {
|
||||
const ring = Math.floor(index / 6);
|
||||
const angle = index * 0.86 + copyIndex * 0.22;
|
||||
const spread = 0.16 + (ring % 4) * 0.085;
|
||||
const x = 0.5 + Math.cos(angle) * spread + ((index % 3) - 1) * 0.026;
|
||||
const y =
|
||||
0.5 + Math.sin(angle * 1.13) * spread + ((copyIndex % 3) - 1) * 0.02;
|
||||
const radius = MATCH3D_LOCAL_BASE_RADIUS * seed.radiusScale;
|
||||
const point = resolveLocalMatch3DSpawnPoint(index, totalItemCount, radius);
|
||||
return {
|
||||
itemInstanceId: `${seed.itemTypeId}-${copyIndex + 1}`,
|
||||
itemTypeId: seed.itemTypeId,
|
||||
visualKey: seed.visualKey,
|
||||
x: Math.max(0.18, Math.min(0.82, x)),
|
||||
y: Math.max(0.18, Math.min(0.82, y)),
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
radius,
|
||||
layer: index + 1,
|
||||
state: 'InBoard',
|
||||
@@ -425,17 +464,19 @@ function settleMatchedTrayItems(run: Match3DRunSnapshot) {
|
||||
}
|
||||
|
||||
export function startLocalMatch3DRun(clearCount = 12): Match3DRunSnapshot {
|
||||
const normalizedClearCount = normalizeLocalMatch3DRuntimeClearCount(clearCount);
|
||||
const normalizedClearCount =
|
||||
normalizeLocalMatch3DRuntimeClearCount(clearCount);
|
||||
const selectedSeeds = selectVisualSeeds(normalizedClearCount);
|
||||
const totalItemCount = normalizedClearCount * 3;
|
||||
const items = Array.from({ length: normalizedClearCount }, (_, clearIndex) =>
|
||||
Array.from({ length: 3 }, (_, copyOffset) => {
|
||||
const seed =
|
||||
selectedSeeds[clearIndex % selectedSeeds.length] ??
|
||||
selectedSeeds[0]!;
|
||||
selectedSeeds[clearIndex % selectedSeeds.length] ?? selectedSeeds[0]!;
|
||||
return buildItem(
|
||||
seed,
|
||||
clearIndex * 3 + copyOffset,
|
||||
clearIndex * 3 + copyOffset,
|
||||
totalItemCount,
|
||||
);
|
||||
}),
|
||||
).flat();
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { setStoredAccessToken, clearStoredAccessToken } from './apiClient';
|
||||
import { clearSignedAssetReadUrlCache } from './assetReadUrlService';
|
||||
import {
|
||||
clearMatch3DGeneratedModelBytesCache,
|
||||
getMatch3DGeneratedImageViewSources,
|
||||
getMatch3DGeneratedImageAssetSources,
|
||||
getMatch3DGeneratedRuntimeUiAssetSources,
|
||||
getMatch3DGeneratedModelAssetSources,
|
||||
hasMatch3DGeneratedImageAsset,
|
||||
mergeMatch3DGeneratedItemAssetsForRuntime,
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime,
|
||||
preloadMatch3DGeneratedImageAssets,
|
||||
preloadMatch3DGeneratedRuntimeAssets,
|
||||
preloadMatch3DGeneratedModelAssets,
|
||||
readMatch3DGeneratedModelBytes,
|
||||
} from './match3dGeneratedModelCache';
|
||||
@@ -18,6 +21,7 @@ describe('match3dGeneratedModelCache', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
clearMatch3DGeneratedModelBytesCache();
|
||||
clearSignedAssetReadUrlCache();
|
||||
clearStoredAccessToken({ emit: false });
|
||||
});
|
||||
|
||||
@@ -266,6 +270,77 @@ describe('match3dGeneratedModelCache', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('运行态预加载同时解析背景和容器 UI 资产', async () => {
|
||||
setStoredAccessToken('test-access-token', { emit: false });
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
read: {
|
||||
signedUrl: 'https://oss.example.com/match3d-asset.png',
|
||||
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
),
|
||||
);
|
||||
const assets = [
|
||||
{
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc: null,
|
||||
imageObjectKey: null,
|
||||
imageViews: [
|
||||
{
|
||||
viewId: 'view-01',
|
||||
viewIndex: 1,
|
||||
imageSrc: null,
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/item-1/views/view-01.png',
|
||||
},
|
||||
],
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
status: 'image_ready',
|
||||
backgroundAsset: {
|
||||
prompt: '果园背景',
|
||||
imageSrc: null,
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/task/background.png',
|
||||
containerPrompt: '果园浅盘',
|
||||
containerImageSrc: null,
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/task/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getMatch3DGeneratedRuntimeUiAssetSources(assets)).toEqual([
|
||||
'generated-match3d-assets/session/profile/background/task/background.png',
|
||||
'generated-match3d-assets/session/profile/ui-container/task/container.png',
|
||||
]);
|
||||
await preloadMatch3DGeneratedRuntimeAssets(assets, null, {
|
||||
expireSeconds: 300,
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(3);
|
||||
expect(
|
||||
vi
|
||||
.mocked(globalThis.fetch)
|
||||
.mock.calls.map((call) => decodeURIComponent(String(call[0]))),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('/items/item-1/views/view-01.png'),
|
||||
expect.stringContaining('/background/task/background.png'),
|
||||
expect.stringContaining('/ui-container/task/container.png'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('作品级背景音乐会归一化到首个抓大鹅素材', () => {
|
||||
const assets = normalizeMatch3DGeneratedItemAssetsForRuntime([
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { Match3DGeneratedItemAsset } from '../../packages/shared/src/contracts/match3dWorks';
|
||||
import type {
|
||||
Match3DGeneratedBackgroundAsset,
|
||||
Match3DGeneratedItemAsset,
|
||||
} from '../../packages/shared/src/contracts/match3dWorks';
|
||||
import { readAssetBytes, resolveAssetReadUrl } from './assetReadUrlService';
|
||||
|
||||
type CachedMatch3DModelBytes = {
|
||||
@@ -117,6 +120,30 @@ export function getMatch3DGeneratedImageAssetSources(
|
||||
];
|
||||
}
|
||||
|
||||
export function getMatch3DGeneratedRuntimeUiAssetSources(
|
||||
assets: readonly Match3DGeneratedItemAsset[] = [],
|
||||
backgroundAsset: Match3DGeneratedBackgroundAsset | null | undefined = null,
|
||||
) {
|
||||
return [
|
||||
...new Set(
|
||||
[
|
||||
backgroundAsset?.imageObjectKey,
|
||||
backgroundAsset?.imageSrc,
|
||||
backgroundAsset?.containerImageObjectKey,
|
||||
backgroundAsset?.containerImageSrc,
|
||||
...assets.flatMap((asset) => [
|
||||
asset.backgroundAsset?.imageObjectKey,
|
||||
asset.backgroundAsset?.imageSrc,
|
||||
asset.backgroundAsset?.containerImageObjectKey,
|
||||
asset.backgroundAsset?.containerImageSrc,
|
||||
]),
|
||||
]
|
||||
.map(normalizeMatch3DModelSource)
|
||||
.filter((source) => source.length > 0),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function hasMatch3DGeneratedImageAsset(
|
||||
assets: readonly Match3DGeneratedItemAsset[] | null | undefined,
|
||||
) {
|
||||
@@ -224,6 +251,7 @@ export function mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
return {
|
||||
...asset,
|
||||
itemName: asset.itemName.trim() || fallbackAsset.itemName,
|
||||
itemSize: asset.itemSize ?? fallbackAsset.itemSize ?? null,
|
||||
imageSrc: asset.imageSrc?.trim()
|
||||
? asset.imageSrc
|
||||
: (fallbackAsset.imageSrc ?? null),
|
||||
@@ -376,12 +404,26 @@ export async function preloadMatch3DGeneratedImageAssets(
|
||||
|
||||
export async function preloadMatch3DGeneratedRuntimeAssets(
|
||||
assets: readonly Match3DGeneratedItemAsset[] = [],
|
||||
backgroundAsset: Match3DGeneratedBackgroundAsset | null | undefined = null,
|
||||
options: Omit<Match3DModelBytesOptions, 'signal'> = {},
|
||||
) {
|
||||
// 中文注释:新抓大鹅运行态以 2D 图片为主;3D 模型只作为历史草稿预览兼容。
|
||||
await preloadMatch3DGeneratedImageAssets(
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime(assets),
|
||||
options,
|
||||
const normalizedAssets = normalizeMatch3DGeneratedItemAssetsForRuntime(assets);
|
||||
const sources = [
|
||||
...new Set([
|
||||
...getMatch3DGeneratedImageAssetSources(normalizedAssets),
|
||||
...getMatch3DGeneratedRuntimeUiAssetSources(
|
||||
normalizedAssets,
|
||||
backgroundAsset,
|
||||
),
|
||||
]),
|
||||
];
|
||||
await Promise.allSettled(
|
||||
sources.map((source) =>
|
||||
resolveAssetReadUrl(source, {
|
||||
expireSeconds: options.expireSeconds,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user