@triforge/context-pool-core 0.1.0
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/dist/adapters/ThreeContextAdapter.d.ts +47 -0
- package/dist/adapters/ThreeContextAdapter.d.ts.map +1 -0
- package/dist/adapters/ThreeContextAdapter.js +87 -0
- package/dist/adapters/ThreeContextAdapter.js.map +1 -0
- package/dist/core/ContextPool.d.ts +30 -0
- package/dist/core/ContextPool.d.ts.map +1 -0
- package/dist/core/ContextPool.js +84 -0
- package/dist/core/ContextPool.js.map +1 -0
- package/dist/core/types.d.ts +37 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import type { Rect } from '../core/types.js';
|
|
3
|
+
export interface ThreeSceneRegistration {
|
|
4
|
+
scene: THREE.Scene;
|
|
5
|
+
camera: THREE.Camera;
|
|
6
|
+
/** DOM element whose on-screen position/size determines this scene's viewport. */
|
|
7
|
+
anchor: Element;
|
|
8
|
+
/** Lower number = higher priority when the context budget is tight. */
|
|
9
|
+
priority?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ThreeContextAdapterConfig {
|
|
12
|
+
renderer: THREE.WebGLRenderer;
|
|
13
|
+
maxConcurrent: number;
|
|
14
|
+
/** Injectable for tests — defaults to the real window viewport. */
|
|
15
|
+
getVisibleRegion?: () => Rect;
|
|
16
|
+
/** Called when the shared context is lost — rendering is skipped until restored. */
|
|
17
|
+
onContextLost?: () => void;
|
|
18
|
+
onContextRestored?: () => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The one engine-specific piece of this package: wraps a single THREE.WebGLRenderer so
|
|
22
|
+
* any number of registered scenes can share it, instead of each needing its own
|
|
23
|
+
* WebGLRenderer (and its own real WebGL context). This is what actually fixes a
|
|
24
|
+
* "one WebGLRenderer per canvas" context-exhaustion bug — the ContextPool it wraps
|
|
25
|
+
* doesn't know or care that Three.js is involved at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ThreeContextAdapter {
|
|
28
|
+
private pool;
|
|
29
|
+
private renderer;
|
|
30
|
+
private registrations;
|
|
31
|
+
private contextLost;
|
|
32
|
+
private currentCanvasHeight;
|
|
33
|
+
private onLost;
|
|
34
|
+
private onRestored;
|
|
35
|
+
private handleContextLost;
|
|
36
|
+
private handleContextRestored;
|
|
37
|
+
constructor(config: ThreeContextAdapterConfig);
|
|
38
|
+
/** Register (or replace, if `id` already exists) a scene/camera pair positioned by `anchor`. */
|
|
39
|
+
registerScene(id: string, registration: ThreeSceneRegistration): void;
|
|
40
|
+
unregisterScene(id: string): void;
|
|
41
|
+
setMaxConcurrent(n: number): void;
|
|
42
|
+
/** Call once per animation frame. No-ops while the shared context is lost. */
|
|
43
|
+
renderFrame(): void;
|
|
44
|
+
private renderScissored;
|
|
45
|
+
dispose(): void;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=ThreeContextAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThreeContextAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/ThreeContextAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAEnC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAE5C,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAA;IAClB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAA;IACpB,kFAAkF;IAClF,MAAM,EAAE,OAAO,CAAA;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAA;IAC7B,aAAa,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC/B;AAMD;;;;;;GAMG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,aAAa,CAA4C;IACjE,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,iBAAiB,CAIxB;IACD,OAAO,CAAC,qBAAqB,CAG5B;gBAEW,MAAM,EAAE,yBAAyB;IAS7C,gGAAgG;IAChG,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,sBAAsB,GAAG,IAAI;IAmBrE,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKjC,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAIjC,8EAA8E;IAC9E,WAAW,IAAI,IAAI;IAWnB,OAAO,CAAC,eAAe;IAUvB,OAAO,IAAI,IAAI;CAIhB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ContextPool } from '../core/ContextPool.js';
|
|
2
|
+
function domRectToRect(r) {
|
|
3
|
+
return { x: r.left, y: r.top, width: r.width, height: r.height };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The one engine-specific piece of this package: wraps a single THREE.WebGLRenderer so
|
|
7
|
+
* any number of registered scenes can share it, instead of each needing its own
|
|
8
|
+
* WebGLRenderer (and its own real WebGL context). This is what actually fixes a
|
|
9
|
+
* "one WebGLRenderer per canvas" context-exhaustion bug — the ContextPool it wraps
|
|
10
|
+
* doesn't know or care that Three.js is involved at all.
|
|
11
|
+
*/
|
|
12
|
+
export class ThreeContextAdapter {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.registrations = new Map();
|
|
15
|
+
this.contextLost = false;
|
|
16
|
+
this.currentCanvasHeight = 0;
|
|
17
|
+
this.handleContextLost = (event) => {
|
|
18
|
+
event.preventDefault(); // required by the WebGL spec to allow eventual restoration
|
|
19
|
+
this.contextLost = true;
|
|
20
|
+
this.onLost();
|
|
21
|
+
};
|
|
22
|
+
this.handleContextRestored = () => {
|
|
23
|
+
this.contextLost = false;
|
|
24
|
+
this.onRestored();
|
|
25
|
+
};
|
|
26
|
+
this.renderer = config.renderer;
|
|
27
|
+
this.pool = new ContextPool({ maxConcurrent: config.maxConcurrent, getVisibleRegion: config.getVisibleRegion });
|
|
28
|
+
this.onLost = config.onContextLost ?? (() => { });
|
|
29
|
+
this.onRestored = config.onContextRestored ?? (() => { });
|
|
30
|
+
this.renderer.domElement.addEventListener('webglcontextlost', this.handleContextLost);
|
|
31
|
+
this.renderer.domElement.addEventListener('webglcontextrestored', this.handleContextRestored);
|
|
32
|
+
}
|
|
33
|
+
/** Register (or replace, if `id` already exists) a scene/camera pair positioned by `anchor`. */
|
|
34
|
+
registerScene(id, registration) {
|
|
35
|
+
this.registrations.set(id, registration);
|
|
36
|
+
this.pool.register({
|
|
37
|
+
id,
|
|
38
|
+
priority: registration.priority,
|
|
39
|
+
getAnchorRect: () => {
|
|
40
|
+
const current = this.registrations.get(id);
|
|
41
|
+
if (!current)
|
|
42
|
+
return null;
|
|
43
|
+
const r = current.anchor.getBoundingClientRect();
|
|
44
|
+
return r.width > 0 && r.height > 0 ? domRectToRect(r) : null;
|
|
45
|
+
},
|
|
46
|
+
render: (viewport) => {
|
|
47
|
+
const current = this.registrations.get(id);
|
|
48
|
+
if (!current)
|
|
49
|
+
return;
|
|
50
|
+
this.renderScissored(current, viewport);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
unregisterScene(id) {
|
|
55
|
+
this.registrations.delete(id);
|
|
56
|
+
this.pool.unregister(id);
|
|
57
|
+
}
|
|
58
|
+
setMaxConcurrent(n) {
|
|
59
|
+
this.pool.setMaxConcurrent(n);
|
|
60
|
+
}
|
|
61
|
+
/** Call once per animation frame. No-ops while the shared context is lost. */
|
|
62
|
+
renderFrame() {
|
|
63
|
+
if (this.contextLost)
|
|
64
|
+
return;
|
|
65
|
+
const canvasRect = domRectToRect(this.renderer.domElement.getBoundingClientRect());
|
|
66
|
+
if (canvasRect.width <= 0 || canvasRect.height <= 0)
|
|
67
|
+
return;
|
|
68
|
+
this.renderer.setScissorTest(false);
|
|
69
|
+
this.renderer.clear();
|
|
70
|
+
this.currentCanvasHeight = canvasRect.height;
|
|
71
|
+
this.pool.tick(canvasRect);
|
|
72
|
+
}
|
|
73
|
+
renderScissored(registration, viewport) {
|
|
74
|
+
// viewport is canvas-local, y-down (DOM convention). WebGL scissor/viewport are
|
|
75
|
+
// y-up from the bottom-left — flip using the canvas height captured this frame.
|
|
76
|
+
const glY = this.currentCanvasHeight - viewport.y - viewport.height;
|
|
77
|
+
this.renderer.setScissorTest(true);
|
|
78
|
+
this.renderer.setScissor(viewport.x, glY, viewport.width, viewport.height);
|
|
79
|
+
this.renderer.setViewport(viewport.x, glY, viewport.width, viewport.height);
|
|
80
|
+
this.renderer.render(registration.scene, registration.camera);
|
|
81
|
+
}
|
|
82
|
+
dispose() {
|
|
83
|
+
this.renderer.domElement.removeEventListener('webglcontextlost', this.handleContextLost);
|
|
84
|
+
this.renderer.domElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=ThreeContextAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThreeContextAdapter.js","sourceRoot":"","sources":["../../src/adapters/ThreeContextAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ContextPoolClient, ContextPoolConfig, Rect, TickResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Engine-agnostic scissor/viewport multiplexer. Clients register a DOM anchor (for
|
|
4
|
+
* positioning/visibility) and a render callback; the pool decides which subset of
|
|
5
|
+
* registered clients fit inside the context budget this tick and calls only theirs.
|
|
6
|
+
*
|
|
7
|
+
* This package never creates a WebGLRenderer or any other engine object — it only
|
|
8
|
+
* decides who gets to render this frame and where. Actually rendering (sharing one
|
|
9
|
+
* real renderer across all active clients via setScissor/setViewport) is the adapter's
|
|
10
|
+
* job (see ThreeContextAdapter) — that's the one piece of this package that's engine-specific.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ContextPool {
|
|
13
|
+
private clients;
|
|
14
|
+
private activeIds;
|
|
15
|
+
private nextOrder;
|
|
16
|
+
private maxConcurrent;
|
|
17
|
+
private getVisibleRegion;
|
|
18
|
+
constructor(config: ContextPoolConfig);
|
|
19
|
+
register(client: ContextPoolClient): void;
|
|
20
|
+
unregister(id: string): void;
|
|
21
|
+
setMaxConcurrent(n: number): void;
|
|
22
|
+
/**
|
|
23
|
+
* Advance one frame. `referenceRect` is typically the shared canvas's own
|
|
24
|
+
* getBoundingClientRect() — client viewports are returned relative to it, ready to
|
|
25
|
+
* hand straight to a GL-style setScissor/setViewport call (after any y-flip the
|
|
26
|
+
* engine adapter needs).
|
|
27
|
+
*/
|
|
28
|
+
tick(referenceRect: Rect): TickResult;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=ContextPool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextPool.d.ts","sourceRoot":"","sources":["../../src/core/ContextPool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AA2BxF;;;;;;;;;GASG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,gBAAgB,CAAY;gBAExB,MAAM,EAAE,iBAAiB;IAKrC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAKzC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAK5B,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAIjC;;;;;OAKG;IACH,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,UAAU;CAwCtC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function defaultVisibleRegion() {
|
|
2
|
+
if (typeof window === 'undefined')
|
|
3
|
+
return { x: 0, y: 0, width: Infinity, height: Infinity };
|
|
4
|
+
return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight };
|
|
5
|
+
}
|
|
6
|
+
function intersects(a, b) {
|
|
7
|
+
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
8
|
+
}
|
|
9
|
+
function relativeRect(rect, reference) {
|
|
10
|
+
return { x: rect.x - reference.x, y: rect.y - reference.y, width: rect.width, height: rect.height };
|
|
11
|
+
}
|
|
12
|
+
/** A non-finite or negative budget must degrade to "render nothing", never throw or admit unbounded clients. */
|
|
13
|
+
function sanitizeMaxConcurrent(n) {
|
|
14
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Engine-agnostic scissor/viewport multiplexer. Clients register a DOM anchor (for
|
|
18
|
+
* positioning/visibility) and a render callback; the pool decides which subset of
|
|
19
|
+
* registered clients fit inside the context budget this tick and calls only theirs.
|
|
20
|
+
*
|
|
21
|
+
* This package never creates a WebGLRenderer or any other engine object — it only
|
|
22
|
+
* decides who gets to render this frame and where. Actually rendering (sharing one
|
|
23
|
+
* real renderer across all active clients via setScissor/setViewport) is the adapter's
|
|
24
|
+
* job (see ThreeContextAdapter) — that's the one piece of this package that's engine-specific.
|
|
25
|
+
*/
|
|
26
|
+
export class ContextPool {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.clients = new Map();
|
|
29
|
+
this.activeIds = new Set();
|
|
30
|
+
this.nextOrder = 0;
|
|
31
|
+
this.maxConcurrent = sanitizeMaxConcurrent(config.maxConcurrent);
|
|
32
|
+
this.getVisibleRegion = config.getVisibleRegion ?? defaultVisibleRegion;
|
|
33
|
+
}
|
|
34
|
+
register(client) {
|
|
35
|
+
const existing = this.clients.get(client.id);
|
|
36
|
+
this.clients.set(client.id, { client, order: existing?.order ?? this.nextOrder++ });
|
|
37
|
+
}
|
|
38
|
+
unregister(id) {
|
|
39
|
+
this.clients.delete(id);
|
|
40
|
+
this.activeIds.delete(id);
|
|
41
|
+
}
|
|
42
|
+
setMaxConcurrent(n) {
|
|
43
|
+
this.maxConcurrent = sanitizeMaxConcurrent(n);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Advance one frame. `referenceRect` is typically the shared canvas's own
|
|
47
|
+
* getBoundingClientRect() — client viewports are returned relative to it, ready to
|
|
48
|
+
* hand straight to a GL-style setScissor/setViewport call (after any y-flip the
|
|
49
|
+
* engine adapter needs).
|
|
50
|
+
*/
|
|
51
|
+
tick(referenceRect) {
|
|
52
|
+
const visibleRegion = this.getVisibleRegion();
|
|
53
|
+
const hidden = [];
|
|
54
|
+
const visible = [];
|
|
55
|
+
for (const [id, entry] of this.clients) {
|
|
56
|
+
const rect = entry.client.getAnchorRect();
|
|
57
|
+
if (!rect || rect.width <= 0 || rect.height <= 0 || !intersects(rect, visibleRegion)) {
|
|
58
|
+
hidden.push(id);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
visible.push({ id, order: entry.order, priority: entry.client.priority ?? entry.order, rect });
|
|
62
|
+
}
|
|
63
|
+
const byPriority = (a, b) => a.priority - b.priority || a.order - b.order;
|
|
64
|
+
// Sticky selection: clients already active last tick keep their slot (if still visible
|
|
65
|
+
// and still within budget) before any newly-visible client takes a remaining slot —
|
|
66
|
+
// this avoids visible flicker from re-ordering who renders every single frame.
|
|
67
|
+
const stillActive = visible.filter((v) => this.activeIds.has(v.id)).sort(byPriority);
|
|
68
|
+
const newlyVisible = visible.filter((v) => !this.activeIds.has(v.id)).sort(byPriority);
|
|
69
|
+
const kept = stillActive.slice(0, this.maxConcurrent);
|
|
70
|
+
const added = newlyVisible.slice(0, Math.max(0, this.maxConcurrent - kept.length));
|
|
71
|
+
const activeList = [...kept, ...added];
|
|
72
|
+
const activeIdSet = new Set(activeList.map((v) => v.id));
|
|
73
|
+
this.activeIds = activeIdSet;
|
|
74
|
+
const skipped = visible.filter((v) => !activeIdSet.has(v.id)).map((v) => v.id);
|
|
75
|
+
for (const v of activeList) {
|
|
76
|
+
const entry = this.clients.get(v.id);
|
|
77
|
+
if (!entry)
|
|
78
|
+
continue;
|
|
79
|
+
entry.client.render(relativeRect(v.rect, referenceRect));
|
|
80
|
+
}
|
|
81
|
+
return { active: activeList.map((v) => v.id), skipped, hidden };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=ContextPool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextPool.js","sourceRoot":"","sources":["../../src/core/ContextPool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Top-left-origin, y-down pixel rectangle — DOM convention. Engines that need a
|
|
2
|
+
* bottom-left/y-up convention (e.g. GL scissor/viewport) convert at their own adapter,
|
|
3
|
+
* not here — this package makes no GL assumptions. */
|
|
4
|
+
export interface Rect {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ContextPoolClient {
|
|
11
|
+
id: string;
|
|
12
|
+
/** Current on-screen position/size in page coordinates, or null if not currently measurable (detached, display:none). */
|
|
13
|
+
getAnchorRect(): Rect | null;
|
|
14
|
+
/** Lower number = higher priority when the budget is tight. Defaults to registration order if omitted. */
|
|
15
|
+
priority?: number;
|
|
16
|
+
/** Called once per tick, only for clients selected as active this frame. `viewport` is already
|
|
17
|
+
* converted to be relative to the reference rect passed into ContextPool.tick(). */
|
|
18
|
+
render(viewport: Rect): void;
|
|
19
|
+
}
|
|
20
|
+
export interface ContextPoolConfig {
|
|
21
|
+
/** Max simultaneous active clients — typically RenderBudgetPlan.maxConcurrentContexts from
|
|
22
|
+
* @triforge/render-budget-core, though this package never imports that one directly (each
|
|
23
|
+
* package does one job; the consumer wires the number across). */
|
|
24
|
+
maxConcurrent: number;
|
|
25
|
+
/** Region a client's anchor must intersect to count as "visible" at all. Defaults to the
|
|
26
|
+
* real window viewport; injectable for tests. */
|
|
27
|
+
getVisibleRegion?: () => Rect;
|
|
28
|
+
}
|
|
29
|
+
export interface TickResult {
|
|
30
|
+
/** Clients rendered this tick. */
|
|
31
|
+
active: string[];
|
|
32
|
+
/** Clients visible but over budget — not rendered this tick. */
|
|
33
|
+
skipped: string[];
|
|
34
|
+
/** Clients not visible at all (anchor null, zero-sized, or outside the visible region). */
|
|
35
|
+
hidden: string[];
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;sDAEsD;AACtD,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,yHAAyH;IACzH,aAAa,IAAI,IAAI,GAAG,IAAI,CAAA;IAC5B,0GAA0G;IAC1G,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;wFACoF;IACpF,MAAM,CAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC;;sEAEkE;IAClE,aAAa,EAAE,MAAM,CAAA;IACrB;qDACiD;IACjD,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,2FAA2F;IAC3F,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,IAAI,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC7F,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@triforge/context-pool-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Engine-agnostic scissor/viewport multiplexer — share one real rendering context across many logical canvases, enforcing a context-count budget instead of creating one context per canvas",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"test": "node test/run-tests.js",
|
|
16
|
+
"bench": "node bench/run-bench.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"context-pool",
|
|
20
|
+
"webgl",
|
|
21
|
+
"scissor",
|
|
22
|
+
"viewport",
|
|
23
|
+
"three.js"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"three": ">=0.128.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"three": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/three": "^0.165.0",
|
|
36
|
+
"three": "^0.165.0",
|
|
37
|
+
"typescript": "^5.3.0"
|
|
38
|
+
},
|
|
39
|
+
"author": "Aftab Ibrahim Kazi",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/AftabIbrahimKazi/triforge.git",
|
|
43
|
+
"directory": "context-pool-core"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/AftabIbrahimKazi/triforge#readme",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"exports": {
|
|
50
|
+
".": {
|
|
51
|
+
"import": "./dist/index.js",
|
|
52
|
+
"types": "./dist/index.d.ts"
|
|
53
|
+
},
|
|
54
|
+
"./three": {
|
|
55
|
+
"import": "./dist/adapters/ThreeContextAdapter.js",
|
|
56
|
+
"types": "./dist/adapters/ThreeContextAdapter.d.ts"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|