@forgeax/engine-picking 0.1.3 → 0.1.6

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.
@@ -13,24 +13,25 @@
13
13
  // TDD red phase (w7): pickVertex full-scene tests covering AC-02 (three-state
14
14
  // return) + R-2 (AABB coarse cull multi-entity) + limit > available vertices.
15
15
 
16
+ import { AssetRegistry, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
16
17
  import { type EntityHandle, World } from '@forgeax/engine-ecs';
17
- import { createPrimitiveMesh } from '@forgeax/engine-geometry';
18
18
  import { mat4, ray, type Vec3Like, vec2, vec3 } from '@forgeax/engine-math';
19
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
19
20
  import {
20
21
  CAMERA_PROJECTION_PERSPECTIVE,
21
22
  Camera,
22
23
  cameraProjectionFromF32,
23
- Materials,
24
24
  MeshFilter,
25
25
  MeshRenderer,
26
26
  } from '@forgeax/engine-render';
27
27
  import { propagateTransforms, Transform } from '@forgeax/engine-scene';
28
28
 
29
- import type { Handle, MeshAsset, VertexAttributeMap } from '@forgeax/engine-types';
29
+ import type { Handle, MaterialAsset, MeshAsset, VertexAttributeMap } from '@forgeax/engine-types';
30
+ import { toShared } from '@forgeax/engine-types';
30
31
  import { describe, expect, it } from 'vitest';
31
- import { resolvePickingAsset } from '../asset-port';
32
32
  import { PickError } from '../pick-errors';
33
33
  import { pickVertex, pickVertexOnEntity, type VertexHit } from '../pick-vertex';
34
+ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
34
35
 
35
36
  // ── shared constants ─────────────────────────────────────────────────────
36
37
 
@@ -52,42 +53,29 @@ function translateTransform(x: number, y: number, z: number) {
52
53
  };
53
54
  }
54
55
 
55
- function aabbFromPositions(positions: ArrayLike<number>): Float32Array {
56
- let minX = Number.POSITIVE_INFINITY;
57
- let minY = Number.POSITIVE_INFINITY;
58
- let minZ = Number.POSITIVE_INFINITY;
59
- let maxX = Number.NEGATIVE_INFINITY;
60
- let maxY = Number.NEGATIVE_INFINITY;
61
- let maxZ = Number.NEGATIVE_INFINITY;
62
- for (let i = 0; i < positions.length; i += 3) {
63
- minX = Math.min(minX, positions[i] as number);
64
- minY = Math.min(minY, positions[i + 1] as number);
65
- minZ = Math.min(minZ, positions[i + 2] as number);
66
- maxX = Math.max(maxX, positions[i] as number);
67
- maxY = Math.max(maxY, positions[i + 1] as number);
68
- maxZ = Math.max(maxZ, positions[i + 2] as number);
69
- }
70
- return new Float32Array([minX, minY, minZ, maxX, maxY, maxZ]);
71
- }
72
-
73
56
  interface Scene {
74
57
  world: World;
58
+ assets: AssetRegistry;
75
59
  material: Handle<'MaterialAsset', 'shared'>;
76
60
  }
77
61
 
78
62
  function makeScene(): Scene {
79
63
  const world = new World();
80
- const material = world.internSharedRef('MaterialAsset', Materials.unlit([1, 1, 1, 1]));
81
- return { world, material };
82
- }
83
-
84
- function registerPrimitiveWithoutAabb(
85
- scene: Scene,
86
- kind: 'cube' | 'triangle',
87
- ): Handle<'MeshAsset', 'shared'> {
88
- const primitive = createPrimitiveMesh(kind).unwrap();
89
- const { aabb: _aabb, ...meshWithoutAabb } = primitive;
90
- return scene.world.internSharedRef('MeshAsset', meshWithoutAabb);
64
+ const assets = new AssetRegistry(makeMockShaderRegistry());
65
+ const matResult = assets.catalog<MaterialAsset>(AssetGuid.format(AssetGuid.random()), {
66
+ kind: 'material',
67
+ passes: [
68
+ {
69
+ name: 'Forward',
70
+ program: { module: 'forgeax::default-unlit' },
71
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
72
+ },
73
+ ],
74
+ values: { baseColor: [1, 1, 1] },
75
+ });
76
+ if (!matResult.ok) throw new Error('material catalog failed');
77
+ const material = world.allocSharedRef('MaterialAsset', matResult.value);
78
+ return { world, assets, material };
91
79
  }
92
80
 
