@forgeax/engine-picking 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +168 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/glyph-text-pick.test.d.ts +2 -0
- package/dist/__tests__/glyph-text-pick.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts +3 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts.map +1 -0
- package/dist/__tests__/pick-errors.test-d.d.ts +2 -0
- package/dist/__tests__/pick-errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/pick-errors.test.d.ts +2 -0
- package/dist/__tests__/pick-errors.test.d.ts.map +1 -0
- package/dist/__tests__/pick-tile.test.d.ts +2 -0
- package/dist/__tests__/pick-tile.test.d.ts.map +1 -0
- package/dist/__tests__/pick-vertex.unit.test.d.ts +2 -0
- package/dist/__tests__/pick-vertex.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick.test.d.ts +2 -0
- package/dist/__tests__/pick.test.d.ts.map +1 -0
- package/dist/__tests__/visibility-regression.integration.test.d.ts +2 -0
- package/dist/__tests__/visibility-regression.integration.test.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +440 -0
- package/dist/index.mjs.map +1 -0
- package/dist/pick-core.d.ts +39 -0
- package/dist/pick-core.d.ts.map +1 -0
- package/dist/pick-errors.d.ts +39 -0
- package/dist/pick-errors.d.ts.map +1 -0
- package/dist/pick-tile.d.ts +44 -0
- package/dist/pick-tile.d.ts.map +1 -0
- package/dist/pick-vertex.d.ts +87 -0
- package/dist/pick-vertex.d.ts.map +1 -0
- package/dist/pick.d.ts +34 -0
- package/dist/pick.d.ts.map +1 -0
- package/dist/viewport-to-world.d.ts +12 -0
- package/dist/viewport-to-world.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/glyph-text-pick.test.ts +132 -0
- package/src/__tests__/helpers/mock-shader-registry.ts +118 -0
- package/src/__tests__/pick-errors.test-d.ts +66 -0
- package/src/__tests__/pick-errors.test.ts +71 -0
- package/src/__tests__/pick-tile.test.ts +239 -0
- package/src/__tests__/pick-vertex.unit.test.ts +1607 -0
- package/src/__tests__/pick.test.ts +538 -0
- package/src/__tests__/visibility-regression.integration.test.ts +73 -0
- package/src/index.ts +21 -0
- package/src/pick-core.ts +114 -0
- package/src/pick-errors.ts +69 -0
- package/src/pick-tile.ts +157 -0
- package/src/pick-vertex.ts +593 -0
- package/src/pick.ts +148 -0
- package/src/viewport-to-world.ts +23 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { mat4, ray } from '@forgeax/engine-math';
|
|
3
|
+
import { type CameraProjection } from '@forgeax/engine-render';
|
|
4
|
+
/**
|
|
5
|
+
* Read an entity's resolved world mat4 (16 column-major floats) from the
|
|
6
|
+
* `Transform.world` column array view (feat-20260601 D-3). Returns a fresh
|
|
7
|
+
* copy (the view aliases live slot bytes); `undefined` when the entity has no
|
|
8
|
+
* Transform / world column.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readWorldMatrix(world: World, entity: EntityHandle): Float32Array | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The screen-to-world ray plus the camera matrices used to build it.
|
|
13
|
+
*
|
|
14
|
+
* - `ray` — the unprojected world-space pick ray (origin + direction).
|
|
15
|
+
* - `view` — `invert(camera Transform.world)`.
|
|
16
|
+
* - `proj` — the camera projection matrix (perspective / orthographic).
|
|
17
|
+
* - `projectionKind` — the resolved camera projection discriminant.
|
|
18
|
+
*
|
|
19
|
+
* `view` and `proj` are returned separately so vertex picking can build its own
|
|
20
|
+
* `viewProj = proj * view` for `worldToScreen` without recomputing the branch.
|
|
21
|
+
*/
|
|
22
|
+
export interface ScreenRay {
|
|
23
|
+
readonly ray: ray.Ray;
|
|
24
|
+
readonly view: mat4.Mat4;
|
|
25
|
+
readonly proj: mat4.Mat4;
|
|
26
|
+
readonly projectionKind: CameraProjection;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the screen-to-world ray for `cameraEntity` at the viewport-relative
|
|
30
|
+
* `(screenX, screenY)` coordinate: validate the camera component, read its world
|
|
31
|
+
* transform, invert it to a view matrix, branch the projection on the camera
|
|
32
|
+
* discriminant, and unproject the coordinate into a world-space ray.
|
|
33
|
+
*
|
|
34
|
+
* @returns The `ScreenRay`, or `undefined` when the camera entity carries no
|
|
35
|
+
* resolvable `Transform.world` (a degenerate miss — no view matrix can be built).
|
|
36
|
+
* @throws {PickError} `code: 'camera-component-missing'` when `cameraEntity` has no `Camera`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeScreenRay(world: World, cameraEntity: EntityHandle, screenX: number, screenY: number, viewportWidth: number, viewportHeight: number): ScreenRay | undefined;
|
|
39
|
+
//# sourceMappingURL=pick-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pick-core.d.ts","sourceRoot":"","sources":["../src/pick-core.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAU,KAAK,gBAAgB,EAA2B,MAAM,wBAAwB,CAAC;AAIhG;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAG,YAAY,GAAG,SAAS,CAG5F;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,gBAAgB,CAAC;CAC3C;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,SAAS,GAAG,SAAS,CA6CvB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail for the `PickError` camera-component-missing code.
|
|
3
|
+
*
|
|
4
|
+
* Carries the offending camera entity (packed `Entity` u32) so AI consumers can read
|
|
5
|
+
* `.detail.cameraEntity` by property access (charter P4) — no string parsing of the
|
|
6
|
+
* human-facing message.
|
|
7
|
+
*/
|
|
8
|
+
export interface PickCameraMissingDetail {
|
|
9
|
+
readonly cameraEntity: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Structured error for the picking precondition failure: the supplied `cameraEntity`
|
|
13
|
+
* does not hold a `Camera` component, so `pick()` cannot build the view / projection
|
|
14
|
+
* matrices needed to unproject the screen coordinate.
|
|
15
|
+
*
|
|
16
|
+
* Three-field structured surface per the AGENTS.md error model + `.detail`:
|
|
17
|
+
* - `.code = 'camera-component-missing'` (closed `PickErrorCode`)
|
|
18
|
+
* - `.expected` — the expected precondition (camera entity carries a `Camera`)
|
|
19
|
+
* - `.hint` — an actionable `world.set` recovery directive
|
|
20
|
+
* - `.detail = { cameraEntity }` — the offending entity (charter P4)
|
|
21
|
+
*
|
|
22
|
+
* Surfaced (not the normal miss path): a no-hit ray returns `undefined` from `pick()`;
|
|
23
|
+
* this error is reserved for the unrecoverable precondition (charter P3 explicit failure,
|
|
24
|
+
* separate channel from the recoverable miss).
|
|
25
|
+
*/
|
|
26
|
+
export declare class PickError extends Error {
|
|
27
|
+
readonly code = "camera-component-missing";
|
|
28
|
+
readonly expected: string;
|
|
29
|
+
readonly hint: string;
|
|
30
|
+
readonly detail: PickCameraMissingDetail;
|
|
31
|
+
constructor(cameraEntity: number);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Closed union of picking precondition error codes, derived from the PickError owner.
|
|
35
|
+
*
|
|
36
|
+
* AI users perform exhaustive `switch (err.code)` without a default; TS guards completeness.
|
|
37
|
+
*/
|
|
38
|
+
export type PickErrorCode = PickError['code'];
|
|
39
|
+
//# sourceMappingURL=pick-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pick-errors.d.ts","sourceRoot":"","sources":["../src/pick-errors.ts"],"names":[],"mappings":"AAkBA;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,8BAA8B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;gBAE7B,YAAY,EAAE,MAAM;CAWjC;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type EntityHandle, type World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { type Result } from '@forgeax/engine-types';
|
|
3
|
+
/**
|
|
4
|
+
* Closed error union for `pickTile` (charter P3 + P4). Two variants cover the
|
|
5
|
+
* structural-break paths; "ray hit nothing" is a value (`Result.ok(null)`),
|
|
6
|
+
* not an error.
|
|
7
|
+
*/
|
|
8
|
+
export type PickTileError = {
|
|
9
|
+
readonly code: 'tilemap-not-found';
|
|
10
|
+
readonly tilemapEntity: EntityHandle;
|
|
11
|
+
} | {
|
|
12
|
+
readonly code: 'tilemap-component-missing';
|
|
13
|
+
readonly tilemapEntity: EntityHandle;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Successful picking outcome. Returned through `Result.ok`; a `null` value
|
|
17
|
+
* means the query landed in-bounds but every layer at that cell was empty
|
|
18
|
+
* (or the point was outside the tilemap world bounds).
|
|
19
|
+
*/
|
|
20
|
+
export interface PickTileHit {
|
|
21
|
+
readonly layerEntity: EntityHandle;
|
|
22
|
+
readonly cellX: number;
|
|
23
|
+
readonly cellY: number;
|
|
24
|
+
readonly tileId: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Find the topmost non-zero tile under `(worldX, worldY)` for a given
|
|
28
|
+
* Tilemap entity, walking child TileLayer entities in DESCENDING
|
|
29
|
+
* `layerOrder`.
|
|
30
|
+
*
|
|
31
|
+
* @returns
|
|
32
|
+
* - `Result.ok(PickTileHit)` for a non-zero cell on some layer.
|
|
33
|
+
* - `Result.ok(null)` for an empty cell or out-of-bounds query.
|
|
34
|
+
* - `Result.err({ code: 'tilemap-not-found' })` for a dead handle.
|
|
35
|
+
* - `Result.err({ code: 'tilemap-component-missing' })` for a live entity
|
|
36
|
+
* without a Tilemap component.
|
|
37
|
+
*
|
|
38
|
+
* The caller must propagate the World so `Transform.world` is current. A
|
|
39
|
+
* singular world transform follows `mat4.invert`'s deterministic identity
|
|
40
|
+
* fallback, which keeps this error union structural rather than adding a
|
|
41
|
+
* third diagnostic arm.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pickTile(world: World, tilemapEntity: EntityHandle, worldX: number, worldY: number): Result<PickTileHit | null, PickTileError>;
|
|
44
|
+
//# sourceMappingURL=pick-tile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pick-tile.d.ts","sourceRoot":"","sources":["../src/pick-tile.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAU,KAAK,YAAY,EAAE,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAI5E,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAA;CAAE,GAC5E;IAAE,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAA;CAAE,CAAC;AAEzF;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,KAAK,EACZ,aAAa,EAAE,YAAY,EAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,WAAW,GAAG,IAAI,EAAE,aAAa,CAAC,CAkF3C"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { type Vec3Like } from '@forgeax/engine-math';
|
|
3
|
+
/**
|
|
4
|
+
* Result of a successful vertex pick: the mesh vertex nearest to the
|
|
5
|
+
* supplied screen-space coordinate.
|
|
6
|
+
*
|
|
7
|
+
* Field set:
|
|
8
|
+
* - `entity` — the entity this vertex belongs to.
|
|
9
|
+
* - `vertexIndex` — index into the vertex position buffer (0-based).
|
|
10
|
+
* - `worldPos` — world-space position of the vertex (Vec3Like; rest-pose when
|
|
11
|
+
* deformed=true). D-7: Vec3Like avoids math brand cast lint in
|
|
12
|
+
* the runtime package.
|
|
13
|
+
* - `screenDist` — screen-space pixel distance from the query coordinate to the
|
|
14
|
+
* projected vertex position (non-negative).
|
|
15
|
+
* - `worldDist` — perpendicular 3D distance from the vertex world position to
|
|
16
|
+
* the pick ray (non-negative). Orthogonal counterpart to screenDist.
|
|
17
|
+
* - `deformed` — true when the mesh is skinned (skinIndex + skinWeight attributes
|
|
18
|
+
* both present), indicating worldPos reflects rest-pose, not GPU
|
|
19
|
+
* skinning output.
|
|
20
|
+
*/
|
|
21
|
+
export interface VertexHit {
|
|
22
|
+
readonly entity: EntityHandle;
|
|
23
|
+
readonly vertexIndex: number;
|
|
24
|
+
readonly worldPos: Vec3Like;
|
|
25
|
+
readonly screenDist: number;
|
|
26
|
+
readonly worldDist: number;
|
|
27
|
+
readonly deformed: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Query the nearest vertex on a single entity.
|
|
31
|
+
*
|
|
32
|
+
* Without options: returns `VertexHit | undefined` (nearest hit, or `undefined` on miss).
|
|
33
|
+
*
|
|
34
|
+
* @param world The ECS world (propagateTransforms must have been called this frame).
|
|
35
|
+
* @param cameraEntity Entity carrying the Camera component (and Transform).
|
|
36
|
+
* @param screenX Horizontal pixel coordinate (viewport top-left, y-down).
|
|
37
|
+
* @param screenY Vertical pixel coordinate.
|
|
38
|
+
* @param viewportWidth Viewport width in pixels.
|
|
39
|
+
* @param viewportHeight Viewport height in pixels.
|
|
40
|
+
* @param entity The mesh entity to query (must carry MeshFilter + MeshRenderer).
|
|
41
|
+
* @returns The nearest `VertexHit`, or `undefined` when nothing is hit.
|
|
42
|
+
* @throws {PickError} `code: 'camera-component-missing'` when cameraEntity has no Camera.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pickVertexOnEntity(world: World, cameraEntity: EntityHandle, screenX: number, screenY: number, viewportWidth: number, viewportHeight: number, entity: EntityHandle): VertexHit | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Query up to `limit` nearest vertices on a single entity.
|
|
47
|
+
*
|
|
48
|
+
* With `{ limit }`: returns `VertexHit[]` sorted by `screenDist` ascending.
|
|
49
|
+
*
|
|
50
|
+
* @param options.limit Maximum number of candidates to return (returns all available
|
|
51
|
+
* vertices when limit exceeds the hit count).
|
|
52
|
+
* @returns Sorted array of `VertexHit` (empty on miss).
|
|
53
|
+
*/
|
|
54
|
+
export declare function pickVertexOnEntity(world: World, cameraEntity: EntityHandle, screenX: number, screenY: number, viewportWidth: number, viewportHeight: number, entity: EntityHandle, options: {
|
|
55
|
+
limit: number;
|
|
56
|
+
}): VertexHit[];
|
|
57
|
+
/**
|
|
58
|
+
* Query the nearest vertex across all pickable mesh entities in the world.
|
|
59
|
+
*
|
|
60
|
+
* Without options: returns `VertexHit | undefined` (globally nearest, or `undefined` on miss).
|
|
61
|
+
*
|
|
62
|
+
* Walks all renderable archetypes, does an AABB coarse cull (R-2), then calls
|
|
63
|
+
* `pickVertexOnEntity` on each ray-intersecting entity. Builtin meshes without AABB
|
|
64
|
+
* fall through to walk-all-vertices (AC-07).
|
|
65
|
+
*
|
|
66
|
+
* @param world The ECS world (propagateTransforms must have been called this frame).
|
|
67
|
+
* @param cameraEntity Entity carrying the Camera component (and Transform).
|
|
68
|
+
* @param screenX Horizontal pixel coordinate (viewport top-left, y-down).
|
|
69
|
+
* @param screenY Vertical pixel coordinate.
|
|
70
|
+
* @param viewportWidth Viewport width in pixels.
|
|
71
|
+
* @param viewportHeight Viewport height in pixels.
|
|
72
|
+
* @returns The globally nearest `VertexHit`, or `undefined` when nothing is hit.
|
|
73
|
+
* @throws {PickError} `code: 'camera-component-missing'` when cameraEntity has no Camera.
|
|
74
|
+
*/
|
|
75
|
+
export declare function pickVertex(world: World, cameraEntity: EntityHandle, screenX: number, screenY: number, viewportWidth: number, viewportHeight: number): VertexHit | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Query up to `limit` nearest vertices across all pickable mesh entities.
|
|
78
|
+
*
|
|
79
|
+
* With `{ limit }`: returns `VertexHit[]` globally sorted by `screenDist` ascending.
|
|
80
|
+
*
|
|
81
|
+
* @param options.limit Maximum number of candidates to return.
|
|
82
|
+
* @returns Sorted array of `VertexHit` (empty on miss).
|
|
83
|
+
*/
|
|
84
|
+
export declare function pickVertex(world: World, cameraEntity: EntityHandle, screenX: number, screenY: number, viewportWidth: number, viewportHeight: number, options: {
|
|
85
|
+
limit: number;
|
|
86
|
+
}): VertexHit[];
|
|
87
|
+
//# sourceMappingURL=pick-vertex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pick-vertex.d.ts","sourceRoot":"","sources":["../src/pick-vertex.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAa,KAAK,QAAQ,EAAc,MAAM,sBAAsB,CAAC;AAS5E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAqTD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,GACnB,SAAS,GAAG,SAAS,CAAC;AAEzB;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACzB,SAAS,EAAE,CAAC;AAmCf;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,SAAS,GAAG,SAAS,CAAC;AAEzB;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACzB,SAAS,EAAE,CAAC"}
|
package/dist/pick.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { type Vec3Like } from '@forgeax/engine-math';
|
|
3
|
+
/**
|
|
4
|
+
* Result of a successful screen-to-entity pick.
|
|
5
|
+
*
|
|
6
|
+
* Minimal three-field surface (D-6):
|
|
7
|
+
* - `entity` — the picked `Entity` (packed u32 handle, ready for `world.get` / `world.set`)
|
|
8
|
+
* - `point` — the world-space ray/AABB entry point (`Vec3Like`; a 3-element array)
|
|
9
|
+
* - `distance` — the entry distance along the ray from the camera (>= 0)
|
|
10
|
+
*
|
|
11
|
+
* No `face` / `uv` / `normal` fields: AABB picking has no triangle resolution, so those
|
|
12
|
+
* would be a lie. A future mesh-precise pick spin-off owns them (requirements OOS).
|
|
13
|
+
*/
|
|
14
|
+
export interface PickHit {
|
|
15
|
+
readonly entity: EntityHandle;
|
|
16
|
+
readonly point: Vec3Like;
|
|
17
|
+
readonly distance: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Raycast from a viewport-relative screen coordinate into the world and return the
|
|
21
|
+
* nearest pickable mesh entity whose world-space AABB the ray enters.
|
|
22
|
+
*
|
|
23
|
+
* @param world The ECS world holding the camera + candidate mesh entities (and the
|
|
24
|
+
* per-World SharedRefStore that owns each `MeshAsset` and its local-space `aabb`).
|
|
25
|
+
* @param cameraEntity The entity carrying the `Camera` component (and a Transform).
|
|
26
|
+
* @param screenX Horizontal pixel coordinate relative to the viewport top-left (y-down).
|
|
27
|
+
* @param screenY Vertical pixel coordinate.
|
|
28
|
+
* @param viewportWidth Viewport width in pixels.
|
|
29
|
+
* @param viewportHeight Viewport height in pixels.
|
|
30
|
+
* @returns The nearest `PickHit`, or `undefined` when the ray hits nothing.
|
|
31
|
+
* @throws {PickError} `code: 'camera-component-missing'` when `cameraEntity` has no `Camera`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pick(world: World, cameraEntity: EntityHandle, screenX: number, screenY: number, viewportWidth: number, viewportHeight: number): PickHit | undefined;
|
|
34
|
+
//# sourceMappingURL=pick.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pick.d.ts","sourceRoot":"","sources":["../src/pick.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAa,KAAK,QAAQ,EAAQ,MAAM,sBAAsB,CAAC;AAOtE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,IAAI,CAClB,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,OAAO,GAAG,SAAS,CAkErB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EntityHandle, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { ray } from '@forgeax/engine-math';
|
|
3
|
+
/**
|
|
4
|
+
* Unproject a viewport pixel into a world-space ray.
|
|
5
|
+
*
|
|
6
|
+
* `screenX`/`screenY` use the canvas convention: top-left origin, y down.
|
|
7
|
+
* The caller must run `propagateTransforms(world)` for the current frame first.
|
|
8
|
+
* A camera without a resolvable Transform returns `undefined`; a missing
|
|
9
|
+
* Camera component throws the package's structured `PickError`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function viewportToWorld(world: World, cameraEntity: EntityHandle, screenX: number, screenY: number, viewportWidth: number, viewportHeight: number): ray.Ray | undefined;
|
|
12
|
+
//# sourceMappingURL=viewport-to-world.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport-to-world.d.ts","sourceRoot":"","sources":["../src/viewport-to-world.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAGhD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,GAAG,CAAC,GAAG,GAAG,SAAS,CAGrB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeax/engine-picking",
|
|
3
|
+
"version": "0.0.0-dev.8d955ade1c79",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"description": "Screen-to-entity + vertex-level + tile-cell picking free functions for forgeax-engine (Tier 2.2 -- extracted from @forgeax/engine-runtime).",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@forgeax/engine-assets-runtime": "0.0.0-dev.8d955ade1c79",
|
|
26
|
+
"@forgeax/engine-ecs": "0.0.0-dev.8d955ade1c79",
|
|
27
|
+
"@forgeax/engine-math": "0.0.0-dev.8d955ade1c79",
|
|
28
|
+
"@forgeax/engine-render": "0.0.0-dev.8d955ade1c79",
|
|
29
|
+
"@forgeax/engine-runtime": "0.0.0-dev.8d955ade1c79",
|
|
30
|
+
"@forgeax/engine-scene": "0.0.0-dev.8d955ade1c79",
|
|
31
|
+
"@forgeax/engine-types": "0.0.0-dev.8d955ade1c79"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@forgeax/engine-pack": "0.0.0-dev.8d955ade1c79",
|
|
35
|
+
"@forgeax/engine-shader": "0.0.0-dev.8d955ade1c79"
|
|
36
|
+
},
|
|
37
|
+
"forgeax": {
|
|
38
|
+
"metrics": {
|
|
39
|
+
"bundle-size": {
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"path": "dist/index.mjs",
|
|
42
|
+
"compression": "gzip"
|
|
43
|
+
},
|
|
44
|
+
"fps": {
|
|
45
|
+
"enabled": false,
|
|
46
|
+
"reason": "picking is a query library invoked on demand (click / hover), not a per-frame canvas; fps reported by hello-picking that consumes it"
|
|
47
|
+
},
|
|
48
|
+
"bench": {
|
|
49
|
+
"enabled": false,
|
|
50
|
+
"reason": "picking runs at interaction time (ray-AABB / ray-triangle per query), not per-frame; no CI micro-benchmark warranted"
|
|
51
|
+
},
|
|
52
|
+
"gate": {
|
|
53
|
+
"enabled": false,
|
|
54
|
+
"reason": "no package-level binary gate; smoke gate covered by hello-picking dawn smoke that exercises the pick contract"
|
|
55
|
+
},
|
|
56
|
+
"spike-report": {
|
|
57
|
+
"enabled": false,
|
|
58
|
+
"reason": "not a spike package; picking cluster extracted from runtime in feat-20260705-runtime-tier2-decomposition"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsup",
|
|
64
|
+
"test": "vitest run"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// glyph-text-pick.test.ts — pick() hits a baked GlyphText entity (AC-13).
|
|
2
|
+
//
|
|
3
|
+
// Extracted from packages/runtime/src/__tests__/text.unit.test.ts (the
|
|
4
|
+
// "from glyph-text-pick.test.ts" block) in feat-20260705 M2 / w25 when the pick
|
|
5
|
+
// cluster moved to @forgeax/engine-picking. Runtime can no longer import the
|
|
6
|
+
// picking package (AC-203), so this cross-cutting test (glyph text bake + pick)
|
|
7
|
+
// lives in the downstream picking package that depends on runtime.
|
|
8
|
+
|
|
9
|
+
import { type EntityHandle, World } from '@forgeax/engine-ecs';
|
|
10
|
+
import { CAMERA_PROJECTION_PERSPECTIVE, Camera } from '@forgeax/engine-render';
|
|
11
|
+
import { GlyphText } from '@forgeax/engine-render/authoring';
|
|
12
|
+
import { propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
13
|
+
import type { FontAsset, GlyphMetric, Handle } from '@forgeax/engine-types';
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import { GpuResidencyCache } from '../../../render/src/device/gpu-residency';
|
|
16
|
+
import {
|
|
17
|
+
glyphTextLayoutSystem,
|
|
18
|
+
resetGlyphBakeCache,
|
|
19
|
+
} from '../../../render/src/glyph-text-layout-system';
|
|
20
|
+
import { pick } from '../pick';
|
|
21
|
+
|
|
22
|
+
describe('glyph-text-pick', () => {
|
|
23
|
+
// Pick tests do not wire a GPU device; the residency store stays CPU-only.
|
|
24
|
+
const gpuStore = new GpuResidencyCache();
|
|
25
|
+
|
|
26
|
+
const VP = 600;
|
|
27
|
+
|
|
28
|
+
function metric(): GlyphMetric {
|
|
29
|
+
return {
|
|
30
|
+
advance: 10,
|
|
31
|
+
bearingX: 0,
|
|
32
|
+
bearingY: 8,
|
|
33
|
+
size: { w: 8, h: 8 },
|
|
34
|
+
region: { x: 0, y: 0, w: 8, h: 8 },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function registerFont(world: World, chars: string): number {
|
|
39
|
+
const glyphs: Record<number, GlyphMetric> = {};
|
|
40
|
+
for (const ch of chars) glyphs[ch.codePointAt(0) as number] = metric();
|
|
41
|
+
const font: FontAsset = {
|
|
42
|
+
kind: 'font',
|
|
43
|
+
atlas: 0 as never,
|
|
44
|
+
sampler: 0 as never,
|
|
45
|
+
glyphs,
|
|
46
|
+
common: {
|
|
47
|
+
lineHeight: 12,
|
|
48
|
+
base: 8,
|
|
49
|
+
distanceRange: 4,
|
|
50
|
+
pxRange: 4,
|
|
51
|
+
atlasWidth: 64,
|
|
52
|
+
atlasHeight: 64,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
return world.allocSharedRef('FontAsset', font) as unknown as number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeWorld(): World {
|
|
59
|
+
const world = new World();
|
|
60
|
+
return world;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function spawnCamera(world: World, z: number): EntityHandle {
|
|
64
|
+
return world
|
|
65
|
+
.spawn(
|
|
66
|
+
{
|
|
67
|
+
component: Transform,
|
|
68
|
+
data: {
|
|
69
|
+
pos: [0, 0, z],
|
|
70
|
+
quat: [0, 0, 0, 1],
|
|
71
|
+
scale: [1, 1, 1],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
component: Camera,
|
|
76
|
+
data: {
|
|
77
|
+
fov: Math.PI / 4,
|
|
78
|
+
aspect: 1,
|
|
79
|
+
near: 0.1,
|
|
80
|
+
far: 100,
|
|
81
|
+
projection: CAMERA_PROJECTION_PERSPECTIVE,
|
|
82
|
+
left: -1,
|
|
83
|
+
right: 1,
|
|
84
|
+
bottom: -1,
|
|
85
|
+
top: 1,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
.unwrap();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function spawnLabel(world: World, fontId: number): EntityHandle {
|
|
93
|
+
return world
|
|
94
|
+
.spawn(
|
|
95
|
+
{
|
|
96
|
+
component: Transform,
|
|
97
|
+
data: {
|
|
98
|
+
pos: [0, 0, 0],
|
|
99
|
+
quat: [0, 0, 0, 1],
|
|
100
|
+
scale: [1, 1, 1],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
component: GlyphText,
|
|
105
|
+
data: {
|
|
106
|
+
fontHandle: fontId as unknown as Handle<'FontAsset', 'shared'>,
|
|
107
|
+
text: 'Hi',
|
|
108
|
+
fontSize: 1,
|
|
109
|
+
color: [1, 1, 1, 1],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
.unwrap();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe('glyph text pick (AC-13, pick.ts unchanged)', () => {
|
|
117
|
+
it('(d) center-viewport ray hits the baked text entity', () => {
|
|
118
|
+
resetGlyphBakeCache();
|
|
119
|
+
const world = makeWorld();
|
|
120
|
+
const fontId = registerFont(world, 'Hi');
|
|
121
|
+
const camera = spawnCamera(world, 5);
|
|
122
|
+
const label = spawnLabel(world, fontId);
|
|
123
|
+
|
|
124
|
+
glyphTextLayoutSystem(world, gpuStore); // bake + attach MeshFilter + MeshRenderer
|
|
125
|
+
propagateTransforms(world);
|
|
126
|
+
|
|
127
|
+
const hit = pick(world, camera, VP / 2, VP / 2, VP, VP);
|
|
128
|
+
expect(hit).toBeDefined();
|
|
129
|
+
expect(hit?.entity).toBe(label);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Shared test helper: minimal mock ShaderRegistry for AssetRegistry constructor.
|
|
2
|
+
// feat-20260527-material-registration-unification M1 / w4.
|
|
3
|
+
//
|
|
4
|
+
// All tests that create AssetRegistry directly must pass a ShaderRegistry
|
|
5
|
+
// since the constructor parameter became required in w1. This helper
|
|
6
|
+
// provides a zero-config mock that satisfies the constructor signature
|
|
7
|
+
// without requiring a real GPU device.
|
|
8
|
+
|
|
9
|
+
import { ShaderRegistry, type ShaderRegistryDevice } from '@forgeax/engine-shader';
|
|
10
|
+
|
|
11
|
+
export function makeMockShaderRegistry(): ShaderRegistry {
|
|
12
|
+
const mockDevice: ShaderRegistryDevice = {
|
|
13
|
+
createShaderModule() {
|
|
14
|
+
return {
|
|
15
|
+
ok: true,
|
|
16
|
+
value: undefined,
|
|
17
|
+
unwrap: () => undefined,
|
|
18
|
+
unwrapOr: (d: unknown) => d,
|
|
19
|
+
} as unknown as ReturnType<ShaderRegistryDevice['createShaderModule']>;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const sr = new ShaderRegistry({
|
|
23
|
+
device: mockDevice,
|
|
24
|
+
manifestUrl: undefined,
|
|
25
|
+
});
|
|
26
|
+
// Register a default test shader so M2 tests can validate against it.
|
|
27
|
+
sr.installMaterialArtifact('test::standard', {
|
|
28
|
+
source: 'fn main() {}',
|
|
29
|
+
paramSchema: [
|
|
30
|
+
{ name: 'baseColor', type: 'color' },
|
|
31
|
+
{ name: 'metallic', type: 'f32' },
|
|
32
|
+
{ name: 'roughness', type: 'f32' },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
sr.installMaterialArtifact('test::unlit', {
|
|
36
|
+
source: 'fn main() {}',
|
|
37
|
+
paramSchema: [
|
|
38
|
+
{ name: 'baseColor', type: 'color' },
|
|
39
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
40
|
+
{ name: 'sampler', type: 'sampler', default: null },
|
|
41
|
+
{ name: 'lightIntensity', type: 'f32' }, // required — no default
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
// Register the engine-shipped default-standard-pbr shader so tests
|
|
45
|
+
// that use it pass the register-time validation gate (M2 / w6).
|
|
46
|
+
// paramSchema mirrors packages/shader/src/default-standard-pbr.schema.json.
|
|
47
|
+
sr.installMaterialArtifact('forgeax::default-standard-pbr', {
|
|
48
|
+
source: 'fn main() {}',
|
|
49
|
+
paramSchema: [
|
|
50
|
+
{ name: 'baseColor', type: 'color', default: [1.0, 1.0, 1.0, 1.0] },
|
|
51
|
+
{ name: 'metallic', type: 'f32', default: 0.0 },
|
|
52
|
+
{ name: 'roughness', type: 'f32', default: 0.5 },
|
|
53
|
+
// feat-20260613 fix-issue-1 (D-8): channelMap split into 4 f32 selectors.
|
|
54
|
+
{ name: 'metallicChannel', type: 'f32', default: 2.0 },
|
|
55
|
+
{ name: 'roughnessChannel', type: 'f32', default: 1.0 },
|
|
56
|
+
{ name: 'aoChannel', type: 'f32', default: 0.0 },
|
|
57
|
+
{ name: 'extraChannel', type: 'f32', default: 0.0 },
|
|
58
|
+
{ name: 'emissive', type: 'vec3', default: [0.0, 0.0, 0.0] },
|
|
59
|
+
{ name: 'emissiveIntensity', type: 'f32', default: 0.0 },
|
|
60
|
+
{ name: 'occlusionStrength', type: 'f32', default: 1.0 },
|
|
61
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
62
|
+
{ name: 'metallicRoughnessTexture', type: 'texture2d' },
|
|
63
|
+
{ name: 'normalTexture', type: 'texture2d' },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
sr.installMaterialArtifact('forgeax::default-unlit', {
|
|
67
|
+
source: 'fn main() {}',
|
|
68
|
+
paramSchema: [
|
|
69
|
+
{ name: 'baseColor', type: 'color' },
|
|
70
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
71
|
+
{ name: 'sampler', type: 'sampler', default: null },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
// test::dummy used by M1 test (c)
|
|
75
|
+
sr.installMaterialArtifact('test::dummy', {
|
|
76
|
+
source: 'fn main() {}',
|
|
77
|
+
paramSchema: [],
|
|
78
|
+
});
|
|
79
|
+
// forgeax::msdf-text used by the glyph layout system (feat-20260531 F-1).
|
|
80
|
+
// paramSchema mirrors packages/shader/src/msdf-text.material.json so the
|
|
81
|
+
// layout system's per-font MaterialAsset register passes validation in the
|
|
82
|
+
// GPU-less unit tests.
|
|
83
|
+
sr.installMaterialArtifact('forgeax::msdf-text', {
|
|
84
|
+
source: 'fn main() {}',
|
|
85
|
+
paramSchema: [
|
|
86
|
+
{ name: 'tintColor', type: 'color', default: [1.0, 1.0, 1.0, 1.0] },
|
|
87
|
+
{ name: 'distanceRange', type: 'f32', default: 4.0 },
|
|
88
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
89
|
+
{ name: 'sampler', type: 'sampler', default: null },
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
// feat-20260609-pipeline-driven-pass-selector-shadowcaster-via-mat M3 / T-007:
|
|
93
|
+
// forgeax::default-shadow-caster — vertex-only depth pass shader for
|
|
94
|
+
// directional shadow maps. Registered as the 6th built-in material shader.
|
|
95
|
+
sr.installMaterialArtifact('forgeax::default-shadow-caster', {
|
|
96
|
+
source: 'fn main() {}',
|
|
97
|
+
paramSchema: [],
|
|
98
|
+
});
|
|
99
|
+
// feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild:
|
|
100
|
+
// forgeax::sprite registered in the mock so resolveTilesetMaterial inside
|
|
101
|
+
// tilemap-chunk-extract-system can register a per-tile material in unit
|
|
102
|
+
// tests (paramSchema mirrors packages/shader/src/sprite.material.json).
|
|
103
|
+
sr.installMaterialArtifact('forgeax::sprite', {
|
|
104
|
+
source: 'fn main() {}',
|
|
105
|
+
paramSchema: [
|
|
106
|
+
{ name: 'baseColor', type: 'color', default: [1.0, 1.0, 1.0, 1.0] },
|
|
107
|
+
{ name: 'texture', type: 'texture2d' },
|
|
108
|
+
{ name: 'sampler', type: 'sampler', default: null },
|
|
109
|
+
{ name: 'region', type: 'vec4', default: [0.0, 0.0, 1.0, 1.0] },
|
|
110
|
+
{ name: 'pivot', type: 'vec2', default: [0.5, 0.5] },
|
|
111
|
+
{ name: 'flipX', type: 'f32', default: 0.0 },
|
|
112
|
+
{ name: 'flipY', type: 'f32', default: 0.0 },
|
|
113
|
+
{ name: 'slices', type: 'vec4', default: [0.0, 0.0, 0.0, 0.0] },
|
|
114
|
+
{ name: 'sliceMode', type: 'f32', default: 0.0 },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
return sr;
|
|
118
|
+
}
|