@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.
- package/LICENSE +202 -0
- package/README.md +165 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/tile-bits.test.d.ts +2 -0
- package/dist/__tests__/tile-bits.test.d.ts.map +1 -0
- package/dist/__tests__/video-capability-probe.unit.test.d.ts +2 -0
- package/dist/__tests__/video-capability-probe.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-loader.unit.test.d.ts +2 -0
- package/dist/__tests__/video-loader.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-player-component.unit.test.d.ts +2 -0
- package/dist/__tests__/video-player-component.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-player-multi-entity.unit.test.d.ts +2 -0
- package/dist/__tests__/video-player-multi-entity.unit.test.d.ts.map +1 -0
- package/dist/glyph-layout.d.ts +48 -0
- package/dist/glyph-layout.d.ts.map +1 -0
- package/dist/glyph-mesh-bake.d.ts +30 -0
- package/dist/glyph-mesh-bake.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +306 -0
- package/dist/index.mjs.map +1 -0
- package/dist/tile-bits.d.ts +21 -0
- package/dist/tile-bits.d.ts.map +1 -0
- package/dist/tileset-decoder.d.ts +4 -0
- package/dist/tileset-decoder.d.ts.map +1 -0
- package/dist/video-element-provider.d.ts +41 -0
- package/dist/video-element-provider.d.ts.map +1 -0
- package/dist/video-loader.d.ts +4 -0
- package/dist/video-loader.d.ts.map +1 -0
- package/dist/video-player-system.d.ts +37 -0
- package/dist/video-player-system.d.ts.map +1 -0
- package/dist/video-player.d.ts +23 -0
- package/dist/video-player.d.ts.map +1 -0
- package/package.json +62 -0
- package/src/__tests__/tile-bits.test.ts +88 -0
- package/src/__tests__/video-capability-probe.unit.test.ts +130 -0
- package/src/__tests__/video-loader.unit.test.ts +27 -0
- package/src/__tests__/video-player-component.unit.test.ts +73 -0
- package/src/__tests__/video-player-multi-entity.unit.test.ts +75 -0
- package/src/glyph-layout.ts +208 -0
- package/src/glyph-mesh-bake.ts +151 -0
- package/src/index.ts +36 -0
- package/src/tile-bits.ts +77 -0
- package/src/tileset-decoder.ts +64 -0
- package/src/video-element-provider.ts +72 -0
- package/src/video-loader.ts +80 -0
- package/src/video-player-system.ts +61 -0
- 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
|
+
});
|