@forgeax/engine-sdk 0.5.0-dev.15093.89d287422343 → 0.5.0-dev.15095.c10283ee6c8f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/sdk/AGENTS.md +4 -4
- package/sdk/README.md +3 -2
- package/sdk/docs/DEVKIT.md +11 -4
- package/sdk/package.json +1 -1
- package/sdk/sdk-manifest.json +158 -143
- package/sdk/skills/forgeax-engine-cli/SKILL.md +4 -3
- package/sdk/skills/forgeax-engine-debug/SKILL.md +15 -0
- package/sdk/skills/forgeax-engine-sdk/SDK-AGENTS.md +4 -4
- package/sdk/skills/forgeax-engine-sdk/SDK-README.md +3 -2
- package/sdk/skills/forgeax-engine-sdk/references/browser-capture-and-local-engine.md +2 -2
- package/sdk/templates/empty/package.json +1 -1
- package/sdk/templates/empty/pnpm-lock.yaml +6 -11
- package/sdk/templates/empty/pnpm-workspace.yaml +0 -2
- package/sdk/templates/game-3d/assets/__tests__/uv-primitives.test.ts +88 -0
- package/sdk/templates/game-3d/assets/uv-primitives.pack.ts +155 -0
- package/sdk/templates/game-3d/docs/feedback.md +9 -0
- package/sdk/templates/game-3d/docs/uv-primitives.md +38 -0
- package/sdk/templates/game-3d/package.json +1 -1
- package/sdk/templates/game-3d/pnpm-lock.yaml +6 -11
- package/sdk/templates/game-3d/pnpm-workspace.yaml +0 -2
- package/sdk/templates/game-3d/scripts/runtime-vase-probe.mjs +9 -3
- package/sdk/toolchain/cli-runtime/package.json +2 -2
- package/sdk/toolchain/cli-runtime/pnpm-lock.yaml +12 -17
- package/sdk/toolchain/cli-runtime/pnpm-workspace.yaml +0 -2
|
@@ -199,9 +199,10 @@ document.fonts.ready. Projects must bundle the same Web fonts; system font fallb
|
|
|
199
199
|
parity.
|
|
200
200
|
|
|
201
201
|
Use --deterministic for local/remote pixel or color comparisons. The CLI opens ?forgeaxCapture=1;
|
|
202
|
-
App
|
|
203
|
-
and dispatches forgeax:frame-submitted on Canvas
|
|
204
|
-
for
|
|
202
|
+
App retains the submitted frame facts on canvas.forgeaxFrameProgress after an actual renderer submission
|
|
203
|
+
and dispatches forgeax:frame-submitted on Canvas as a future-only notification. The CLI reads the snapshot
|
|
204
|
+
and waits for a non-flat canvas crop before accepting the game's
|
|
205
|
+
`document.documentElement.dataset.forgeaxCaptureReady = 'true'` checkpoint. Before readiness, the game must pin
|
|
205
206
|
random seed, logical frame, state, and project fonts. Viewport, DPR, and elapsed time alone do not establish parity.
|
|
206
207
|
|
|
207
208
|
For continuous playthroughs, reuse one session; repeated one-shot capture restarts the page and World:
|
|
@@ -39,11 +39,26 @@ Pack path and the existing exact-target capture. One-level Equirect sources do
|
|
|
39
39
|
not expose Renderer-owned IBL roughness mips. Follow RHI Debug first for an
|
|
40
40
|
unexplained rendering failure; see the [View texture gate](../../scripts/ci/README.md#concurrent-local-sessions-and-the-gpu-lease).
|
|
41
41
|
|
|
42
|
+
For View UV layouts or material primitive/navigation failures, run
|
|
43
|
+
`tools/view/scripts/verify-resource-preview-lifecycle.mjs` and inspect its UV and
|
|
44
|
+
material PNGs plus exact target diagnostics. The maintained workspace shard owns
|
|
45
|
+
this real-browser journey. A script syntax error routes to the View probe owner;
|
|
46
|
+
preserve its runtime assertions while repairing startup. Follow RHI Debug first
|
|
47
|
+
for unexplained rendered output.
|
|
48
|
+
|
|
42
49
|
For a blank MCP View with React error #527, run its emitted frontend startup gate
|
|
43
50
|
through the View contracts owner. The MCP builder resolves React and React DOM
|
|
44
51
|
from View's installed dependencies; inspect that owner before changing startup
|
|
45
52
|
deadlines. Continue through the full game-3d/Play/Stop gate after repair.
|
|
46
53
|
|
|
54
|
+
For a View shard reaching its job deadline, inspect emitted stage seconds before
|
|
55
|
+
changing a timeout. Keep Game 3D's stdio and native workspace journeys together
|
|
56
|
+
in its own lane; contracts/plugin/TS share another. Preserve complete JS/TS
|
|
57
|
+
live/cold chains, four actual jobs and all assertions; see the
|
|
58
|
+
[Engine/View integration roster](../../scripts/ci/README.md#failure-recovery).
|
|
59
|
+
An artifact-only transport failure requires a same-source rerun, not a weaker
|
|
60
|
+
success aggregate.
|
|
61
|
+
|
|
47
62
|
For a View startup Host disconnect, read the failed probe's Backend diagnostics
|
|
48
63
|
under `artifacts/view-integration/<probe>/` before assigning a renderer cause.
|
|
49
64
|
The CI owner matches native daemon spawn records to the failed child and retains
|
|
@@ -134,10 +134,10 @@ pnpm exec forgeax project capture --headless true --backend software --require-u
|
|
|
134
134
|
|
|
135
135
|
This uses source-development assets and browser compositor screenshots; retain adapter/error sidecar evidence.
|
|
136
136
|
For cross-machine comparison, add --deterministic and pin game state in forgeaxCapture mode.
|
|
137
|
-
App
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
App retains submitted and completed facts on `canvas.forgeaxFrameProgress` and dispatches
|
|
138
|
+
the Canvas `forgeax:frame-submitted` event as a future-only notification. Capture reads the
|
|
139
|
+
snapshot and waits for non-flat Canvas pixels, then the game's forgeaxCaptureReady checkpoint.
|
|
140
|
+
Elapsed waiting is
|
|
141
141
|
only extra settling, not a deterministic frame or substitute for pixel evidence.
|
|
142
142
|
Use a Node/Bun SDK command-client scenario for input, assertions, and multiple checkpoints in one session.
|
|
143
143
|
Open browser once, drive native session.page, and call session.capture(name)
|
|
@@ -92,8 +92,9 @@ Bundle the same Web fonts; do not rely on matching system fonts.
|
|
|
92
92
|
For cross-machine color regression, use --deterministic and ?forgeaxCapture=1. Games pin random
|
|
93
93
|
seed/logical frame/fonts and set readiness only after Canvas/UI stabilize:
|
|
94
94
|
document.documentElement.dataset.forgeaxCaptureReady = 'true'. App also publishes
|
|
95
|
-
|
|
96
|
-
forgeax:frame-submitted
|
|
95
|
+
`canvas.forgeaxFrameProgress.submitted` after actual Renderer submission and dispatches
|
|
96
|
+
`forgeax:frame-submitted` as a future-only notification. The CLI reads the canvas snapshot and
|
|
97
|
+
waits for non-flat output before the game marker.
|
|
97
98
|
wait-ms adds settling after these handshakes; it does not identify a deterministic frame.
|
|
98
99
|
|
|
99
100
|
For continuous captures, keep one Vite/Xvfb/Chrome/Page/World session and use named ready markers
|
|
@@ -11,14 +11,14 @@ The readiness chain is explicit:
|
|
|
11
11
|
|
|
12
12
|
```mermaid
|
|
13
13
|
flowchart LR
|
|
14
|
-
A[Renderer submits frame] --> B[
|
|
14
|
+
A[Renderer submits frame] --> B[canvas.forgeaxFrameProgress.submitted]
|
|
15
15
|
B --> C[non-flat Canvas witness]
|
|
16
16
|
C --> D[optional named game checkpoint]
|
|
17
17
|
D --> E[compositor settle]
|
|
18
18
|
E --> F[full viewport PNG and run.json]
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The Engine
|
|
21
|
+
The Engine retains submitted and completed frame facts on the owner Canvas as `forgeaxFrameProgress`. Its `forgeax:frame-submitted` Canvas event is a future-only notification; it does not replay an earlier submission. A deterministic game may additionally set `data-forgeax-capture-ready` to `true` or a checkpoint name. `--wait-ms` is only extra compositor settle time after those signals; it is not the game-readiness authority.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
forgeax project capture --backend auto --require-ui --deterministic \
|
|
@@ -167,8 +167,8 @@ importers:
|
|
|
167
167
|
.:
|
|
168
168
|
dependencies:
|
|
169
169
|
'@forgeax/engine':
|
|
170
|
-
specifier: 0.5.0-dev.
|
|
171
|
-
version: 0.5.0-dev.
|
|
170
|
+
specifier: 0.5.0-dev.15095.c10283ee6c8f
|
|
171
|
+
version: 0.5.0-dev.15095.c10283ee6c8f(48eb1dc1d1b0f9a6a9ad276467f438dc)
|
|
172
172
|
devDependencies:
|
|
173
173
|
'@types/node':
|
|
174
174
|
specifier: 20.19.40
|
|
@@ -528,8 +528,8 @@ packages:
|
|
|
528
528
|
'@fontsource-variable/noto-sans-sc@5.3.0':
|
|
529
529
|
resolution: {integrity: sha512-lNar1dF7Ik/lHNPo/7JWG0TolXY29LtsqYgMvEysooZ5bsO9uH4shJmRrwyJ3PjyTPljhpMJEK0jDuLSU4vJ1w==, tarball: https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.3.0.tgz}
|
|
530
530
|
|
|
531
|
-
'@forgeax/engine@0.5.0-dev.
|
|
532
|
-
resolution: {integrity: sha512
|
|
531
|
+
'@forgeax/engine@0.5.0-dev.15095.c10283ee6c8f':
|
|
532
|
+
resolution: {integrity: sha512-Nqhf+87EZBjq+TrsevtiTbAKYCHu2ys9gG3f6eMkINluzE0c58u4EWRkthA1iy35k0xfMUopuVbEDEbuY26aog==}
|
|
533
533
|
hasBin: true
|
|
534
534
|
bundledDependencies:
|
|
535
535
|
- '@deepseek-ai/cordis'
|
|
@@ -611,6 +611,7 @@ packages:
|
|
|
611
611
|
- '@forgeax/engine-vite-plugin-rhi-debug'
|
|
612
612
|
- '@forgeax/engine-vite-plugin-shader'
|
|
613
613
|
- '@forgeax/engine-wgpu-wasm'
|
|
614
|
+
- core-js-pure
|
|
614
615
|
|
|
615
616
|
'@jridgewell/sourcemap-codec@1.6.0':
|
|
616
617
|
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz}
|
|
@@ -861,9 +862,6 @@ packages:
|
|
|
861
862
|
convert-source-map@2.0.0:
|
|
862
863
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, tarball: https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz}
|
|
863
864
|
|
|
864
|
-
core-js-pure@3.50.0:
|
|
865
|
-
resolution: {integrity: sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==, tarball: https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz}
|
|
866
|
-
|
|
867
865
|
css-select@7.0.0:
|
|
868
866
|
resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==, tarball: https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz}
|
|
869
867
|
engines: {node: '>=20.19.0'}
|
|
@@ -1623,7 +1621,7 @@ snapshots:
|
|
|
1623
1621
|
|
|
1624
1622
|
'@fontsource-variable/noto-sans-sc@5.3.0': {}
|
|
1625
1623
|
|
|
1626
|
-
'@forgeax/engine@0.5.0-dev.
|
|
1624
|
+
'@forgeax/engine@0.5.0-dev.15095.c10283ee6c8f(48eb1dc1d1b0f9a6a9ad276467f438dc)':
|
|
1627
1625
|
dependencies:
|
|
1628
1626
|
'@bokuweb/zstd-wasm': 0.0.27
|
|
1629
1627
|
'@deepseek-ai/cosmokit': 1.8.5
|
|
@@ -1645,7 +1643,6 @@ snapshots:
|
|
|
1645
1643
|
acorn: 8.16.0
|
|
1646
1644
|
ajv: 8.20.0
|
|
1647
1645
|
chokidar: 4.0.3
|
|
1648
|
-
core-js-pure: 3.50.0
|
|
1649
1646
|
css-tree: 3.2.1
|
|
1650
1647
|
draco3dgltf: 1.5.7
|
|
1651
1648
|
earcut: 3.2.4
|
|
@@ -1914,8 +1911,6 @@ snapshots:
|
|
|
1914
1911
|
|
|
1915
1912
|
convert-source-map@2.0.0: {}
|
|
1916
1913
|
|
|
1917
|
-
core-js-pure@3.50.0: {}
|
|
1918
|
-
|
|
1919
1914
|
css-select@7.0.0:
|
|
1920
1915
|
dependencies:
|
|
1921
1916
|
boolbase: 2.0.0
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { MeshAsset } from '@forgeax/engine/types';
|
|
3
|
+
import pack from '../uv-primitives.pack.ts';
|
|
4
|
+
|
|
5
|
+
async function meshes(): Promise<Record<string, MeshAsset>> {
|
|
6
|
+
const result = await pack.build(undefined as never);
|
|
7
|
+
if (!result.ok) throw result.error;
|
|
8
|
+
return result.value as Record<string, MeshAsset>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Point = readonly [number, number];
|
|
12
|
+
function overlap(a: readonly Point[], b: readonly Point[]): boolean {
|
|
13
|
+
for (const triangle of [a, b]) {
|
|
14
|
+
for (let edge = 0; edge < 3; edge += 1) {
|
|
15
|
+
const start = triangle[edge]!;
|
|
16
|
+
const end = triangle[(edge + 1) % 3]!;
|
|
17
|
+
const axis = [end[1] - start[1], start[0] - end[0]];
|
|
18
|
+
const project = (points: readonly Point[]) => points.map(([u, v]) => u * axis[0]! + v * axis[1]!);
|
|
19
|
+
const pa = project(a);
|
|
20
|
+
const pb = project(b);
|
|
21
|
+
if (Math.min(Math.max(...pa), Math.max(...pb)) - Math.max(Math.min(...pa), Math.min(...pb)) <= 1e-9) return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('UV primitives authoring', () => {
|
|
28
|
+
it('produces exactly three deterministic fixed meshes', async () => {
|
|
29
|
+
const first = await meshes();
|
|
30
|
+
expect(Object.keys(first)).toEqual(['mesh/triangular-ramp', 'mesh/half-cylinder', 'mesh/quarter-cylinder']);
|
|
31
|
+
expect(await meshes()).toEqual(first);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
for (const [key, count, volume, islandCount] of [
|
|
35
|
+
['mesh/triangular-ramp', 8, 4, 5],
|
|
36
|
+
['mesh/half-cylinder', 132, 32 * Math.sin(Math.PI / 32), 4],
|
|
37
|
+
['mesh/quarter-cylinder', 68, 16 * Math.sin(Math.PI / 32), 5],
|
|
38
|
+
] as const) {
|
|
39
|
+
it(`${key}: closed, outward, nondegenerate, with separated proportional UV islands`, async () => {
|
|
40
|
+
const mesh = (await meshes())[key]!;
|
|
41
|
+
const positions = mesh.attributes.position as Float32Array;
|
|
42
|
+
const normals = mesh.attributes.normal as Float32Array;
|
|
43
|
+
const uv = mesh.attributes.uv as Float32Array;
|
|
44
|
+
const indices = mesh.indices!;
|
|
45
|
+
expect(indices.length / 3).toBe(count);
|
|
46
|
+
for (const value of uv) expect(value).toBeGreaterThan(0);
|
|
47
|
+
for (const value of uv) expect(value).toBeLessThan(1);
|
|
48
|
+
const triangles: Point[][] = [];
|
|
49
|
+
const edges = new Map<string, number>();
|
|
50
|
+
const density: number[] = [];
|
|
51
|
+
const parent = Array.from({ length: positions.length / 3 }, (_, i) => i);
|
|
52
|
+
const root = (i: number): number => parent[i] === i ? i : root(parent[i]!);
|
|
53
|
+
const pointKey = (i: number) => Array.from(positions.slice(i * 3, i * 3 + 3), (v) => v.toFixed(5)).join(',');
|
|
54
|
+
let signedVolume = 0;
|
|
55
|
+
for (let index = 0; index < indices.length; index += 3) {
|
|
56
|
+
const ids = [indices[index]!, indices[index + 1]!, indices[index + 2]!];
|
|
57
|
+
const [a, b, c] = ids.map((id) => Array.from(positions.slice(id * 3, id * 3 + 3)));
|
|
58
|
+
const ab = b!.map((v, axis) => v - a![axis]!);
|
|
59
|
+
const ac = c!.map((v, axis) => v - a![axis]!);
|
|
60
|
+
const cross = [ab[1]! * ac[2]! - ab[2]! * ac[1]!, ab[2]! * ac[0]! - ab[0]! * ac[2]!, ab[0]! * ac[1]! - ab[1]! * ac[0]!];
|
|
61
|
+
const area = Math.hypot(...cross) / 2;
|
|
62
|
+
expect(area).toBeGreaterThan(1e-6);
|
|
63
|
+
expect(cross.reduce((sum, v, axis) => sum + v * normals[ids[0]! * 3 + axis]!, 0)).toBeGreaterThan(0);
|
|
64
|
+
signedVolume += (a![0]! * (b![1]! * c![2]! - b![2]! * c![1]!) + a![1]! * (b![2]! * c![0]! - b![0]! * c![2]!) + a![2]! * (b![0]! * c![1]! - b![1]! * c![0]!)) / 6;
|
|
65
|
+
const triangle: Point[] = ids.map((id) => [uv[id * 2]!, uv[id * 2 + 1]!]);
|
|
66
|
+
const [u, v, w] = triangle;
|
|
67
|
+
const uvArea = Math.abs((v![0] - u![0]) * (w![1] - u![1]) - (v![1] - u![1]) * (w![0] - u![0])) / 2;
|
|
68
|
+
expect(uvArea).toBeGreaterThan(1e-7);
|
|
69
|
+
density.push(uvArea / area);
|
|
70
|
+
triangles.push(triangle);
|
|
71
|
+
for (let edge = 0; edge < 3; edge += 1) {
|
|
72
|
+
const current = ids[edge]!;
|
|
73
|
+
const next = ids[(edge + 1) % 3]!;
|
|
74
|
+
parent[root(current)] = root(next);
|
|
75
|
+
const edgeKey = [pointKey(current), pointKey(next)].sort().join('|');
|
|
76
|
+
edges.set(edgeKey, (edges.get(edgeKey) ?? 0) + 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
expect(signedVolume).toBeCloseTo(volume, 5);
|
|
80
|
+
expect([...edges.values()].every((uses) => uses === 2)).toBe(true);
|
|
81
|
+
expect(new Set(parent.map((_, i) => root(i))).size).toBe(islandCount);
|
|
82
|
+
expect(Math.max(...density) / Math.min(...density)).toBeLessThan(1.003);
|
|
83
|
+
for (let a = 0; a < triangles.length; a += 1) {
|
|
84
|
+
for (let b = a + 1; b < triangles.length; b += 1) expect(overlap(triangles[a]!, triangles[b]!)).toBe(false);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { meshFromInterleaved } from '@forgeax/engine/geometry';
|
|
2
|
+
import { definePack, definePackageId } from '@forgeax/engine/pack/source';
|
|
3
|
+
import { ok } from '@forgeax/engine/types';
|
|
4
|
+
import { assetGuid, PACKAGE_IDS } from './shared/asset-refs.ts';
|
|
5
|
+
|
|
6
|
+
type Point = readonly [number, number, number];
|
|
7
|
+
type Uv = readonly [number, number];
|
|
8
|
+
interface Island {
|
|
9
|
+
vertices: number[];
|
|
10
|
+
indices: number[];
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const HEIGHT = 2;
|
|
16
|
+
const RADIUS = 1;
|
|
17
|
+
const GUTTER = 0.12;
|
|
18
|
+
|
|
19
|
+
function face(points: readonly Point[], normal: Point, uvs: readonly Uv[], width: number, height: number): Island {
|
|
20
|
+
return {
|
|
21
|
+
vertices: points.flatMap((point, index) => [...point, ...normal, ...uvs[index]!]),
|
|
22
|
+
indices: points.length === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3],
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rectangleUvs = (width: number, height: number): Uv[] => [[0, 0], [width, 0], [width, height], [0, height]];
|
|
29
|
+
|
|
30
|
+
function ramp(): Island[] {
|
|
31
|
+
const a: Point = [-1, 0, -1];
|
|
32
|
+
const b: Point = [1, 0, -1];
|
|
33
|
+
const c: Point = [1, 0, 1];
|
|
34
|
+
const d: Point = [-1, 0, 1];
|
|
35
|
+
const e: Point = [-1, HEIGHT, 1];
|
|
36
|
+
const f: Point = [1, HEIGHT, 1];
|
|
37
|
+
const slope = Math.hypot(2, HEIGHT);
|
|
38
|
+
return [
|
|
39
|
+
face([a, b, c, d], [0, -1, 0], rectangleUvs(2, 2), 2, 2),
|
|
40
|
+
face([a, b, f, e], [0, 2 / slope, -HEIGHT / slope], rectangleUvs(2, slope), 2, slope),
|
|
41
|
+
face([d, c, f, e], [0, 0, 1], rectangleUvs(2, HEIGHT), 2, HEIGHT),
|
|
42
|
+
face([a, d, e], [-1, 0, 0], [[0, 0], [2, 0], [2, HEIGHT]], 2, HEIGHT),
|
|
43
|
+
face([b, c, f], [1, 0, 0], [[0, 0], [2, 0], [2, HEIGHT]], 2, HEIGHT),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cylinderSector(angle: number, segments: number): Island[] {
|
|
48
|
+
const arcLength = RADIUS * angle;
|
|
49
|
+
const strip: Island = { vertices: [], indices: [], width: arcLength, height: HEIGHT };
|
|
50
|
+
const capWidth = angle === Math.PI ? 2 * RADIUS : RADIUS;
|
|
51
|
+
const caps = [0, HEIGHT].map((y): Island => {
|
|
52
|
+
const normal: Point = [0, y === 0 ? -1 : 1, 0];
|
|
53
|
+
const offsetX = angle === Math.PI ? RADIUS : 0;
|
|
54
|
+
const vertices = [0, y, 0, ...normal, offsetX, 0];
|
|
55
|
+
const indices: number[] = [];
|
|
56
|
+
for (let step = 0; step <= segments; step += 1) {
|
|
57
|
+
const theta = angle * step / segments;
|
|
58
|
+
const x = RADIUS * Math.cos(theta);
|
|
59
|
+
const z = RADIUS * Math.sin(theta);
|
|
60
|
+
vertices.push(x, y, z, ...normal, offsetX + x, z);
|
|
61
|
+
if (step > 0) indices.push(0, step, step + 1);
|
|
62
|
+
}
|
|
63
|
+
return { vertices, indices, width: capWidth, height: RADIUS };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Each angular column is shared along the curved surface, including its UV seam endpoints.
|
|
67
|
+
for (let step = 0; step <= segments; step += 1) {
|
|
68
|
+
const theta = angle * step / segments;
|
|
69
|
+
const x = Math.cos(theta);
|
|
70
|
+
const z = Math.sin(theta);
|
|
71
|
+
for (const y of [0, HEIGHT]) {
|
|
72
|
+
strip.vertices.push(RADIUS * x, y, RADIUS * z, x, 0, z, RADIUS * theta, y);
|
|
73
|
+
}
|
|
74
|
+
if (step < segments) {
|
|
75
|
+
const current = step * 2;
|
|
76
|
+
strip.indices.push(current, current + 2, current + 3, current, current + 3, current + 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const diameter: Island = { vertices: [], indices: [0, 2, 3, 0, 3, 1, 2, 4, 5, 2, 5, 3], width: 2 * RADIUS, height: HEIGHT };
|
|
81
|
+
for (const x of [RADIUS, 0, -RADIUS]) {
|
|
82
|
+
for (const y of [0, HEIGHT]) diameter.vertices.push(x, y, 0, 0, 0, -1, RADIUS - x, y);
|
|
83
|
+
}
|
|
84
|
+
const cuts = angle === Math.PI
|
|
85
|
+
? [diameter]
|
|
86
|
+
: [
|
|
87
|
+
face([[0, 0, 0], [RADIUS, 0, 0], [RADIUS, HEIGHT, 0], [0, HEIGHT, 0]], [0, 0, -1], rectangleUvs(RADIUS, HEIGHT), RADIUS, HEIGHT),
|
|
88
|
+
face([[0, 0, 0], [0, 0, RADIUS], [0, HEIGHT, RADIUS], [0, HEIGHT, 0]], [-1, 0, 0], rectangleUvs(RADIUS, HEIGHT), RADIUS, HEIGHT),
|
|
89
|
+
];
|
|
90
|
+
return [...caps, strip, ...cuts];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildMesh(islands: readonly Island[], rowWidth: number) {
|
|
94
|
+
const vertices: number[] = [];
|
|
95
|
+
const indices: number[] = [];
|
|
96
|
+
let x = GUTTER;
|
|
97
|
+
let y = GUTTER;
|
|
98
|
+
let rowHeight = 0;
|
|
99
|
+
let atlasWidth = 0;
|
|
100
|
+
for (const island of islands) {
|
|
101
|
+
if (x > GUTTER && x + island.width + GUTTER > rowWidth) {
|
|
102
|
+
x = GUTTER;
|
|
103
|
+
y += rowHeight + GUTTER;
|
|
104
|
+
rowHeight = 0;
|
|
105
|
+
}
|
|
106
|
+
const base = vertices.length / 8;
|
|
107
|
+
for (let offset = 0; offset < island.vertices.length; offset += 8) {
|
|
108
|
+
vertices.push(...island.vertices.slice(offset, offset + 6), island.vertices[offset + 6]! + x, island.vertices[offset + 7]! + y);
|
|
109
|
+
}
|
|
110
|
+
indices.push(...island.indices.map((index) => index + base));
|
|
111
|
+
x += island.width + GUTTER;
|
|
112
|
+
atlasWidth = Math.max(atlasWidth, x);
|
|
113
|
+
rowHeight = Math.max(rowHeight, island.height);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// One uniform scale preserves texel density and the circular cap shapes across all islands.
|
|
117
|
+
const atlasHeight = y + rowHeight + GUTTER;
|
|
118
|
+
const extent = Math.max(atlasWidth, atlasHeight);
|
|
119
|
+
for (let offset = 0; offset < vertices.length; offset += 8) {
|
|
120
|
+
vertices[offset + 6] = (vertices[offset + 6]! + (extent - atlasWidth) / 2) / extent;
|
|
121
|
+
vertices[offset + 7] = (vertices[offset + 7]! + (extent - atlasHeight) / 2) / extent;
|
|
122
|
+
}
|
|
123
|
+
for (let offset = 0; offset < indices.length; offset += 3) {
|
|
124
|
+
const a = indices[offset]! * 8;
|
|
125
|
+
const b = indices[offset + 1]! * 8;
|
|
126
|
+
const c = indices[offset + 2]! * 8;
|
|
127
|
+
const ab = [0, 1, 2].map((axis) => vertices[b + axis]! - vertices[a + axis]!);
|
|
128
|
+
const ac = [0, 1, 2].map((axis) => vertices[c + axis]! - vertices[a + axis]!);
|
|
129
|
+
const cross = [ab[1]! * ac[2]! - ab[2]! * ac[1]!, ab[2]! * ac[0]! - ab[0]! * ac[2]!, ab[0]! * ac[1]! - ab[1]! * ac[0]!];
|
|
130
|
+
const alignment = cross.reduce((sum, value, axis) => sum + value * (vertices[a + 3 + axis]! + vertices[b + 3 + axis]! + vertices[c + 3 + axis]!), 0);
|
|
131
|
+
if (alignment < 0) [indices[offset + 1], indices[offset + 2]] = [indices[offset + 2]!, indices[offset + 1]!];
|
|
132
|
+
}
|
|
133
|
+
const result = meshFromInterleaved(new Float32Array(vertices), new Uint32Array(indices));
|
|
134
|
+
if (!result.ok) return result;
|
|
135
|
+
return ok({ ...result.value, materialSlots: [{ slotName: 'Surface', defaultMaterial: assetGuid(PACKAGE_IDS.materials, 'material/white') }] });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default definePack({
|
|
139
|
+
schemaVersion: '2.0.0',
|
|
140
|
+
packageId: definePackageId('01a15d30-7190-7000-8000-000000000000'),
|
|
141
|
+
name: 'Game 3D / UV Primitives',
|
|
142
|
+
build: () => {
|
|
143
|
+
const triangularRamp = buildMesh(ramp(), 6.5);
|
|
144
|
+
if (!triangularRamp.ok) return triangularRamp;
|
|
145
|
+
const halfCylinder = buildMesh(cylinderSector(Math.PI, 32), 4.5);
|
|
146
|
+
if (!halfCylinder.ok) return halfCylinder;
|
|
147
|
+
const quarterCylinder = buildMesh(cylinderSector(Math.PI / 2, 16), 3.5);
|
|
148
|
+
if (!quarterCylinder.ok) return quarterCylinder;
|
|
149
|
+
return ok({
|
|
150
|
+
'mesh/triangular-ramp': triangularRamp.value,
|
|
151
|
+
'mesh/half-cylinder': halfCylinder.value,
|
|
152
|
+
'mesh/quarter-cylinder': quarterCylinder.value,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
});
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
记录开发期间确认的 Engine、SDK、模板、构建、资产、运行时或浏览器问题。该文件会和根
|
|
4
4
|
`README.md` 一起原样进入 Web ZIP;不要写入 token、密码、私有 URL 或其他敏感信息。
|
|
5
5
|
|
|
6
|
+
### 2026-10-21 — Local worktree project check cannot resolve template lockfile
|
|
7
|
+
|
|
8
|
+
- Scope: local template dependency verification
|
|
9
|
+
- Reproduction: run `node packages/devkit/dist/cli.mjs project check --root templates/game-3d --json` from the mesh-uv-preview Engine worktree.
|
|
10
|
+
- Expected: verify the installed Game 3D dependency closure.
|
|
11
|
+
- Actual: `project-pnpm-record-unreadable` reports a missing `templates/pnpm-lock.yaml`; the verifier selects the containing templates directory for its pnpm record.
|
|
12
|
+
- Evidence: the same project's TypeScript check and all 44 tests pass; the native View Catalog publishes all three `uv-primitives.pack.ts` outputs and renders the half-cylinder with its UV0 layout.
|
|
13
|
+
- Status: open — local project-check environment limitation; no dependency files or verification policy were changed to bypass it.
|
|
14
|
+
|
|
6
15
|
### 2026-10-20 — Showcase contact shadows lost after inheriting camera-wide CSM coverage
|
|
7
16
|
|
|
8
17
|
- Scope: template
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# UV primitives
|
|
2
|
+
|
|
3
|
+
`assets/uv-primitives.pack.ts` is a fixed, build-time procedural Pack with exactly
|
|
4
|
+
three mesh outputs. It uses the project's existing white material and the normal
|
|
5
|
+
Pack producer, Catalog and mesh preview path. No runtime generator is installed.
|
|
6
|
+
|
|
7
|
+
| Source key | Shape | Triangles | UV0 islands |
|
|
8
|
+
|:--|:--|--:|--:|
|
|
9
|
+
| `mesh/triangular-ramp` | 2 x 2 x 2 triangular prism | 8 | 5 |
|
|
10
|
+
| `mesh/half-cylinder` | Radius 1, height 2, 180 degrees, 32 arc segments | 132 | 4 |
|
|
11
|
+
| `mesh/quarter-cylinder` | Radius 1, height 2, 90 degrees, 16 arc segments | 68 | 5 |
|
|
12
|
+
|
|
13
|
+
Y is up. Cylindrical caps retain their semicircle/quarter-circle outlines and
|
|
14
|
+
radial triangle fans. The curved surface unwraps by arc length into a continuous
|
|
15
|
+
rectangular strip with matching angular columns. Cut faces have separate planar
|
|
16
|
+
islands; the half-cylinder's diameter is split at the cap center to keep the solid
|
|
17
|
+
watertight. Normals are smooth on the arc and hard at cap/cut boundaries.
|
|
18
|
+
|
|
19
|
+
All UV0 islands are inside 0-1 without overlap, with one uniform physical scale
|
|
20
|
+
and 0.12 model-unit gutters before normalization. There is no extra UV channel:
|
|
21
|
+
select **UV channel 0** in View. The small difference between curved strip and
|
|
22
|
+
faceted triangle area is bounded by the 5.625-degree angular sampling.
|
|
23
|
+
|
|
24
|
+
The layout follows the seam/unwrapping/island packing principles described in
|
|
25
|
+
[Epic's UV tools documentation](https://dev.epicgames.com/documentation/en-us/unreal-engine/uvs-category-in-unreal-engine).
|
|
26
|
+
The reference images guide the shape and topology, not stored geometry data.
|
|
27
|
+
|
|
28
|
+
Template disposition: retain the existing runtime/controller, player/animation,
|
|
29
|
+
scene composition, materials, lighting, UI and template tests because this task
|
|
30
|
+
adds inspection assets to the Game 3D capability reference, not a new game.
|
|
31
|
+
Only the three requested meshes and their tests/documentation are added.
|
|
32
|
+
|
|
33
|
+
Local validation on 2026-10-21: all 44 project tests and `tsc --noEmit` pass.
|
|
34
|
+
The native producer publishes all three outputs in the live Game 3D Catalog;
|
|
35
|
+
View renders the half-cylinder and its four UV0 islands. `asset verify` admits
|
|
36
|
+
the Pack source identity, but that command alone does not execute generation.
|
|
37
|
+
The separate `project check` dependency-record limitation is recorded in
|
|
38
|
+
`feedback.md`.
|
|
@@ -167,8 +167,8 @@ importers:
|
|
|
167
167
|
.:
|
|
168
168
|
dependencies:
|
|
169
169
|
'@forgeax/engine':
|
|
170
|
-
specifier: 0.5.0-dev.
|
|
171
|
-
version: 0.5.0-dev.
|
|
170
|
+
specifier: 0.5.0-dev.15095.c10283ee6c8f
|
|
171
|
+
version: 0.5.0-dev.15095.c10283ee6c8f(48eb1dc1d1b0f9a6a9ad276467f438dc)
|
|
172
172
|
devDependencies:
|
|
173
173
|
'@types/node':
|
|
174
174
|
specifier: 20.19.40
|
|
@@ -528,8 +528,8 @@ packages:
|
|
|
528
528
|
'@fontsource-variable/noto-sans-sc@5.3.0':
|
|
529
529
|
resolution: {integrity: sha512-lNar1dF7Ik/lHNPo/7JWG0TolXY29LtsqYgMvEysooZ5bsO9uH4shJmRrwyJ3PjyTPljhpMJEK0jDuLSU4vJ1w==, tarball: https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.3.0.tgz}
|
|
530
530
|
|
|
531
|
-
'@forgeax/engine@0.5.0-dev.
|
|
532
|
-
resolution: {integrity: sha512
|
|
531
|
+
'@forgeax/engine@0.5.0-dev.15095.c10283ee6c8f':
|
|
532
|
+
resolution: {integrity: sha512-Nqhf+87EZBjq+TrsevtiTbAKYCHu2ys9gG3f6eMkINluzE0c58u4EWRkthA1iy35k0xfMUopuVbEDEbuY26aog==}
|
|
533
533
|
hasBin: true
|
|
534
534
|
bundledDependencies:
|
|
535
535
|
- '@deepseek-ai/cordis'
|
|
@@ -611,6 +611,7 @@ packages:
|
|
|
611
611
|
- '@forgeax/engine-vite-plugin-rhi-debug'
|
|
612
612
|
- '@forgeax/engine-vite-plugin-shader'
|
|
613
613
|
- '@forgeax/engine-wgpu-wasm'
|
|
614
|
+
- core-js-pure
|
|
614
615
|
|
|
615
616
|
'@jridgewell/sourcemap-codec@1.6.0':
|
|
616
617
|
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz}
|
|
@@ -861,9 +862,6 @@ packages:
|
|
|
861
862
|
convert-source-map@2.0.0:
|
|
862
863
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, tarball: https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz}
|
|
863
864
|
|
|
864
|
-
core-js-pure@3.50.0:
|
|
865
|
-
resolution: {integrity: sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==, tarball: https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz}
|
|
866
|
-
|
|
867
865
|
css-select@7.0.0:
|
|
868
866
|
resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==, tarball: https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz}
|
|
869
867
|
engines: {node: '>=20.19.0'}
|
|
@@ -1623,7 +1621,7 @@ snapshots:
|
|
|
1623
1621
|
|
|
1624
1622
|
'@fontsource-variable/noto-sans-sc@5.3.0': {}
|
|
1625
1623
|
|
|
1626
|
-
'@forgeax/engine@0.5.0-dev.
|
|
1624
|
+
'@forgeax/engine@0.5.0-dev.15095.c10283ee6c8f(48eb1dc1d1b0f9a6a9ad276467f438dc)':
|
|
1627
1625
|
dependencies:
|
|
1628
1626
|
'@bokuweb/zstd-wasm': 0.0.27
|
|
1629
1627
|
'@deepseek-ai/cosmokit': 1.8.5
|
|
@@ -1645,7 +1643,6 @@ snapshots:
|
|
|
1645
1643
|
acorn: 8.16.0
|
|
1646
1644
|
ajv: 8.20.0
|
|
1647
1645
|
chokidar: 4.0.3
|
|
1648
|
-
core-js-pure: 3.50.0
|
|
1649
1646
|
css-tree: 3.2.1
|
|
1650
1647
|
draco3dgltf: 1.5.7
|
|
1651
1648
|
earcut: 3.2.4
|
|
@@ -1914,8 +1911,6 @@ snapshots:
|
|
|
1914
1911
|
|
|
1915
1912
|
convert-source-map@2.0.0: {}
|
|
1916
1913
|
|
|
1917
|
-
core-js-pure@3.50.0: {}
|
|
1918
|
-
|
|
1919
1914
|
css-select@7.0.0:
|
|
1920
1915
|
dependencies:
|
|
1921
1916
|
boolbase: 2.0.0
|
|
@@ -5,9 +5,15 @@ import { join } from 'node:path';
|
|
|
5
5
|
/** Shared by Preview and the generated DevKit project's main/Worker browser gates. */
|
|
6
6
|
export async function probeRuntimeVase(page, read, evidenceDir, capture = options => page.screenshot(options)) {
|
|
7
7
|
const settleRender = async () => {
|
|
8
|
-
const before = await page.evaluate(
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const before = await page.evaluate(
|
|
9
|
+
() => document.querySelector('canvas')?.forgeaxFrameProgress?.submitted?.frameId ?? 0,
|
|
10
|
+
);
|
|
11
|
+
await page.waitForFunction(
|
|
12
|
+
(previousFrameId) =>
|
|
13
|
+
(document.querySelector('canvas')?.forgeaxFrameProgress?.submitted?.frameId ?? 0) >= previousFrameId + 2,
|
|
14
|
+
before,
|
|
15
|
+
{ timeout: 60_000 },
|
|
16
|
+
);
|
|
11
17
|
};
|
|
12
18
|
const panel = page.getByRole('region', { name: 'Runtime vase' });
|
|
13
19
|
await panel.waitFor({ timeout: 120_000 });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@12.10.1",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@forgeax/engine": "0.5.0-dev.
|
|
8
|
-
"@forgeax/view": "0.5.0-dev.
|
|
7
|
+
"@forgeax/engine": "0.5.0-dev.15095.c10283ee6c8f",
|
|
8
|
+
"@forgeax/view": "0.5.0-dev.15095.c10283ee6c8f"
|
|
9
9
|
}
|
|
10
10
|
}
|