@forgeax/engine-graphics-extras 0.1.2

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.
Files changed (48) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +165 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/tile-bits.test.d.ts +2 -0
  5. package/dist/__tests__/tile-bits.test.d.ts.map +1 -0
  6. package/dist/__tests__/video-capability-probe.unit.test.d.ts +2 -0
  7. package/dist/__tests__/video-capability-probe.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/video-loader.unit.test.d.ts +2 -0
  9. package/dist/__tests__/video-loader.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/video-player-component.unit.test.d.ts +2 -0
  11. package/dist/__tests__/video-player-component.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/video-player-multi-entity.unit.test.d.ts +2 -0
  13. package/dist/__tests__/video-player-multi-entity.unit.test.d.ts.map +1 -0
  14. package/dist/glyph-layout.d.ts +48 -0
  15. package/dist/glyph-layout.d.ts.map +1 -0
  16. package/dist/glyph-mesh-bake.d.ts +30 -0
  17. package/dist/glyph-mesh-bake.d.ts.map +1 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.mjs +306 -0
  21. package/dist/index.mjs.map +1 -0
  22. package/dist/tile-bits.d.ts +21 -0
  23. package/dist/tile-bits.d.ts.map +1 -0
  24. package/dist/tileset-decoder.d.ts +4 -0
  25. package/dist/tileset-decoder.d.ts.map +1 -0
  26. package/dist/video-element-provider.d.ts +41 -0
  27. package/dist/video-element-provider.d.ts.map +1 -0
  28. package/dist/video-loader.d.ts +4 -0
  29. package/dist/video-loader.d.ts.map +1 -0
  30. package/dist/video-player-system.d.ts +37 -0
  31. package/dist/video-player-system.d.ts.map +1 -0
  32. package/dist/video-player.d.ts +23 -0
  33. package/dist/video-player.d.ts.map +1 -0
  34. package/package.json +62 -0
  35. package/src/__tests__/tile-bits.test.ts +88 -0
  36. package/src/__tests__/video-capability-probe.unit.test.ts +130 -0
  37. package/src/__tests__/video-loader.unit.test.ts +27 -0
  38. package/src/__tests__/video-player-component.unit.test.ts +73 -0
  39. package/src/__tests__/video-player-multi-entity.unit.test.ts +75 -0
  40. package/src/glyph-layout.ts +208 -0
  41. package/src/glyph-mesh-bake.ts +151 -0
  42. package/src/index.ts +36 -0
  43. package/src/tile-bits.ts +77 -0
  44. package/src/tileset-decoder.ts +64 -0
  45. package/src/video-element-provider.ts +72 -0
  46. package/src/video-loader.ts +80 -0
  47. package/src/video-player-system.ts +61 -0
  48. package/src/video-player.ts +51 -0