93
81
  function spawnPerspectiveCamera(world: World, z: number): EntityHandle {
@@ -126,18 +114,19 @@ function registerTriangle(scene: Scene): Handle<'MeshAsset', 'shared'> {
126
114
  v[i * 12 + 1] = positions[i * 3 + 1] as number;
127
115
  v[i * 12 + 2] = positions[i * 3 + 2] as number;
128
116
  }
129
- return scene.world.allocSharedRef('MeshAsset', {
117
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
130
118
  kind: 'mesh',
131
119
  vertices: v,
132
120
  indices: new Uint16Array([0, 1, 2]),
133
121
  attributes: { position: positions },
134
- aabb: aabbFromPositions(positions),
135
122
  submeshes: [
136
123
  { indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
137
124
  ],
138
125
 
139
126
  materialSlots: [{ slotName: 'Default' }],
140
127
  });
128
+ if (!result.ok) throw new Error(`triangle mesh catalog failed: ${result.error.message}`);
129
+ return scene.world.allocSharedRef('MeshAsset', result.value);
141
130
  }
142
131
 
143
132
  /**
@@ -215,12 +204,11 @@ function registerCube(scene: Scene): Handle<'MeshAsset', 'shared'> {
215
204
  7,
216
205
  4, // left
217
206
  ]);
218
- return scene.world.allocSharedRef('MeshAsset', {
207
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
219
208
  kind: 'mesh',
220
209
  vertices: v,
221
210
  indices,
222
211
  attributes: { position: positions },
223
- aabb: aabbFromPositions(positions),
224
212
  submeshes: [
225
213
  {
226
214
  indexOffset: 0,
@@ -233,6 +221,8 @@ function registerCube(scene: Scene): Handle<'MeshAsset', 'shared'> {
233
221
 
234
222
  materialSlots: [{ slotName: 'Default' }],
235
223
  });
224
+ if (!result.ok) throw new Error(`cube mesh catalog failed: ${result.error.message}`);
225
+ return scene.world.allocSharedRef('MeshAsset', result.value);
236
226
  }
237
227
 
238
228
  /**
@@ -262,18 +252,19 @@ function registerSkinnedTriangle(scene: Scene): Handle<'MeshAsset', 'shared'> {
262
252
  skinIndex,
263
253
  skinWeight,
264
254
  };
265
- return scene.world.allocSharedRef('MeshAsset', {
255
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
266
256
  kind: 'mesh',
267
257
  vertices: v,
268
258
  indices: new Uint16Array([0, 1, 2]),
269
259
  attributes: attrs,
270
- aabb: aabbFromPositions(positions),
271
260
  submeshes: [
272
261
  { indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
273
262
  ],
274
263
 
275
264
  materialSlots: [{ slotName: 'Default' }],
276
265
  });
266
+ if (!result.ok) throw new Error(`skinned mesh catalog failed: ${result.error.message}`);
267
+ return scene.world.allocSharedRef('MeshAsset', result.value);
277
268
  }
278
269
 
279
270
  function spawnMeshEntity(
@@ -498,7 +489,10 @@ describe('pickVertexOnEntity', () => {
498
489
  const entity = spawnMeshEntity(scene, mesh, 0, 0, 0);
499
490
  propagateTransforms(scene.world);
500
491
 
501
- const meshRes = resolvePickingAsset<MeshAsset>(scene.world, mesh);
492
+ const meshRes = resolveAssetHandle<MeshAsset>(
493
+ scene.world,
494
+ toShared<'MeshAsset'>(mesh as unknown as number),
495
+ );
502
496
  if (!meshRes.ok) throw new Error('resolve failed');
503
497
  const position = meshRes.value.attributes.position as Float32Array;
504
498
  const wm = readWorldMatrix(scene.world, entity)!;
@@ -535,7 +529,10 @@ describe('pickVertexOnEntity', () => {
535
529
  const entity = spawnMeshEntity(scene, mesh, 0.1, 0.2, 0);
536
530
  propagateTransforms(scene.world);
537
531
 
538
- const meshRes = resolvePickingAsset<MeshAsset>(scene.world, mesh);
532
+ const meshRes = resolveAssetHandle<MeshAsset>(
533
+ scene.world,
534
+ toShared<'MeshAsset'>(mesh as unknown as number),
535
+ );
539
536
  if (!meshRes.ok) throw new Error('resolve failed');
540
537
  const position = meshRes.value.attributes.position as Float32Array;
541
538
  const wm = readWorldMatrix(scene.world, entity)!;
@@ -726,7 +723,10 @@ describe('pickVertexOnEntity', () => {
726
723
  propagateTransforms(scene.world);
727
724
 
728
725
  // Verify mesh is skinned
729
- const meshRes = resolvePickingAsset<MeshAsset>(scene.world, mesh);
726
+ const meshRes = resolveAssetHandle<MeshAsset>(
727
+ scene.world,
728
+ toShared<'MeshAsset'>(mesh as unknown as number),
729
+ );
730
730
  expect(meshRes.ok).toBe(true);
731
731
  if (!meshRes.ok) throw new Error('resolve failed');
732
732
  const attrs = meshRes.value.attributes;
@@ -750,7 +750,10 @@ describe('pickVertexOnEntity', () => {
750
750
  const entity = spawnMeshEntity(scene, mesh, 0.1, 0.2, 0);
751
751
  propagateTransforms(scene.world);
752
752
 
753
- const meshRes = resolvePickingAsset<MeshAsset>(scene.world, mesh);
753
+ const meshRes = resolveAssetHandle<MeshAsset>(
754
+ scene.world,
755
+ toShared<'MeshAsset'>(mesh as unknown as number),
756
+ );
754
757
  if (!meshRes.ok) throw new Error('resolve failed');
755
758
  const position = meshRes.value.attributes.position as Float32Array;
756
759
  const wm = readWorldMatrix(scene.world, entity)!;
@@ -872,7 +875,10 @@ describe('pickVertexOnEntity', () => {
872
875
  // propagate must be called before pickVertexOnEntity
873
876
  propagateTransforms(scene.world);
874
877
 
875
- const meshRes = resolvePickingAsset<MeshAsset>(scene.world, mesh);
878
+ const meshRes = resolveAssetHandle<MeshAsset>(
879
+ scene.world,
880
+ toShared<'MeshAsset'>(mesh as unknown as number),
881
+ );
876
882
  if (!meshRes.ok) throw new Error('resolve failed');
877
883
  const position = meshRes.value.attributes.position as Float32Array;
878
884
  const wm = readWorldMatrix(scene.world, entity)!;
@@ -1014,7 +1020,7 @@ describe('w6: degradation input + builtin fallback', () => {
1014
1020
  v[i * 12 + 1] = positions[i * 3 + 1] as number;
1015
1021
  v[i * 12 + 2] = positions[i * 3 + 2] as number;
1016
1022
  }
1017
- return scene.world.allocSharedRef('MeshAsset', {
1023
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1018
1024
  kind: 'mesh',
1019
1025
  vertices: v,
1020
1026
  indices: new Uint16Array([0, 1, 2, 3]),
@@ -1031,6 +1037,8 @@ describe('w6: degradation input + builtin fallback', () => {
1031
1037
 
1032
1038
  materialSlots: [{ slotName: 'Default' }],
1033
1039
  });
1040
+ if (!result.ok) throw new Error(`strip mesh catalog failed: ${result.error.message}`);
1041
+ return scene.world.allocSharedRef('MeshAsset', result.value);
1034
1042
  }
1035
1043
 
1036
1044
  it('strip submesh returns undefined (no crash, no approximate hit)', () => {
@@ -1062,7 +1070,7 @@ describe('w6: degradation input + builtin fallback', () => {
1062
1070
  function registerUint16Position(scene: Scene): Handle<'MeshAsset', 'shared'> {
1063
1071
  const positions = new Uint16Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
1064
1072
  const v = new Float32Array(3 * 12);
1065
- return scene.world.allocSharedRef('MeshAsset', {
1073
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1066
1074
  kind: 'mesh',
1067
1075
  vertices: v,
1068
1076
  indices: new Uint16Array([0, 1, 2]),
@@ -1079,11 +1087,13 @@ describe('w6: degradation input + builtin fallback', () => {
1079
1087
 
1080
1088
  materialSlots: [{ slotName: 'Default' }],
1081
1089
  });
1090
+ if (!result.ok) throw new Error(`u16 mesh catalog failed: ${result.error.message}`);
1091
+ return scene.world.allocSharedRef('MeshAsset', result.value);
1082
1092
  }
1083
1093
 
1084
1094
  function registerUndefinedPosition(scene: Scene): Handle<'MeshAsset', 'shared'> {
1085
1095
  const v = new Float32Array(3 * 12);
1086
- return scene.world.allocSharedRef('MeshAsset', {
1096
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1087
1097
  kind: 'mesh',
1088
1098
  vertices: v,
1089
1099
  indices: new Uint16Array([0, 1, 2]),
@@ -1100,6 +1110,8 @@ describe('w6: degradation input + builtin fallback', () => {
1100
1110
 
1101
1111
  materialSlots: [{ slotName: 'Default' }],
1102
1112
  });
1113
+ if (!result.ok) throw new Error(`no-position mesh catalog failed: ${result.error.message}`);
1114
+ return scene.world.allocSharedRef('MeshAsset', result.value);
1103
1115
  }
1104
1116
 
1105
1117
  it('Uint16Array position returns undefined (skip, no crash)', () => {
@@ -1136,7 +1148,7 @@ describe('w6: degradation input + builtin fallback', () => {
1136
1148
  v[i * 12 + 1] = positions[i * 3 + 1] as number;
1137
1149
  v[i * 12 + 2] = positions[i * 3 + 2] as number;
1138
1150
  }
1139
- return scene.world.allocSharedRef('MeshAsset', {
1151
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1140
1152
  kind: 'mesh',
1141
1153
  vertices: v,
1142
1154
  attributes: { position: positions },
@@ -1152,6 +1164,8 @@ describe('w6: degradation input + builtin fallback', () => {
1152
1164
 
1153
1165
  materialSlots: [{ slotName: 'Default' }],
1154
1166
  });
1167
+ if (!result.ok) throw new Error(`indexless mesh catalog failed: ${result.error.message}`);
1168
+ return scene.world.allocSharedRef('MeshAsset', result.value);
1155
1169
  }
1156
1170
 
1157
1171
  it('non-indexed mesh hits vertices (every consecutive 3 vertices = one face)', () => {
@@ -1177,7 +1191,7 @@ describe('w6: degradation input + builtin fallback', () => {
1177
1191
  function registerEmptyMesh(scene: Scene): Handle<'MeshAsset', 'shared'> {
1178
1192
  const positions = new Float32Array(0);
1179
1193
  const v = new Float32Array(0);
1180
- return scene.world.allocSharedRef('MeshAsset', {
1194
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1181
1195
  kind: 'mesh',
1182
1196
  vertices: v,
1183
1197
  attributes: { position: positions },
@@ -1193,6 +1207,8 @@ describe('w6: degradation input + builtin fallback', () => {
1193
1207
 
1194
1208
  materialSlots: [{ slotName: 'Default' }],
1195
1209
  });
1210
+ if (!result.ok) throw new Error(`empty mesh catalog failed: ${result.error.message}`);
1211
+ return scene.world.allocSharedRef('MeshAsset', result.value);
1196
1212
  }
1197
1213
 
1198
1214
  it('0 vertices returns undefined (no crash)', () => {
@@ -1229,7 +1245,7 @@ describe('w6: degradation input + builtin fallback', () => {
1229
1245
  v[i * 12 + 1] = positions[i * 3 + 1] as number;
1230
1246
  v[i * 12 + 2] = positions[i * 3 + 2] as number;
1231
1247
  }
1232
- return scene.world.allocSharedRef('MeshAsset', {
1248
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1233
1249
  kind: 'mesh',
1234
1250
  vertices: v,
1235
1251
  indices: new Uint16Array([0, 1, 2]),
@@ -1246,6 +1262,8 @@ describe('w6: degradation input + builtin fallback', () => {
1246
1262
 
1247
1263
  materialSlots: [{ slotName: 'Default' }],
1248
1264
  });
1265
+ if (!result.ok) throw new Error(`NaN mesh catalog failed: ${result.error.message}`);
1266
+ return scene.world.allocSharedRef('MeshAsset', result.value);
1249
1267
  }
1250
1268
 
1251
1269
  it('NaN vertex coordinates excluded, valid vertices still hit (no NaN propagation)', () => {
@@ -1276,7 +1294,7 @@ describe('w6: degradation input + builtin fallback', () => {
1276
1294
  v[i * 12 + 1] = positions[i * 3 + 1] as number;
1277
1295
  v[i * 12 + 2] = positions[i * 3 + 2] as number;
1278
1296
  }
1279
- const mesh = scene.world.allocSharedRef('MeshAsset', {
1297
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1280
1298
  kind: 'mesh',
1281
1299
  vertices: v,
1282
1300
  indices: new Uint16Array([0, 1, 2]),
@@ -1293,6 +1311,8 @@ describe('w6: degradation input + builtin fallback', () => {
1293
1311
 
1294
1312
  materialSlots: [{ slotName: 'Default' }],
1295
1313
  });
1314
+ if (!result.ok) throw new Error('all-NaN mesh catalog failed');
1315
+ const mesh = scene.world.allocSharedRef('MeshAsset', result.value);
1296
1316
  const entity = spawnMeshEntity(scene, mesh, 0, 0, 0);
1297
1317
  const hit = runPickVertexOnEntity(scene.world, camera, VP / 2, VP / 2, VP, VP, entity) as
1298
1318
  | VertexHit
@@ -1301,17 +1321,16 @@ describe('w6: degradation input + builtin fallback', () => {
1301
1321
  });
1302
1322
  });
1303
1323
 
1304
- // ── AC-07: World-owned primitive mesh without AABB → walk-all-vertices fallback ──
1324
+ // ── AC-07: builtin mesh without AABB → walk-all-vertices fallback ──
1305
1325
 
1306
- describe('AC-07: primitive mesh without AABB — walk-all-vertices fallback', () => {
1307
- it('triangle (no AABB) returns a valid hit via full vertex walk', () => {
1326
+ describe('AC-07: builtin mesh without AABB — walk-all-vertices fallback', () => {
1327
+ it('BUILTIN_TRIANGLE (no AABB) returns a valid hit via full vertex walk', () => {
1308
1328
  const scene = makeScene();
1309
1329
  const camera = spawnPerspectiveCamera(scene.world, 5);
1310
- const mesh = registerPrimitiveWithoutAabb(scene, 'triangle');
1311
1330
  const entity = scene.world
1312
1331
  .spawn(
1313
1332
  { component: Transform, data: translateTransform(0, 0, 0) },
1314
- { component: MeshFilter, data: { assetHandle: mesh } },
1333
+ { component: MeshFilter, data: { assetHandle: toShared<'MeshAsset'>(1) } },
1315
1334
  { component: MeshRenderer, data: { materials: [scene.material] } },
1316
1335
  )
1317
1336
  .unwrap();
@@ -1327,14 +1346,13 @@ describe('w6: degradation input + builtin fallback', () => {
1327
1346
  }
1328
1347
  });
1329
1348
 
1330
- it('cube (no AABB) returns valid vertex hits', () => {
1349
+ it('BUILTIN_CUBE (no AABB) returns valid vertex hits', () => {
1331
1350
  const scene = makeScene();
1332
1351
  const camera = spawnPerspectiveCamera(scene.world, 5);
1333
- const mesh = registerPrimitiveWithoutAabb(scene, 'cube');
1334
1352
  const entity = scene.world
1335
1353
  .spawn(
1336
1354
  { component: Transform, data: translateTransform(0, 0, 0) },
1337
- { component: MeshFilter, data: { assetHandle: mesh } },
1355
+ { component: MeshFilter, data: { assetHandle: toShared<'MeshAsset'>(2) } },
1338
1356
  { component: MeshRenderer, data: { materials: [scene.material] } },
1339
1357
  )
1340
1358
  .unwrap();
@@ -1517,12 +1535,11 @@ describe('w7: pickVertex full-scene', () => {
1517
1535
  v[i * 12 + 1] = positions[i * 3 + 1] as number;
1518
1536
  v[i * 12 + 2] = positions[i * 3 + 2] as number;
1519
1537
  }
1520
- return scene.world.allocSharedRef('MeshAsset', {
1538
+ const result = scene.assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
1521
1539
  kind: 'mesh',
1522
1540
  vertices: v,
1523
1541
  indices: new Uint16Array([0, 1, 2]),
1524
1542
  attributes: { position: positions },
1525
- aabb: aabbFromPositions(positions),
1526
1543
  submeshes: [
1527
1544
  {
1528
1545
  indexOffset: 0,
@@ -1535,6 +1552,8 @@ describe('w7: pickVertex full-scene', () => {
1535
1552
 
1536
1553
  materialSlots: [{ slotName: 'Default' }],
1537
1554
  });
1555
+ if (!result.ok) throw new Error(`small triangle catalog failed: ${result.error.message}`);
1556
+ return scene.world.allocSharedRef('MeshAsset', result.value);
1538
1557
  }
1539
1558
 
1540
1559
  it('entity far from ray is excluded by AABB coarse cull', () => {
@@ -6,7 +6,9 @@
6
6
  // package (AC-203), so these integration tests live in the downstream picking
7
7
  // package that depends on runtime.
8
8
 
9
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
9
10
  import { type EntityHandle, World } from '@forgeax/engine-ecs';
11
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
10
12
  import {
11
13
  CAMERA_PROJECTION_ORTHOGRAPHIC,
12
14
  CAMERA_PROJECTION_PERSPECTIVE,
@@ -15,13 +17,13 @@ import {
15
17
  MeshRenderer,
16
18
  } from '@forgeax/engine-render';
17
19
  import { ChildOf, propagateTransforms, Transform } from '@forgeax/engine-scene';
18
- import type { Handle } from '@forgeax/engine-types';
19
-
20
+ import type { Handle, MaterialAsset, MeshAsset } from '@forgeax/engine-types';
20
21
  import { describe, expect, it } from 'vitest';
21
22
  import { type PickHit, pick } from '../pick';
22
23
  import { PickError } from '../pick-errors';
23
24
  import { pickVertex, pickVertexOnEntity } from '../pick-vertex';
24
25
  import { viewportToWorld } from '../viewport-to-world';
26
+ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
25
27
 
26
28
  // --- from pick.test.ts ---
27
29
  // pick.test.ts — feat-20260529-picking-raycasting-screen-to-entity M3 / w12 (TDD red).
@@ -30,10 +32,14 @@ import { viewportToWorld } from '../viewport-to-world';
30
32
  // pick(world, cameraEntity, screenX, screenY, viewportWidth, viewportHeight)
31
33
  // -> PickHit | undefined
32
34
  //
33
- // The deterministic scene is built with a bare `new World()` and direct owner
34
- // payloads. A full `createRenderer` is intentionally NOT used
35
+ // The deterministic scene is built with a bare `new World()` + a real
36
+ // `AssetRegistry` (the `assets` param introduced by the 2026-05-29 replan: the
37
+ // ray-AABB test needs `MeshAsset.aabb`, which lives ONLY in the AssetRegistry,
38
+ // never on a world column). A full `createRenderer` is intentionally NOT used —
35
39
  // it requires a live WebGPU device unavailable in the `pnpm test:unit` project,
36
- // and `pick` only consumes World shared references, not the renderer.
40
+ // and `pick` only consumes an `AssetRegistry`, not the renderer. The registry
41
+ // instance IS `renderer.assets` at the demo call site (w16), so the surface
42
+ // under test is identical.
37
43
  //
38
44
  // Coverage (all pick acceptance criteria):
39
45
  // (AC-06) nearest hit wins — two boxes along the ray, the closer one returns
@@ -97,7 +103,7 @@ function translateTransform(
97
103
  * `vertices` must be a multiple of the 12-float interleaved stride; a single 3-vertex
98
104
  * triangle (36 floats) satisfies the gate while the position attribute drives the AABB.
99
105
  */
100
- function registerBox(world: World): Handle<'MeshAsset', 'shared'> {
106
+ function registerBox(world: World, assets: AssetRegistry): Handle<'MeshAsset', 'shared'> {
101
107
  const vertices = new Float32Array([
102
108
  0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
103
109
  0, 0, 0, 0,
@@ -107,7 +113,9 @@ function registerBox(world: World): Handle<'MeshAsset', 'shared'> {
107
113
  -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5,
108
114
  0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
109
115
  ]);
110
- return world.allocSharedRef('MeshAsset', {
116
+ // catalog computes the local-space AABB (withMeshAabb); mint the augmented
117
+ // payload on the world so resolveAssetHandle (used by pick) reads .aabb.
118
+ const result = assets.catalog<MeshAsset>(AssetGuid.format(AssetGuid.random()), {
111
119
  kind: 'mesh',
112
120
  vertices,
113
121
  indices: new Uint16Array([0, 1, 2]),
@@ -123,12 +131,13 @@ function registerBox(world: World): Handle<'MeshAsset', 'shared'> {
123
131
  ],
124
132
 
125
133
  materialSlots: [{ slotName: 'Default' }],
126
- aabb: new Float32Array([-0.5, -0.5, -0.5, 0.5, 0.5, 0.5]),
127
134
  });
135
+ if (!result.ok) throw new Error('mesh catalog failed');
136
+ return world.allocSharedRef('MeshAsset', result.value);
128
137
  }
129
138
 
130
- function registerMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
131
- return world.allocSharedRef('MaterialAsset', {
139
+ function registerMaterial(world: World, assets: AssetRegistry): Handle<'MaterialAsset', 'shared'> {
140
+ const result = assets.catalog<MaterialAsset>(AssetGuid.format(AssetGuid.random()), {
132
141
  kind: 'material',
133
142
  passes: [
134
143
  {
@@ -139,19 +148,23 @@ function registerMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
139
148
  ],
140
149
  values: { baseColor: [1, 1, 1] },
141
150
  });
151
+ if (!result.ok) throw new Error('material catalog failed');
152
+ return world.allocSharedRef('MaterialAsset', result.value);
142
153
  }
143
154
 
144
155
  interface Scene {
145
156
  world: World;
157
+ assets: AssetRegistry;
146
158
  mesh: Handle<'MeshAsset', 'shared'>;
147
159
  material: Handle<'MaterialAsset', 'shared'>;
148
160
  }
149
161
 
150
162
  function makeScene(): Scene {
151
163
  const world = new World();
152
- const mesh = registerBox(world);
153
- const material = registerMaterial(world);
154
- return { world, mesh, material };
164
+ const assets = new AssetRegistry(makeMockShaderRegistry());
165
+ const mesh = registerBox(world, assets);
166
+ const material = registerMaterial(world, assets);
167
+ return { world, assets, mesh, material };
155
168
  }
156
169
 
157
170
  /** Spawn a perspective camera at (x,y,z) looking down -Z (identity rotation). */
@@ -1,5 +1,5 @@
1
+ import { HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
1
2
  import { type EntityHandle, World } from '@forgeax/engine-ecs';
2
- import { createPrimitiveMesh } from '@forgeax/engine-geometry';
3
3
  import {
4
4
  CAMERA_PROJECTION_PERSPECTIVE,
5
5
  Camera,
@@ -8,9 +8,9 @@ import {
8
8
  Visibility,
9
9
  VisibilityStateValue,
10
10
  } from '@forgeax/engine-render';
11
- import { extractFrames } from '@forgeax/engine-render/internal';
12
11
  import { propagateTransforms, Transform } from '@forgeax/engine-scene';
13
12
  import { describe, expect, it } from 'vitest';
13
+ import { extractFrames } from '../../../render/src/render-system-extract';
14
14
  import { pick } from '../pick';
15
15
 
16
16
  const VIEWPORT = 600;
@@ -49,11 +49,10 @@ describe('visibility picking boundary', () => {
49
49
  it('keeps pick results unchanged when the render candidate is hidden', () => {
50
50
  const world = new World();
51
51
  const camera = spawnCamera(world);
52
- const mesh = world.internSharedRef('MeshAsset', createPrimitiveMesh('cube').unwrap());
53
52
  const entity = world
54
53
  .spawn(
55
54
  { component: Transform, data: transform([0, 0, 0]) },
56
- { component: MeshFilter, data: { assetHandle: mesh } },
55
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
57
56
  { component: MeshRenderer, data: {} },
58
57
  { component: Visibility, data: { state: VisibilityStateValue.visible } },
59
58
  )
@@ -15,7 +15,7 @@
15
15
  // - { limit: N } → VertexHit[]
16
16
  //
17
17
  // Reuses pick()'s full skeleton (camera validation / view=invert(world) / projection branch /
18
- // screenToRay / world.sharedRefs.resolve / Transform.world / AABB coarse cull), then for each
18
+ // screenToRay / resolveAssetHandle / Transform.world / AABB coarse cull), then for each
19
19
  // triangle-list submesh that passes the AABB test, iterates every triangle and collects
20
20
  // vertex candidates via rayTriangleIntersects.
21
21
  //
@@ -43,13 +43,13 @@
43
43
  // Finding 9 (behind flag) / Finding 10 (isSkinned);
44
44
  // plan-tasks.json w5/w8 acceptanceCheck.
45
45
 
46
+ import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
46
47
  import type { EntityHandle, World } from '@forgeax/engine-ecs';
47
48
  import { mat4, ray, type Vec3Like, vec2, vec3 } from '@forgeax/engine-math';
48
49
  import { MeshFilter, MeshRenderer } from '@forgeax/engine-render';
49
50
  import { Transform } from '@forgeax/engine-scene';
50
51
  import type { MeshAsset } from '@forgeax/engine-types';
51
52
  import { toShared } from '@forgeax/engine-types';
52
- import { resolvePickingAsset } from './asset-port';
53
53
  import { computeScreenRay, readWorldMatrix } from './pick-core';
54
54
 
55
55
  // ── types ────────────────────────────────────────────────────────────────
@@ -236,7 +236,7 @@ function collectVertexHits(
236
236
  if (!mfRes.ok) {
237
237
  return [];
238
238
  }
239
- const meshRes = resolvePickingAsset<MeshAsset>(
239
+ const meshRes = resolveAssetHandle<MeshAsset>(
240
240
  world,
241
241
  toShared<'MeshAsset'>(mfRes.value.assetHandle as unknown as number),
242
242
  );
@@ -546,7 +546,7 @@ export function pickVertex(
546
546
  for (const row of query) {
547
547
  const assetHandleRaw = Math.round(row.get(MeshFilter).assetHandle as number);
548
548
  if (assetHandleRaw === 0) continue;
549
- const meshRes = resolvePickingAsset<MeshAsset>(world, toShared<'MeshAsset'>(assetHandleRaw));
549
+ const meshRes = resolveAssetHandle<MeshAsset>(world, toShared<'MeshAsset'>(assetHandleRaw));
550
550
  if (!meshRes.ok) continue;
551
551
 
552
552
  // read entity (mirrors pick.ts:190)
package/src/pick.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // Mesh AABB source (feat-20260614 M8, D-15/D-18): the ray-AABB test needs each
10
10
  // mesh's local-space `MeshAsset.aabb`, resolved from the entity's `MeshFilter`
11
- // handle via `world.sharedRefs.resolve<MeshAsset>(handle)` (two-tier builtin /
11
+ // handle via `resolveAssetHandle<MeshAsset>(world, handle)` (two-tier builtin /
12
12
  // world.sharedRefs dispatch). The registry no longer holds handles, so `pick`
13
13
  // takes no `AssetRegistry` -- it resolves entirely World-side. Keeping `pick` a
14
14
  // free function (rather than a `World` method) preserves the layering: `World`
@@ -32,13 +32,13 @@
32
32
  // Related: requirements in-scope #5/#6/#7 + AC-05..AC-11; plan-strategy D-3 / D-6 / 5.3;
33
33
  // research Finding 4 (local->world AABB) + Finding 5 (entity-id via query rows).
34
34
 
35
+ import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
35
36
  import type { EntityHandle, World } from '@forgeax/engine-ecs';
36
37
  import { box3, ray, type Vec3Like, vec3 } from '@forgeax/engine-math';
37
38
  import { MeshFilter, MeshRenderer } from '@forgeax/engine-render';
38
39
  import { Transform } from '@forgeax/engine-scene';
39
40
  import type { MeshAsset } from '@forgeax/engine-types';
40
41
  import { toShared } from '@forgeax/engine-types';
41
- import { resolvePickingAsset } from './asset-port';
42
42
  import { computeScreenRay, readWorldMatrix } from './pick-core';
43
43
 
44
44
  /**
@@ -103,7 +103,7 @@ export function pick(
103
103
  for (const row of query) {
104
104
  const assetHandleRaw = Math.round(row.get(MeshFilter).assetHandle as number);
105
105
  if (assetHandleRaw === 0) continue;
106
- const meshRes = resolvePickingAsset<MeshAsset>(world, toShared<'MeshAsset'>(assetHandleRaw));
106
+ const meshRes = resolveAssetHandle<MeshAsset>(world, toShared<'MeshAsset'>(assetHandleRaw));
107
107
  if (!meshRes.ok) continue;
108
108
  const localAabb = meshRes.value.aabb;
109
109
  if (localAabb === undefined) continue;
@@ -1,6 +0,0 @@
1
- import type { World } from '@forgeax/engine-ecs';
2
- import type { Asset, Handle, Result } from '@forgeax/engine-types';
3
- export type PickingAssetResolutionError = ReturnType<World['sharedRefs']['resolve']> extends Result<unknown, infer E> ? E : never;
4
- /** Resolve a picking payload through the World shared-ref owner. */
5
- export declare function resolvePickingAsset<T extends Asset>(world: World, handle: Handle<string, 'shared'>): Result<T, PickingAssetResolutionError>;
6
- //# sourceMappingURL=asset-port.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"asset-port.d.ts","sourceRoot":"","sources":["../src/asset-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEnE,MAAM,MAAM,2BAA2B,GACrC,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE1F,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,KAAK,EACjD,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAC/B,MAAM,CAAC,CAAC,EAAE,2BAA2B,CAAC,CAExC"}
package/src/asset-port.ts DELETED
@@ -1,13 +0,0 @@
1
- import type { World } from '@forgeax/engine-ecs';
2
- import type { Asset, Handle, Result } from '@forgeax/engine-types';
3
-
4
- export type PickingAssetResolutionError =
5
- ReturnType<World['sharedRefs']['resolve']> extends Result<unknown, infer E> ? E : never;
6
-
7
- /** Resolve a picking payload through the World shared-ref owner. */
8
- export function resolvePickingAsset<T extends Asset>(
9
- world: World,
10
- handle: Handle<string, 'shared'>,
11
- ): Result<T, PickingAssetResolutionError> {
12
- return world.sharedRefs.resolve<string, T>(handle) as Result<T, PickingAssetResolutionError>;
13
- }