@@ -0,0 +1,80 @@
1
+ // @forgeax/engine-graphics-extras - video loader (feat-20260623-world-space-video-asset M2 / w4).
2
+ //
3
+ // Descriptor-only loader for the 'video' asset kind. VideoAsset is a pure
4
+ // `{ url }` descriptor (no pixel decode, no import/cook pipeline — OOS-1);
5
+ // the runtime resolves it into an HTMLVideoElement via the host-provided
6
+ // `VideoElementProvider` World Resource (plan-strategy D-1).
7
+ //
8
+ // The loader returns the payload as VideoAsset synchronously — no fetch or
9
+ // decode. Audio differs because its renderer-injected catalog-entry loader
10
+ // fetches and decodes a Web Audio payload before cataloguing it.
11
+ //
12
+ // Registered in wireDefaultLoaders alongside the other 10 default kinds
13
+ // (plan-strategy D-7: engine-own kind goes in the default set so AI users
14
+ // don't have to manually register it).
15
+
16
+ import {
17
+ type AssetDecoderContribution,
18
+ type AssetKind,
19
+ err,
20
+ type Loader,
21
+ ok,
22
+ type VideoAsset,
23
+ } from '@forgeax/engine-types';
24
+
25
+ const VIDEO_URL_RESOLUTION_BASE = 'https://forgeax.invalid/';
26
+ const VIDEO_URL_WHITESPACE = /\s/u;
27
+
28
+ function hasVideoUrlControlCharacter(value: string): boolean {
29
+ for (const character of value) {
30
+ const code = character.charCodeAt(0);
31
+ if (code <= 0x1f || code === 0x7f) return true;
32
+ }
33
+ return false;
34
+ }
35
+
36
+ function isBrowserResolvableVideoUrl(value: unknown): value is string {
37
+ if (
38
+ typeof value !== 'string' ||
39
+ value.length === 0 ||
40
+ value.trim().length === 0 ||
41
+ value !== value.trim() ||
42
+ hasVideoUrlControlCharacter(value) ||
43
+ VIDEO_URL_WHITESPACE.test(value) ||
44
+ value.startsWith('//')
45
+ ) {
46
+ return false;
47
+ }
48
+ try {
49
+ const resolved = new URL(value, VIDEO_URL_RESOLUTION_BASE);
50
+ return resolved.protocol === 'http:' || resolved.protocol === 'https:';
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ export const videoLoader: Loader<VideoAsset> = {
57
+ kind: 'video',
58
+ load(payload: Record<string, unknown>): VideoAsset | undefined {
59
+ if (!isBrowserResolvableVideoUrl(payload.url)) return undefined;
60
+ return { kind: 'video', url: payload.url };
61
+ },
62
+ };
63
+
64
+ export const videoContribution: AssetDecoderContribution<VideoAsset, 'video'> = {
65
+ kind: { kind: 'video' } as AssetKind<VideoAsset, 'video'>,
66
+ consumer: 'VideoElementProvider',
67
+ decoder: {
68
+ async decode({ envelope }) {
69
+ const payload = envelope.payload;
70
+ return payload.kind === 'video' && isBrowserResolvableVideoUrl(payload.url)
71
+ ? ok(payload)
72
+ : err({
73
+ code: 'asset-package-invalid',
74
+ expected: 'a browser-resolvable video URL descriptor',
75
+ hint: 'publish an http(s) video URL and let the host create the video element',
76
+ detail: { guid: envelope.guid, reason: 'video owner validation failed' },
77
+ });
78
+ },
79
+ },
80
+ };
@@ -0,0 +1,61 @@
1
+ // @forgeax/engine-graphics-extras — video high-perf upload capability probe
2
+ // (feat-20260623-world-space-video-asset M4 / w17).
3
+ //
4
+ // The single per-frame video upload path lives in the record stage
5
+ // (`render-system-record.ts` `videoTextureView`): it reads the host-registered
6
+ // VideoElementProvider (World Resource, D-1), uploads the current frame via
7
+ // `DynamicTextureStore.uploadFrame` (copyExternalImageToTexture), and fires the
8
+ // structured `VideoUploadUnsupportedError` on the engine error channel when a
9
+ // VideoPlayer entity can reach NEITHER the general path (no host element) NOR
10
+ // the high-perf path (AC-10 double-miss, charter P3). There is exactly ONE video
11
+ // upload/failure path — this module only contributes the capability probe the
12
+ // record stage consults to decide whether the reserved high-perf branch is
13
+ // available.
14
+ //
15
+ // Decision anchors:
16
+ // - requirements AC-09 (two paths left in place, capability probe explicit).
17
+ // - plan-strategy D-2 (grep-able capability branch, not a TODO).
18
+
19
+ /**
20
+ * Minimal device shape the high-perf capability probe inspects: the backend
21
+ * kind plus the (currently-absent) `importExternalTexture` method. Declared
22
+ * structurally so the probe stays decoupled from the full RhiDevice surface and
23
+ * unit tests drive it with a small object.
24
+ */
25
+ export interface VideoCapabilityDevice {
26
+ readonly caps: { readonly backendKind: 'webgpu' | 'wgpu-native' | 'wgpu-webgl2' | 'null' };
27
+ /**
28
+ * The WebGPU zero-copy video import entry point. forgeax exposes NO such RHI
29
+ * method today (research Finding 4 confirmed `importExternalTexture` grep=0),
30
+ * so this is always `undefined` — the probe's presence check is the explicit,
31
+ * grep-able boundary between the general path and the reserved high-perf path
32
+ * (D-2 / AC-09; OOS-5 keeps the upload body unimplemented).
33
+ */
34
+ readonly importExternalTexture?: unknown;
35
+ }
36
+
37
+ /**
38
+ * AC-09 / D-2 capability probe: decide whether the high-perf zero-copy
39
+ * GPUExternalTexture upload path is available for video this frame. This is the
40
+ * EXPLICIT reserved hook the AC-09 "two paths left in place" acceptance is
41
+ * checked against by code review — it is a real, grep-able code branch, not a
42
+ * TODO comment.
43
+ *
44
+ * The high-perf path requires BOTH a WebGPU backend (GPUExternalTexture is a
45
+ * browser-WebGPU feature) AND the RHI exposing an `importExternalTexture` entry
46
+ * point. The latter does not exist in forgeax today (OOS-5: importing the
47
+ * external texture + a `texture_external` MaterialParamType + WGSL external
48
+ * sampling is out of scope), so this probe ALWAYS returns false and the general
49
+ * `copyExternalImageToTexture` path (record stage) is the sole route end-to-end.
50
+ * The day a future feat lands `importExternalTexture`, this probe flips on for
51
+ * WebGPU backends without touching the call sites.
52
+ */
53
+ export function probeVideoHighPerfUpload(device: VideoCapabilityDevice | undefined): boolean {
54
+ if (device === undefined) return false;
55
+ // GPUExternalTexture is browser-WebGPU only (wgpu-native / wgpu-webgl2 lack it).
56
+ if (device.caps.backendKind !== 'webgpu') return false;
57
+ // Reserved high-perf hook: available only when the RHI exposes the import
58
+ // entry point. It is absent today (OOS-5), so this is the false-returning
59
+ // boundary the AC-09 two-path code review verifies.
60
+ return typeof device.importExternalTexture === 'function';
61
+ }
@@ -0,0 +1,51 @@
1
+ // @forgeax/engine-graphics-extras — VideoPlayer ECS component
2
+ // (feat-20260623-world-space-video-asset M3 / w7).
3
+ //
4
+ // Single-component play-state surface for world-space video textures. An
5
+ // entity carries one VideoPlayer referencing a VideoAsset via the `clip`
6
+ // handle; the per-entity playing / loop / currentTime live in independent
7
+ // archetype column slots so multiple entities share one VideoAsset GUID with
8
+ // independent play state (AC-05, research Finding 6).
9
+ //
10
+ // Schema vocab is the CLOSED ECS set (component.ts:315-333): clip uses
11
+ // `shared<VideoAsset>` (a branded u32 handle, NOT a bare GUID string), the
12
+ // three play-state fields use `bool` / `f32`. No opaque / object field type is
13
+ // introduced — the host HTMLVideoElement reference travels through the
14
+ // VideoElementProvider World Resource (plan-strategy D-1 / w9), never inside an
15
+ // ECS field (research Finding 5: schema vocab closed).
16
+ //
17
+ // Decision anchors:
18
+ // - requirements AC-04 (VideoPlayer registers via defineComponent; reference
19
+ // field is a handle type, not a bare GUID; play-state fields playing/loop/
20
+ // currentTime).
21
+ // - plan-strategy D-4 (clip: Handle<'VideoAsset','shared'>, brand string
22
+ // 'VideoAsset' mirrors AudioSource.clip: Handle<'AudioClipAsset','shared'>
23
+ // so AI users carry over the audio naming intuition — charter P4).
24
+ // - charter P1 (progressive disclosure: 4-field minimal surface).
25
+
26
+ import { defineComponent } from '@forgeax/engine-ecs';
27
+
28
+ /**
29
+ * VideoPlayer — attaches video play state to an entity.
30
+ *
31
+ * Fields:
32
+ * - `clip: shared<VideoAsset>` — handle to the VideoAsset describing the
33
+ * source URL (mirrors `AudioSource.clip`). Resolved into an
34
+ * HTMLVideoElement at frame time via the host `VideoElementProvider`
35
+ * (the engine never decodes video bytes — D-1).
36
+ * - `playing: bool` — whether the clip advances this frame (default false).
37
+ * - `loop: bool` — whether the clip restarts at end (default false).
38
+ * - `currentTime: f32` — playback head in seconds (default 0).
39
+ *
40
+ * Multiple entities may reference the same `clip` GUID with distinct
41
+ * play state — each entity's playing / loop / currentTime occupy independent
42
+ * archetype column slots (AC-05).
43
+ */
44
+ export const VideoPlayer = defineComponent('VideoPlayer', {
45
+ // The host HTMLVideoElement owns the live asset/presentation binding; the
46
+ // portable play controls remain in the simulation projection.
47
+ clip: { type: 'shared<VideoAsset>' },
48
+ playing: { type: 'bool', default: false },
49
+ loop: { type: 'bool', default: false },
50
+ currentTime: { type: 'f32', default: 0, transient: true },
51
+ });