@three-blocks/devtools 11.0.0 → 11.1.0-alpha.395.g9b8ccb48a515
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/CHANGELOG.md +18 -0
- package/NOTICE +26 -0
- package/README.md +117 -170
- package/dist/asset-optimize-YJGWKD4Y.js +1 -0
- package/dist/asset-transforms-ZVZ4RD4L.js +11 -0
- package/dist/browser/shader-capture.js +2 -2
- package/dist/browser-smoke-4MYMHKWB.js +1 -0
- package/dist/chunk-4SVF5BRM.js +2 -0
- package/dist/chunk-7X4DHW36.js +1 -0
- package/dist/chunk-BBODS5RH.js +5 -0
- package/dist/{chunk-IILE5RBI.js → chunk-CMMYXGQ3.js} +1 -2
- package/dist/chunk-DCLDEPPB.js +253 -0
- package/dist/{chunk-5L2VS3BN.js → chunk-ECQE4D2U.js} +1 -2
- package/dist/chunk-IWQV456Z.js +3 -0
- package/dist/chunk-LSH64HEU.js +80 -0
- package/dist/{chunk-X5OFYHIY.js → chunk-MIJXPQPM.js} +2 -3
- package/dist/chunk-NDIPQLTU.js +16 -0
- package/dist/{chunk-NPLE7PPF.js → chunk-NJITVI23.js} +1 -2
- package/dist/chunk-O5FU2ASS.js +16 -0
- package/dist/chunk-QQKHDH26.js +1 -0
- package/dist/chunk-S5LI2C2H.js +5 -0
- package/dist/chunk-SBUKHO24.js +1 -0
- package/dist/chunk-TK566QET.js +1 -0
- package/dist/{chunk-65VEJRRT.js → chunk-WNSGAV6N.js} +1 -2
- package/dist/chunk-ZG3LO3VB.js +1 -0
- package/dist/{chunk-UDUICSME.js → chunk-ZTHZE36A.js} +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +26 -26
- package/dist/environment-bake-ON5I27N7.js +1 -0
- package/dist/index.d.ts +24 -12
- package/dist/index.js +1 -279
- package/dist/{shader-autowire-SPE5DM2X.js → shader-autowire-D2EOG6GF.js} +1 -2
- package/dist/shader-browser-6ICBHK3S.js +1 -0
- package/dist/shader-capture-server.d.ts +1 -1
- package/dist/shader-capture-server.js +1 -1
- package/dist/{shader-config-EOFU5D3Y.js → shader-config-AKZEKIRB.js} +1 -2
- package/dist/{shader-parity-QXJWR6OZ.js → shader-parity-7DEPQEDI.js} +1 -1
- package/dist/{shader-project-runner-S2AID2RJ.js → shader-project-runner-NV4GKHDX.js} +1 -2
- package/dist/shader-watch-JBUA4LP5.js +1 -0
- package/dist/shader-workflow-5EHHR32D.js +1 -0
- package/dist/status-GDVSLQWV.js +1 -0
- package/dist/text-atlas-YYWV7S6F.js +1 -0
- package/dist/transform.cjs +1 -1
- package/dist/transform.js +1 -1
- package/dist/webpack-loader.cjs +1 -1
- package/dist/webpack-loader.js +1 -1
- package/node/index.d.mts +70 -0
- package/node/index.mjs +407 -0
- package/node/ipc.mjs +31 -0
- package/node/layout.mjs +546 -0
- package/node/loader.mjs +103 -0
- package/node/media.mjs +454 -0
- package/node/recording.mjs +67 -0
- package/node/worker.mjs +1314 -0
- package/package.json +27 -11
- package/dist/asset-optimize-GG72JCOX.js +0 -6
- package/dist/browser-smoke-KFF7XV7F.js +0 -254
- package/dist/chunk-45AVKQQK.js +0 -80
- package/dist/chunk-5FT6AZD6.js +0 -82
- package/dist/chunk-7N6TU4JG.js +0 -17
- package/dist/chunk-EI7MMDWY.js +0 -1
- package/dist/chunk-HOZDD6PW.js +0 -2
- package/dist/chunk-LKXY5YU2.js +0 -17
- package/dist/chunk-MIUIIPS4.js +0 -1
- package/dist/chunk-NF4RFSUJ.js +0 -18
- package/dist/chunk-QJFPNM6W.js +0 -2
- package/dist/chunk-ZT6T2W4B.js +0 -2
- package/dist/chunk-ZZLAZ7GJ.js +0 -6
- package/dist/environment-bake-BGMGFAVK.js +0 -4
- package/dist/shader-browser-H5CB2ZQX.js +0 -2
- package/dist/shader-browser-UXXR6QI2.js +0 -1
- package/dist/shader-parity-XPKQQK26.js +0 -2
- package/dist/shader-watch-EORZ2SEC.js +0 -2
- package/dist/shader-workflow-7NQFDQI5.js +0 -2
- package/dist/status-PTBB4SW2.js +0 -2
- package/dist/text-atlas-UORTLOQJ.js +0 -2
- /package/dist/{chunk-TRHSN2DJ.cjs → chunk-LMLJIVNY.cjs} +0 -0
package/node/worker.mjs
ADDED
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
import { register } from 'node:module';
|
|
2
|
+
import { resolveObjectURL } from 'node:buffer';
|
|
3
|
+
import { runInThisContext } from 'node:vm';
|
|
4
|
+
import { formatWithOptions } from 'node:util';
|
|
5
|
+
import { performance as nativePerformance } from 'node:perf_hooks';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { installDOM, Canvas, Image, ImageBitmap, registerImageDecoder } from '@onirenaud/node-webgl';
|
|
8
|
+
import { parentPort, workerData, Worker as Thread, MessageChannel, receiveMessageOnPort } from 'node:worker_threads';
|
|
9
|
+
import { createCanvas, loadImage, Image as SkiaImage, ImageData as SkiaImageData } from '@napi-rs/canvas';
|
|
10
|
+
import jpeg from '@cwasm/jpeg-turbo';
|
|
11
|
+
import { installMedia, installExternalTextures } from './media.mjs';
|
|
12
|
+
import { parseHTML, NodeFilter } from 'linkedom';
|
|
13
|
+
import * as domTypes from 'linkedom';
|
|
14
|
+
import { PNG } from 'pngjs';
|
|
15
|
+
import cssEscape from 'css.escape';
|
|
16
|
+
import { CookieJar } from 'tough-cookie';
|
|
17
|
+
import { patchNativeThreeCode } from './loader.mjs';
|
|
18
|
+
import { encodeIPC, decodeIPC } from './ipc.mjs';
|
|
19
|
+
|
|
20
|
+
const jsonIPC = process.env.THREE_BLOCKS_NODE_JSON_IPC === '1';
|
|
21
|
+
|
|
22
|
+
let pageURL;
|
|
23
|
+
let pageViewport;
|
|
24
|
+
let cookieJar;
|
|
25
|
+
let gpuEnabled = true;
|
|
26
|
+
let paintPage;
|
|
27
|
+
let importMap = {};
|
|
28
|
+
let surfaceId = 0;
|
|
29
|
+
let networkId = 0;
|
|
30
|
+
let importScopes = {};
|
|
31
|
+
const handles = new Map();
|
|
32
|
+
let handleId = 0;
|
|
33
|
+
const wallNow = nativePerformance.now.bind( nativePerformance );
|
|
34
|
+
const transferredSurfaces = new Map();
|
|
35
|
+
let routes = false;
|
|
36
|
+
let routeId = 0;
|
|
37
|
+
const routeRequests = new Map();
|
|
38
|
+
const surfaces = new Map();
|
|
39
|
+
const canvasTargetBackends = new WeakSet();
|
|
40
|
+
const send = message => {
|
|
41
|
+
|
|
42
|
+
if ( parentPort ) parentPort.postMessage( { native: message } );
|
|
43
|
+
else if ( process.connected ) process.send( jsonIPC ? encodeIPC( message ) : message );
|
|
44
|
+
|
|
45
|
+
};
|
|
46
|
+
const failure = error => send( { event: 'pageerror', error: error?.stack ?? error?.message ?? String( error ) } );
|
|
47
|
+
const unsupported = feature => { throw new Error( `Unsupported native rendering capability: ${feature}` ); };
|
|
48
|
+
process.on( 'uncaughtException', failure );
|
|
49
|
+
process.on( 'unhandledRejection', error => send( { event: 'unhandledrejection', error: error?.stack ?? String( error ) } ) );
|
|
50
|
+
for ( const level of [ 'log', 'info', 'warn', 'error', 'debug' ] ) console[ level ] = ( ...args ) => send( {
|
|
51
|
+
event: 'console', level: level === 'warn' ? 'warning' : level, text: formatWithOptions( { depth: 6 }, ...args ),
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
const originalFetch = globalThis.fetch;
|
|
55
|
+
const OriginalRequest = globalThis.Request;
|
|
56
|
+
async function routedFetch( input, init, resourceType ) {
|
|
57
|
+
|
|
58
|
+
const request = new Request( input instanceof Request ? input : new URL( input, pageURL ), init );
|
|
59
|
+
if ( routes ) {
|
|
60
|
+
|
|
61
|
+
const id = ++ routeId;
|
|
62
|
+
const response = await new Promise( resolve => {
|
|
63
|
+
|
|
64
|
+
routeRequests.set( id, resolve );
|
|
65
|
+
void request.clone().arrayBuffer().then( body => send( {
|
|
66
|
+
event: 'route', id, url: request.url, method: request.method, headers: Object.fromEntries( request.headers ), resourceType, body: Buffer.from( body ),
|
|
67
|
+
} ) );
|
|
68
|
+
|
|
69
|
+
} );
|
|
70
|
+
if ( response?.error ) throw new Error( `Request aborted: ${response.error} (${request.url})` );
|
|
71
|
+
if ( response ) return new Response( response.body ?? null, {
|
|
72
|
+
status: response.status ?? 200,
|
|
73
|
+
headers: { ...response.headers, ...( response.contentType ? { 'content-type': response.contentType } : {} ) },
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
return originalFetch( request );
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fetchPage( input, init, resourceType = 'fetch' ) {
|
|
82
|
+
|
|
83
|
+
let request = new Request( input instanceof Request ? input : new URL( input, pageURL ), init );
|
|
84
|
+
const redirectMode = request.redirect;
|
|
85
|
+
for ( let redirects = 0; ; redirects ++ ) {
|
|
86
|
+
const sameOrigin = new URL( request.url ).origin === new URL( pageURL ).origin;
|
|
87
|
+
// Authenticated cross-origin capture needs a CORS implementation, not Node's
|
|
88
|
+
// unrestricted fetch. Fail closed until an actual capture requires it.
|
|
89
|
+
if ( ! sameOrigin && request.credentials === 'include' ) unsupported( 'cross-origin credentialed fetch' );
|
|
90
|
+
const credentials = /^https?:/.test( request.url ) && sameOrigin && request.credentials !== 'omit';
|
|
91
|
+
const headers = new Headers( request.headers );
|
|
92
|
+
if ( credentials ) {
|
|
93
|
+
const cookie = cookieJar.getCookieStringSync( request.url );
|
|
94
|
+
if ( cookie ) headers.set( 'cookie', cookie );
|
|
95
|
+
}
|
|
96
|
+
if ( resourceType !== 'document' && ! [ 'GET', 'HEAD' ].includes( request.method ) ) headers.set( 'origin', new URL( pageURL ).origin );
|
|
97
|
+
request = new Request( request, { headers, redirect: 'manual' } );
|
|
98
|
+
const replay = request.clone();
|
|
99
|
+
const id = `${parentPort ? 'thread' : 'page'}-${++ networkId}-${pageURL}`;
|
|
100
|
+
send( { event: 'network', phase: 'request', id, url: request.url, method: request.method, headers: Object.fromEntries( request.headers ), resourceType } );
|
|
101
|
+
try {
|
|
102
|
+
|
|
103
|
+
const response = await routedFetch( request, undefined, resourceType );
|
|
104
|
+
if ( credentials ) {
|
|
105
|
+
for ( const cookie of response.headers.getSetCookie() ) cookieJar.setCookieSync( cookie, request.url, { ignoreError: true } );
|
|
106
|
+
if ( response.headers.has( 'set-cookie' ) ) send( { event: 'cookies', cookies: cookieJar.serializeSync() } );
|
|
107
|
+
}
|
|
108
|
+
Object.defineProperty( response, 'url', { value: request.url, configurable: true } );
|
|
109
|
+
Object.defineProperty( response, 'redirected', { value: redirects > 0, configurable: true } );
|
|
110
|
+
send( { event: 'network', phase: 'response', id, status: response.status, statusText: response.statusText, headers: Object.fromEntries( response.headers ) } );
|
|
111
|
+
// Drain a clone without delaying the app's streaming reader.
|
|
112
|
+
void response.clone().arrayBuffer().then( body => send( { event: 'network', phase: 'finished', id, body: Buffer.from( body ) } ), error => send( { event: 'network', phase: 'failed', id, error: String( error ) } ) );
|
|
113
|
+
if ( [ 301, 302, 303, 307, 308 ].includes( response.status ) && response.headers.has( 'location' ) && redirectMode !== 'manual' ) {
|
|
114
|
+
if ( redirectMode === 'error' || redirects >= 20 ) throw new TypeError( 'Fetch redirect rejected.' );
|
|
115
|
+
const next = new URL( response.headers.get( 'location' ), request.url );
|
|
116
|
+
headers.delete( 'cookie' );
|
|
117
|
+
if ( next.origin !== new URL( request.url ).origin ) headers.delete( 'authorization' );
|
|
118
|
+
const get = response.status === 303 && request.method !== 'HEAD' || [ 301, 302 ].includes( response.status ) && request.method === 'POST';
|
|
119
|
+
if ( get ) { headers.delete( 'content-type' ); headers.delete( 'content-length' ); }
|
|
120
|
+
request = new Request( next, { method: get ? 'GET' : request.method, headers, credentials: request.credentials, signal: request.signal,
|
|
121
|
+
...( ! get && ! [ 'GET', 'HEAD' ].includes( request.method ) ? { body: await replay.arrayBuffer() } : {} ) } );
|
|
122
|
+
await response.body?.cancel();
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
return response;
|
|
126
|
+
|
|
127
|
+
} catch ( error ) { send( { event: 'network', phase: 'failed', id, error: String( error ) } ); throw error; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function run( source, filename = pageURL ) {
|
|
133
|
+
|
|
134
|
+
return runInThisContext( patchNativeThreeCode( source ), {
|
|
135
|
+
filename,
|
|
136
|
+
importModuleDynamically: specifier => import( new URL( specifier, pageURL ).href ),
|
|
137
|
+
} );
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function navigate( { url, viewport, scripts: initialScripts = [], routes: intercept, thread, imports = {}, source, blob, scopes = {}, storage = {}, media = [], cookies, gpu = true } ) {
|
|
142
|
+
|
|
143
|
+
pageURL = url; pageViewport = viewport; routes = intercept; importMap = imports;
|
|
144
|
+
cookieJar = cookies ? CookieJar.deserializeSync( cookies ) : new CookieJar();
|
|
145
|
+
gpuEnabled = gpu;
|
|
146
|
+
globalThis.Request = class Request extends OriginalRequest {
|
|
147
|
+
|
|
148
|
+
constructor( input, init ) { super( input instanceof OriginalRequest ? input : new URL( input, pageURL ), init ); }
|
|
149
|
+
|
|
150
|
+
};
|
|
151
|
+
globalThis.fetch = fetchPage;
|
|
152
|
+
const response = thread ? null : await fetchPage( url, undefined, 'document' );
|
|
153
|
+
if ( response && ! response.ok ) throw new Error( `Page ${url}: HTTP ${response.status}` );
|
|
154
|
+
if ( response?.url ) { url = pageURL = response.url; send( { event: 'url', url } ); }
|
|
155
|
+
const html = response ? await response.text() : '<html><body></body></html>';
|
|
156
|
+
const { document, window: dom } = parseHTML( html );
|
|
157
|
+
// HTML entity callbacks in LinkeDOM split adjacent text nodes. Browsers merge
|
|
158
|
+
// them, and React hydrates against that single text node.
|
|
159
|
+
dom.Node.prototype.normalize = function () {
|
|
160
|
+
for ( let child = this.firstChild; child; ) {
|
|
161
|
+
if ( child.nodeType === 3 ) {
|
|
162
|
+
while ( child.nextSibling?.nodeType === 3 ) { child.data += child.nextSibling.data; child.nextSibling.remove(); }
|
|
163
|
+
const next = child.nextSibling; if ( child.data === '' ) child.remove(); child = next;
|
|
164
|
+
} else { child.normalize(); child = child.nextSibling; }
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
document.normalize();
|
|
168
|
+
// LinkeDOM assumes head/body adjacency; valid pages can put styles between them.
|
|
169
|
+
for ( const tag of [ 'head', 'body' ] ) {
|
|
170
|
+
|
|
171
|
+
const get = Object.getOwnPropertyDescriptor( Object.getPrototypeOf( document ), tag ).get;
|
|
172
|
+
Object.defineProperty( document, tag, { get: () => document.querySelector( tag ) ?? get.call( document ) } );
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
installDOM( { fetch: false, innerWidth: viewport.width, innerHeight: viewport.height, frameInterval: viewport.frameIntervalMs ?? 0 } );
|
|
176
|
+
installAnimationFrames( viewport.frameIntervalMs ?? 0 );
|
|
177
|
+
globalThis.__THREE_BLOCKS_NATIVE_CANVAS_TARGET__ = installCanvasTargets;
|
|
178
|
+
FileReader.prototype.fire = function ( type ) {
|
|
179
|
+
|
|
180
|
+
const event = new Event( type );
|
|
181
|
+
// node-webgl 0.2.0 calls property handlers before dispatch sets their target.
|
|
182
|
+
Object.defineProperties( event, { target: { value: this }, currentTarget: { value: this } } );
|
|
183
|
+
this[ `on${type}` ]?.call( this, event );
|
|
184
|
+
this.dispatchEvent( event );
|
|
185
|
+
|
|
186
|
+
};
|
|
187
|
+
const locationURL = new URL( url );
|
|
188
|
+
const navigateTo = value => { setTimeout( () => send( { event: 'navigate', url: new URL( value, pageURL ).href } ), 0 ); };
|
|
189
|
+
const location = new Proxy( locationURL, {
|
|
190
|
+
get( target, key ) { const value = Reflect.get( target, key ); return typeof value === 'function' ? value.bind( target ) : value; },
|
|
191
|
+
set( target, key, value ) {
|
|
192
|
+
if ( [ 'href', 'pathname', 'search', 'hash', 'host', 'hostname', 'port', 'protocol' ].includes( key ) ) {
|
|
193
|
+
const next = key === 'href' ? new URL( value, target.href ) : new URL( target.href );
|
|
194
|
+
if ( key !== 'href' ) next[ key ] = value;
|
|
195
|
+
navigateTo( next ); return true;
|
|
196
|
+
}
|
|
197
|
+
return Reflect.set( target, key, value );
|
|
198
|
+
},
|
|
199
|
+
} );
|
|
200
|
+
Object.assign( globalThis, { NodeFilter, document, location } );
|
|
201
|
+
// ponytail: name lookups are snapshots; add a live collection if a caller retains one across mutations.
|
|
202
|
+
document.getElementsByName = name => [ ...document.querySelectorAll( '[name]' ) ].filter( element => element.getAttribute( 'name' ) === String( name ) );
|
|
203
|
+
document.queryCommandSupported = () => false;
|
|
204
|
+
globalThis.CSS = { escape: cssEscape };
|
|
205
|
+
Object.defineProperties( document, {
|
|
206
|
+
cookie: { get: () => cookieJar.getCookieStringSync( pageURL, { http: false } ), set: value => {
|
|
207
|
+
cookieJar.setCookieSync( String( value ), pageURL, { http: false, ignoreError: true } );
|
|
208
|
+
send( { event: 'cookies', cookies: cookieJar.serializeSync() } );
|
|
209
|
+
} },
|
|
210
|
+
location: { get: () => globalThis.location },
|
|
211
|
+
} );
|
|
212
|
+
globalThis.Window = class Window { static [ Symbol.hasInstance ]( value ) { return value === globalThis; } };
|
|
213
|
+
Object.defineProperties( navigator, { userAgent: { configurable: true, value: `Node.js/${process.versions.node} ANGLE/Dawn` }, platform: { configurable: true, value: process.platform } } );
|
|
214
|
+
navigator.getGamepads = () => []; // Captures have no connected input devices.
|
|
215
|
+
globalThis.__THREE_BLOCKS_NODE_MEMORY__ = () => process.memoryUsage();
|
|
216
|
+
globalThis[ Symbol.for( 'three-blocks.native.event-loop-census' ) ] = () => {
|
|
217
|
+
|
|
218
|
+
const entries = []; let previous = wallNow();
|
|
219
|
+
const sample = () => {
|
|
220
|
+
|
|
221
|
+
const now = wallNow(), duration = now - previous - 1;
|
|
222
|
+
if ( duration >= 50 ) entries.push( { startTime: previous + 1, duration } );
|
|
223
|
+
previous = now;
|
|
224
|
+
|
|
225
|
+
};
|
|
226
|
+
const timer = setInterval( sample, 1 ); timer.unref();
|
|
227
|
+
return { supported: true, source: 'node-event-loop-delay', entries, flush: sample, disconnect: () => clearInterval( timer ) };
|
|
228
|
+
|
|
229
|
+
};
|
|
230
|
+
for ( const name of [ 'Node', 'Element', 'HTMLElement', 'HTMLCanvasElement', 'HTMLDivElement', 'HTMLDetailsElement', 'Document', 'DOMParser', 'customElements', 'CSSStyleSheet', 'ShadowRoot' ] ) globalThis[ name ] = dom[ name ];
|
|
231
|
+
for ( const [ name, type ] of Object.entries( domTypes ) ) if ( /^HTML\w+Element$/.test( name ) ) globalThis[ name ] = type;
|
|
232
|
+
Object.defineProperty( dom.HTMLScriptElement.prototype, 'src', {
|
|
233
|
+
configurable: true,
|
|
234
|
+
get() { const value = this.getAttribute( 'src' ); return value === null ? '' : new URL( value, pageURL ).href; },
|
|
235
|
+
set( value ) { this.setAttribute( 'src', String( value ) ); },
|
|
236
|
+
} );
|
|
237
|
+
const elementAttributes = Object.getOwnPropertyDescriptor( dom.Element.prototype, 'attributes' );
|
|
238
|
+
const liveAttributes = new WeakMap();
|
|
239
|
+
Object.defineProperty( dom.Element.prototype, 'attributes', { ...elementAttributes, get() {
|
|
240
|
+
|
|
241
|
+
if ( ! liveAttributes.has( this ) ) liveAttributes.set( this, new Proxy( {}, { get: ( _target, name ) => {
|
|
242
|
+
|
|
243
|
+
const snapshot = elementAttributes.get.call( this ), value = Reflect.get( snapshot, name );
|
|
244
|
+
return typeof value === 'function' ? value.bind( snapshot ) : value;
|
|
245
|
+
|
|
246
|
+
} } ) );
|
|
247
|
+
return liveAttributes.get( this );
|
|
248
|
+
|
|
249
|
+
} } );
|
|
250
|
+
const elementStyle = Object.getOwnPropertyDescriptor( dom.Element.prototype, 'style' );
|
|
251
|
+
const styleDeclarations = new WeakMap();
|
|
252
|
+
Object.defineProperty( dom.Element.prototype, 'style', { ...elementStyle, get() {
|
|
253
|
+
|
|
254
|
+
if ( ! styleDeclarations.has( this ) ) styleDeclarations.set( this, new Proxy( elementStyle.get.call( this ), { get( style, name ) {
|
|
255
|
+
|
|
256
|
+
if ( name === 'getPropertyValue' ) return property => String( style.getPropertyValue( property ) ?? '' ).replace( /\s*!important\s*$/, '' );
|
|
257
|
+
if ( name === 'getPropertyPriority' ) return property => /!important\s*$/.test( style.getPropertyValue( property ) ?? '' ) ? 'important' : '';
|
|
258
|
+
if ( name === 'setProperty' ) return ( property, value, priority = '' ) => { if ( priority === '' || priority === 'important' ) style.setProperty( property, `${value}${priority ? ' !important' : ''}` ); };
|
|
259
|
+
return Reflect.get( style, name ) ?? ( typeof name === 'string' ? '' : undefined );
|
|
260
|
+
|
|
261
|
+
} } ) );
|
|
262
|
+
return styleDeclarations.get( this );
|
|
263
|
+
|
|
264
|
+
} } );
|
|
265
|
+
const innerHTML = Object.getOwnPropertyDescriptor( dom.Element.prototype, 'innerHTML' );
|
|
266
|
+
Object.defineProperty( dom.Element.prototype, 'innerHTML', { ...innerHTML, set( value ) { innerHTML.set.call( this, value === null ? '' : String( value ) ); this.normalize(); } } );
|
|
267
|
+
Object.defineProperty( dom.HTMLElement.prototype, 'innerText', {
|
|
268
|
+
...Object.getOwnPropertyDescriptor( dom.Element.prototype, 'innerText' ),
|
|
269
|
+
set( value ) {
|
|
270
|
+
|
|
271
|
+
this.replaceChildren();
|
|
272
|
+
for ( const [ index, line ] of ( value === null ? '' : String( value ) ).split( /\r\n|\r|\n/ ).entries() ) {
|
|
273
|
+
|
|
274
|
+
if ( index ) this.appendChild( document.createElement( 'br' ) );
|
|
275
|
+
this.appendChild( document.createTextNode( line ) );
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
},
|
|
280
|
+
} );
|
|
281
|
+
Object.defineProperty( dom.ShadowRoot.prototype, 'textContent', Object.getOwnPropertyDescriptor( dom.Element.prototype, 'textContent' ) );
|
|
282
|
+
const checkedInputs = new WeakMap();
|
|
283
|
+
Object.defineProperty( dom.HTMLInputElement.prototype, 'checked', { configurable: true,
|
|
284
|
+
get() { return checkedInputs.get( this ) ?? this.hasAttribute( 'checked' ); },
|
|
285
|
+
set( value ) { checkedInputs.set( this, Boolean( value ) ); },
|
|
286
|
+
} );
|
|
287
|
+
dom.HTMLElement.prototype.click = function () {
|
|
288
|
+
if ( this.disabled ) return;
|
|
289
|
+
const checkbox = this.localName === 'input' && this.type === 'checkbox';
|
|
290
|
+
const previous = this.checked;
|
|
291
|
+
if ( checkbox ) checkedInputs.set( this, ! previous );
|
|
292
|
+
if ( ! this.dispatchEvent( new dom.Event( 'click', { bubbles: true, cancelable: true } ) ) ) {
|
|
293
|
+
if ( checkbox ) checkedInputs.set( this, previous );
|
|
294
|
+
} else if ( checkbox ) {
|
|
295
|
+
for ( const type of [ 'input', 'change' ] ) this.dispatchEvent( new dom.Event( type, { bubbles: true } ) );
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
// ponytail: the gallery's only WAAPI effect is a linear rotation. Reject
|
|
299
|
+
// other effects until a capture actually needs a wider animation engine.
|
|
300
|
+
dom.Element.prototype.animate = function ( keyframes, options ) {
|
|
301
|
+
|
|
302
|
+
const angles = Array.isArray( keyframes ) && keyframes.map( frame => /^rotate\((-?[\d.]+)deg\)$/.exec( frame.transform ?? '' ) );
|
|
303
|
+
if ( ! angles || angles.length !== 2 || angles.some( angle => ! angle ) || options.easing && options.easing !== 'linear' ) unsupported( 'Element.animate effect' );
|
|
304
|
+
const animation = new EventTarget(), started = wallNow(), duration = Number( options.duration ), iterations = options.iterations ?? 1;
|
|
305
|
+
if ( ! ( duration > 0 ) || ! ( iterations > 0 ) ) throw new TypeError( 'Animation duration and iterations must be positive.' );
|
|
306
|
+
let timer, resolve;
|
|
307
|
+
const previous = this.style.transform;
|
|
308
|
+
animation.playState = 'running'; animation.finished = new Promise( done => { resolve = done; } );
|
|
309
|
+
animation.cancel = () => { clearTimeout( timer ); this.style.transform = previous; animation.playState = 'idle'; resolve( animation ); };
|
|
310
|
+
const tick = () => {
|
|
311
|
+
|
|
312
|
+
if ( ! this.isConnected ) { animation.cancel(); return; }
|
|
313
|
+
const elapsed = wallNow() - started, finished = elapsed >= duration * iterations;
|
|
314
|
+
const progress = finished ? 1 : elapsed % duration / duration;
|
|
315
|
+
this.style.transform = `rotate(${Number( angles[ 0 ][ 1 ] ) + ( Number( angles[ 1 ][ 1 ] ) - Number( angles[ 0 ][ 1 ] ) ) * progress}deg)`;
|
|
316
|
+
if ( finished ) { animation.playState = 'finished'; resolve( animation ); animation.dispatchEvent( new Event( 'finish' ) ); }
|
|
317
|
+
else timer = setTimeout( tick, 16 );
|
|
318
|
+
|
|
319
|
+
};
|
|
320
|
+
timer = setTimeout( tick, 0 ); return animation;
|
|
321
|
+
|
|
322
|
+
};
|
|
323
|
+
// LinkeDOM mutates dispatch bookkeeping, whereas Node's Event fields are read-only.
|
|
324
|
+
const dispatchDOMEvent = dom.EventTarget.prototype.dispatchEvent;
|
|
325
|
+
const addDOMListener = dom.EventTarget.prototype.addEventListener;
|
|
326
|
+
const removeDOMListener = dom.EventTarget.prototype.removeEventListener;
|
|
327
|
+
const captureListeners = new WeakMap();
|
|
328
|
+
dom.EventTarget.prototype.addEventListener = function ( type, listener, options ) {
|
|
329
|
+
|
|
330
|
+
if ( options !== true && ! options?.capture ) return addDOMListener.call( this, type, listener, options );
|
|
331
|
+
if ( ! captureListeners.has( this ) ) captureListeners.set( this, new Map() );
|
|
332
|
+
const types = captureListeners.get( this );
|
|
333
|
+
if ( ! types.has( type ) ) types.set( type, new Map() );
|
|
334
|
+
types.get( type ).set( listener, options );
|
|
335
|
+
|
|
336
|
+
};
|
|
337
|
+
dom.EventTarget.prototype.removeEventListener = function ( type, listener, options ) {
|
|
338
|
+
|
|
339
|
+
if ( options === true || options?.capture ) captureListeners.get( this )?.get( type )?.delete( listener );
|
|
340
|
+
else removeDOMListener.call( this, type, listener );
|
|
341
|
+
|
|
342
|
+
};
|
|
343
|
+
dom.EventTarget.prototype.dispatchEvent = function ( event ) {
|
|
344
|
+
|
|
345
|
+
const copy = event instanceof dom.Event ? event : Object.assign(
|
|
346
|
+
new dom.Event( event.type, { bubbles: event.bubbles, cancelable: event.cancelable, composed: event.composed } ),
|
|
347
|
+
{ detail: event.detail }, Object.fromEntries( Object.keys( event ).map( key => [ key, event[ key ] ] ) ),
|
|
348
|
+
);
|
|
349
|
+
// LinkeDOM only delivers bubbling listeners; details/focus also need capture.
|
|
350
|
+
const path = [];
|
|
351
|
+
for ( let node = this; node; node = node._getParent?.() ) path.unshift( node );
|
|
352
|
+
copy.target = this;
|
|
353
|
+
for ( const node of path ) {
|
|
354
|
+
|
|
355
|
+
copy.currentTarget = node; copy.eventPhase = node === this ? copy.AT_TARGET : copy.CAPTURING_PHASE;
|
|
356
|
+
for ( const [ listener, options ] of captureListeners.get( node )?.get( copy.type ) ?? [] ) {
|
|
357
|
+
|
|
358
|
+
if ( options?.once ) captureListeners.get( node ).get( copy.type ).delete( listener );
|
|
359
|
+
if ( typeof listener === 'function' ) listener.call( node, copy ); else listener?.handleEvent( copy );
|
|
360
|
+
if ( copy._stopImmediatePropagationFlag ) break;
|
|
361
|
+
|
|
362
|
+
}
|
|
363
|
+
if ( copy.cancelBubble ) break;
|
|
364
|
+
|
|
365
|
+
}
|
|
366
|
+
const accepted = copy.cancelBubble ? ! copy.defaultPrevented : dispatchDOMEvent.call( this, copy );
|
|
367
|
+
if ( ! accepted ) event.preventDefault();
|
|
368
|
+
return accepted;
|
|
369
|
+
|
|
370
|
+
};
|
|
371
|
+
// LinkeDOM parses details as generic HTMLElements. Reflect its boolean state.
|
|
372
|
+
Object.defineProperty( dom.HTMLDetailsElement, Symbol.hasInstance, { configurable: true, value: element => element?.localName === 'details' } );
|
|
373
|
+
Object.defineProperty( dom.HTMLElement.prototype, 'open', { configurable: true, get() { return this.localName === 'details' ? this.hasAttribute( 'open' ) : undefined; }, set( value ) {
|
|
374
|
+
|
|
375
|
+
if ( this.localName !== 'details' ) { Object.defineProperty( this, 'open', { configurable: true, writable: true, value } ); return; }
|
|
376
|
+
if ( this.open === Boolean( value ) ) return;
|
|
377
|
+
this.toggleAttribute( 'open', Boolean( value ) );
|
|
378
|
+
queueMicrotask( () => this.dispatchEvent( new dom.Event( 'toggle' ) ) );
|
|
379
|
+
|
|
380
|
+
} } );
|
|
381
|
+
let focused;
|
|
382
|
+
Object.defineProperty( document, 'activeElement', { get: () => focused?.getRootNode()?.host ?? focused ?? document.body } );
|
|
383
|
+
Object.defineProperty( dom.ShadowRoot.prototype, 'activeElement', { configurable: true, get() { return focused?.getRootNode() === this ? focused : null; } } );
|
|
384
|
+
dom.HTMLElement.prototype.focus = function () {
|
|
385
|
+
|
|
386
|
+
if ( focused === this ) return;
|
|
387
|
+
focused?.blur(); focused = this;
|
|
388
|
+
this.dispatchEvent( new dom.Event( 'focus' ) );
|
|
389
|
+
this.dispatchEvent( new dom.Event( 'focusin', { bubbles: true } ) );
|
|
390
|
+
|
|
391
|
+
};
|
|
392
|
+
dom.HTMLElement.prototype.blur = function () {
|
|
393
|
+
|
|
394
|
+
if ( focused !== this ) return;
|
|
395
|
+
focused = undefined;
|
|
396
|
+
this.dispatchEvent( new dom.Event( 'blur' ) );
|
|
397
|
+
this.dispatchEvent( new dom.Event( 'focusout', { bubbles: true } ) );
|
|
398
|
+
|
|
399
|
+
};
|
|
400
|
+
installLayout( dom, viewport, storage, media );
|
|
401
|
+
Object.assign( locationURL, { reload: () => navigateTo( pageURL ), replace: navigateTo, assign: navigateTo } );
|
|
402
|
+
const historyEntries = [ { url, state: null } ];
|
|
403
|
+
let historyIndex = 0;
|
|
404
|
+
const changeHistory = ( state, value, replace ) => {
|
|
405
|
+
|
|
406
|
+
const next = new URL( value ?? location.href, location.href );
|
|
407
|
+
if ( next.origin !== location.origin ) throw new DOMException( 'History URL must share the document origin.', 'SecurityError' );
|
|
408
|
+
const entry = { url: next.href, state: structuredClone( state ) };
|
|
409
|
+
if ( replace ) historyEntries[ historyIndex ] = entry;
|
|
410
|
+
else { historyEntries.splice( historyIndex + 1, Infinity, entry ); historyIndex ++; }
|
|
411
|
+
locationURL.href = pageURL = next.href;
|
|
412
|
+
send( { event: 'url', url: pageURL } );
|
|
413
|
+
|
|
414
|
+
};
|
|
415
|
+
globalThis.history = {
|
|
416
|
+
get length() { return historyEntries.length; }, get state() { return historyEntries[ historyIndex ].state; }, scrollRestoration: 'auto',
|
|
417
|
+
pushState: ( state, _unused, value ) => changeHistory( state, value, false ),
|
|
418
|
+
replaceState: ( state, _unused, value ) => changeHistory( state, value, true ),
|
|
419
|
+
go( delta = 0 ) {
|
|
420
|
+
|
|
421
|
+
if ( delta === 0 ) return location.reload();
|
|
422
|
+
const next = historyIndex + Math.trunc( delta );
|
|
423
|
+
if ( next < 0 || next >= historyEntries.length ) return;
|
|
424
|
+
setTimeout( () => {
|
|
425
|
+
|
|
426
|
+
historyIndex = next; locationURL.href = pageURL = historyEntries[ next ].url;
|
|
427
|
+
send( { event: 'url', url: pageURL } );
|
|
428
|
+
globalThis.dispatchEvent( Object.assign( new Event( 'popstate' ), { state: history.state } ) );
|
|
429
|
+
|
|
430
|
+
}, 0 );
|
|
431
|
+
|
|
432
|
+
},
|
|
433
|
+
back() { this.go( -1 ); }, forward() { this.go( 1 ); },
|
|
434
|
+
};
|
|
435
|
+
globalThis.ProgressEvent = class extends Event {
|
|
436
|
+
|
|
437
|
+
constructor( type, options ) { super( type ); Object.assign( this, options ); }
|
|
438
|
+
|
|
439
|
+
};
|
|
440
|
+
globalThis.CustomEvent = class extends Event {
|
|
441
|
+
|
|
442
|
+
constructor( type, options ) { super( type ); this.detail = options?.detail; }
|
|
443
|
+
|
|
444
|
+
};
|
|
445
|
+
globalThis.UIEvent = class UIEvent extends Event {
|
|
446
|
+
|
|
447
|
+
constructor( type, options = {} ) { super( type, options ); this.view = options.view ?? null; this.detail = options.detail ?? 0; }
|
|
448
|
+
|
|
449
|
+
};
|
|
450
|
+
globalThis.KeyboardEvent = class extends UIEvent {
|
|
451
|
+
|
|
452
|
+
constructor( type, options = {} ) {
|
|
453
|
+
|
|
454
|
+
super( type, options );
|
|
455
|
+
for ( const key of [ 'key', 'code' ] ) this[ key ] = options[ key ] ?? '';
|
|
456
|
+
for ( const key of [ 'altKey', 'ctrlKey', 'metaKey', 'shiftKey', 'repeat', 'isComposing' ] ) this[ key ] = options[ key ] ?? false;
|
|
457
|
+
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
};
|
|
461
|
+
const events = new EventTarget();
|
|
462
|
+
if ( ! parentPort ) {
|
|
463
|
+
|
|
464
|
+
globalThis.parent = globalThis.top = globalThis;
|
|
465
|
+
globalThis.postMessage = ( value, targetOrigin = '/' ) => {
|
|
466
|
+
|
|
467
|
+
if ( targetOrigin !== '*' && targetOrigin !== '/' && targetOrigin !== location.origin ) return;
|
|
468
|
+
const data = structuredClone( value );
|
|
469
|
+
queueMicrotask( () => events.dispatchEvent( new MessageEvent( 'message', { data, origin: location.origin, source: null } ) ) );
|
|
470
|
+
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
for ( const method of [ 'addEventListener', 'removeEventListener', 'dispatchEvent' ] ) globalThis[ method ] = events[ method ].bind( events );
|
|
475
|
+
for ( const name of [ 'PointerEvent', 'MouseEvent', 'WheelEvent' ] ) globalThis[ name ] = class extends dom.Event { constructor( type, options ) { super( type, options ); Object.assign( this, options ); } };
|
|
476
|
+
globalThis.Worker = NativeWorker;
|
|
477
|
+
globalThis.OffscreenCanvas = NativeOffscreenCanvas;
|
|
478
|
+
Object.defineProperty( globalThis, 'crossOriginIsolated', { value: true, configurable: true } );
|
|
479
|
+
Object.defineProperties( document, {
|
|
480
|
+
readyState: { value: 'loading', writable: true }, currentScript: { value: null, writable: true }, hidden: { value: false }, visibilityState: { value: 'visible' },
|
|
481
|
+
} );
|
|
482
|
+
const createElement = document.createElement.bind( document );
|
|
483
|
+
const images = new WeakMap();
|
|
484
|
+
const prepareImage = element => {
|
|
485
|
+
|
|
486
|
+
if ( images.has( element ) ) return element;
|
|
487
|
+
const native = new Image(); images.set( element, native );
|
|
488
|
+
const setAttribute = element.setAttribute.bind( element );
|
|
489
|
+
for ( const name of [ 'src', 'currentSrc', 'complete', 'naturalWidth', 'naturalHeight', 'width', 'height', 'crossOrigin', 'decoding' ] ) Object.defineProperty( element, name, {
|
|
490
|
+
configurable: true, get: () => native[ name ], set: value => {
|
|
491
|
+
if ( name === 'src' ) { setAttribute( name, value ); value = new URL( value, pageURL ).href; }
|
|
492
|
+
native[ name ] = value;
|
|
493
|
+
},
|
|
494
|
+
} );
|
|
495
|
+
element.decode = () => native.decode(); element._toRGBA8 = () => native._toRGBA8();
|
|
496
|
+
element.setAttribute = ( name, value ) => { setAttribute( name, value ); if ( [ 'src', 'width', 'height' ].includes( name ) ) element[ name ] = name === 'src' ? value : Number( value ); };
|
|
497
|
+
for ( const type of [ 'load', 'error' ] ) native.addEventListener( type, event => {
|
|
498
|
+
|
|
499
|
+
globalThis.__THREE_BLOCKS_INVALIDATE_LAYOUT__?.();
|
|
500
|
+
element.dispatchEvent( Object.assign( new dom.Event( type ), { error: event.error, message: event.message } ) );
|
|
501
|
+
|
|
502
|
+
} );
|
|
503
|
+
if ( element.getAttribute( 'src' ) ) element.src = element.getAttribute( 'src' );
|
|
504
|
+
return element;
|
|
505
|
+
|
|
506
|
+
};
|
|
507
|
+
globalThis.HTMLImageElement = dom.HTMLImageElement;
|
|
508
|
+
globalThis.Image = class ImageElement {
|
|
509
|
+
|
|
510
|
+
static [ Symbol.hasInstance ]( value ) { return value instanceof dom.HTMLImageElement; }
|
|
511
|
+
constructor( width, height ) {
|
|
512
|
+
|
|
513
|
+
const element = prepareImage( createElement( 'img' ) );
|
|
514
|
+
if ( width !== undefined ) element.width = width;
|
|
515
|
+
if ( height !== undefined ) element.height = height;
|
|
516
|
+
return element;
|
|
517
|
+
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
};
|
|
521
|
+
document.createElement = ( tag, ...args ) => {
|
|
522
|
+
|
|
523
|
+
if ( tag.toLowerCase() === 'video' ) return new HTMLVideoElement();
|
|
524
|
+
if ( /^(audio|iframe)$/i.test( tag ) ) unsupported( `<${tag}>` );
|
|
525
|
+
if ( tag.toLowerCase() === 'img' ) return new globalThis.Image();
|
|
526
|
+
const element = createElement( tag, ...args );
|
|
527
|
+
if ( tag.toLowerCase() === 'canvas' ) prepareCanvas( element );
|
|
528
|
+
return element;
|
|
529
|
+
|
|
530
|
+
};
|
|
531
|
+
const createElementNS = document.createElementNS.bind( document );
|
|
532
|
+
document.createElementNS = ( ns, tag ) => ns === 'http://www.w3.org/1999/xhtml' ? document.createElement( tag ) : createElementNS( ns, tag );
|
|
533
|
+
// Also prepare canvases created by innerHTML and support capability probes.
|
|
534
|
+
dom.HTMLCanvasElement.prototype.getContext = function ( ...args ) { prepareCanvas( this ); return surfaces.get( this ).canvas.getContext( ...args ); };
|
|
535
|
+
dom.HTMLCanvasElement.prototype.transferControlToOffscreen = function () { prepareCanvas( this ); return this.transferControlToOffscreen(); };
|
|
536
|
+
for ( const canvas of document.querySelectorAll( 'canvas' ) ) prepareCanvas( canvas );
|
|
537
|
+
for ( const element of document.querySelectorAll( 'img' ) ) prepareImage( element );
|
|
538
|
+
registerImageDecoder( { name: 'libjpeg-turbo', test: bytes => bytes[ 0 ] === 0xff && bytes[ 1 ] === 0xd8, decode: jpeg.decode } );
|
|
539
|
+
registerImageDecoder( { name: 'pngjs', test: bytes => bytes[ 0 ] === 0x89 && bytes[ 1 ] === 0x50, decode: bytes => {
|
|
540
|
+
|
|
541
|
+
// GLB buffer views may include up to three zero alignment bytes after IEND.
|
|
542
|
+
// Its fixed CRC ends in 0x82, so trimming only that padding preserves validation.
|
|
543
|
+
let end = bytes.length;
|
|
544
|
+
while ( end > bytes.length - 3 && bytes[ end - 1 ] === 0 ) end --;
|
|
545
|
+
return PNG.sync.read( Buffer.from( bytes.subarray( 0, end ) ) );
|
|
546
|
+
|
|
547
|
+
} } );
|
|
548
|
+
registerImageDecoder( { name: 'skia', test: bytes => bytes[ 0 ] !== 0x89 && bytes[ 0 ] !== 0xff, decode: bytes => {
|
|
549
|
+
const image = new SkiaImage(); image.src = Buffer.from( bytes );
|
|
550
|
+
if ( ! image.complete || image.width < 1 || image.height < 1 ) throw new Error( 'Native image could not be decoded.' );
|
|
551
|
+
const canvas = createCanvas( image.width, image.height ), context = canvas.getContext( '2d' );
|
|
552
|
+
context.drawImage( image, 0, 0 );
|
|
553
|
+
return context.getImageData( 0, 0, image.width, image.height );
|
|
554
|
+
} } );
|
|
555
|
+
const prepareVideo = await installMedia( createElement );
|
|
556
|
+
for ( const element of document.querySelectorAll( 'video' ) ) prepareVideo( element );
|
|
557
|
+
// Keep PNG and libjpeg-turbo uploads unchanged; Skia handles other formats.
|
|
558
|
+
const bitmap = globalThis.createImageBitmap;
|
|
559
|
+
globalThis.createImageBitmap = async ( source, ...options ) => {
|
|
560
|
+
|
|
561
|
+
if ( source instanceof Blob ) {
|
|
562
|
+
|
|
563
|
+
const bytes = Buffer.from( await source.arrayBuffer() );
|
|
564
|
+
if ( bytes[ 0 ] !== 0x89 && bytes[ 0 ] !== 0xff ) {
|
|
565
|
+
const image = await loadImage( bytes );
|
|
566
|
+
const canvas = createCanvas( image.width, image.height );
|
|
567
|
+
canvas.getContext( '2d' ).drawImage( image, 0, 0 );
|
|
568
|
+
source = new Blob( [ await canvas.encode( 'png' ) ], { type: 'image/png' } );
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
}
|
|
572
|
+
if ( surfaces.get( source )?.context ) source = await readPixels( source );
|
|
573
|
+
if ( source instanceof VideoFrame ) await source.preparePixels();
|
|
574
|
+
return bitmap( source, ...options );
|
|
575
|
+
|
|
576
|
+
};
|
|
577
|
+
if ( gpu ) {
|
|
578
|
+
const { create, globals } = await import( 'webgpu' );
|
|
579
|
+
Object.assign( globalThis, globals, { GPUCanvasContext: NativeGPUCanvasContext } );
|
|
580
|
+
const backend = process.env.THREE_BLOCKS_NODE_WEBGPU_BACKEND
|
|
581
|
+
?? ( process.platform === 'darwin' ? 'metal' : process.platform === 'win32' ? 'd3d12' : 'vulkan' );
|
|
582
|
+
navigator.gpu = create( [ `backend=${backend}` ] );
|
|
583
|
+
const requestAdapter = navigator.gpu.requestAdapter.bind( navigator.gpu );
|
|
584
|
+
navigator.gpu.requestAdapter = async options => {
|
|
585
|
+
|
|
586
|
+
const adapter = await requestAdapter( options );
|
|
587
|
+
if ( ! adapter ) throw new Error( `No native WebGPU adapter for ${backend}.` );
|
|
588
|
+
const requestDevice = adapter.requestDevice.bind( adapter );
|
|
589
|
+
adapter.requestDevice = async descriptor => {
|
|
590
|
+
|
|
591
|
+
const device = await requestDevice( descriptor );
|
|
592
|
+
const copyYUV = installExternalTextures( device );
|
|
593
|
+
device.addEventListener( 'uncapturederror', event => failure( event.error ) );
|
|
594
|
+
device.lost.then( info => { if ( info.reason !== 'destroyed' ) failure( new Error( `WebGPU device lost: ${info.message}` ) ); } );
|
|
595
|
+
device.queue.copyExternalImageToTexture = ( source, destination, size ) => {
|
|
596
|
+
|
|
597
|
+
if ( ! /^rgba8unorm(-srgb)?$/.test( destination.texture.format ) ) throw new Error( `Unsupported image upload format: ${destination.texture.format}` );
|
|
598
|
+
const image = source.source._toRGBA8?.();
|
|
599
|
+
if ( ! image ) unsupported( 'external image source' );
|
|
600
|
+
if ( copyYUV( image, source, destination, size ) ) return;
|
|
601
|
+
const width = size.width ?? size[ 0 ];
|
|
602
|
+
const height = size.height ?? size[ 1 ] ?? 1;
|
|
603
|
+
const x = source.origin?.x ?? source.origin?.[ 0 ] ?? 0;
|
|
604
|
+
const y = source.origin?.y ?? source.origin?.[ 1 ] ?? 0;
|
|
605
|
+
if ( x + width > image.width || y + height > image.height ) throw new Error( 'External image copy exceeds source bounds.' );
|
|
606
|
+
const pixels = new Uint8Array( width * height * 4 );
|
|
607
|
+
for ( let row = 0; row < height; row ++ ) {
|
|
608
|
+
|
|
609
|
+
const offset = ( ( source.flipY ? image.height - 1 - y - row : y + row ) * image.width + x ) * 4;
|
|
610
|
+
pixels.set( image.data.subarray( offset, offset + width * 4 ), row * width * 4 );
|
|
611
|
+
|
|
612
|
+
}
|
|
613
|
+
if ( destination.premultipliedAlpha ) for ( let i = 0; i < pixels.length; i += 4 ) for ( let c = 0; c < 3; c ++ ) pixels[ i + c ] = Math.round( pixels[ i + c ] * pixels[ i + 3 ] / 255 );
|
|
614
|
+
device.queue.writeTexture( destination, pixels, { bytesPerRow: width * 4 }, size );
|
|
615
|
+
|
|
616
|
+
};
|
|
617
|
+
return device;
|
|
618
|
+
|
|
619
|
+
};
|
|
620
|
+
return adapter;
|
|
621
|
+
|
|
622
|
+
};
|
|
623
|
+
} else { delete navigator.gpu; }
|
|
624
|
+
const scripts = [ ...document.querySelectorAll( 'script' ) ];
|
|
625
|
+
Object.assign( importMap, ...scripts.filter( script => script.type === 'importmap' ).map( script => {
|
|
626
|
+
|
|
627
|
+
const map = JSON.parse( script.textContent );
|
|
628
|
+
Object.assign( scopes, map.scopes );
|
|
629
|
+
return map.imports;
|
|
630
|
+
|
|
631
|
+
} ) );
|
|
632
|
+
importScopes = scopes;
|
|
633
|
+
const modules = {};
|
|
634
|
+
if ( blob ) modules[ url ] = await blob.text();
|
|
635
|
+
for ( const [ index, script ] of scripts.entries() ) if ( script.type === 'module' && ! script.src ) modules[ `${url.split( '#' )[ 0 ]}#inline-${index}` ] = script.textContent;
|
|
636
|
+
const { port1, port2 } = new MessageChannel();
|
|
637
|
+
port1.on( 'message', send ); port1.unref();
|
|
638
|
+
register( './loader.mjs', import.meta.url, { data: { pageURL, imports: importMap, modules, scopes, network: port2 }, transferList: [ port2 ] } );
|
|
639
|
+
if ( thread ) {
|
|
640
|
+
|
|
641
|
+
delete globalThis.document; delete globalThis.window;
|
|
642
|
+
globalThis.onmessage = null; globalThis.onerror = null;
|
|
643
|
+
globalThis.postMessage = ( value, transfer = [] ) => {
|
|
644
|
+
|
|
645
|
+
if ( ! Array.isArray( transfer ) ) transfer = transfer.transfer ?? [];
|
|
646
|
+
parentPort.postMessage( { data: pack( value, transfer ) }, nativeTransfers( transfer ) );
|
|
647
|
+
|
|
648
|
+
};
|
|
649
|
+
globalThis.close = () => process.exit( 0 );
|
|
650
|
+
if ( thread === 'classic' ) globalThis.importScripts = ( ...urls ) => {
|
|
651
|
+
|
|
652
|
+
for ( const specifier of urls ) {
|
|
653
|
+
|
|
654
|
+
const url = new URL( specifier, pageURL ).href;
|
|
655
|
+
const signal = new Int32Array( new SharedArrayBuffer( 4 ) );
|
|
656
|
+
const { port1, port2 } = new MessageChannel();
|
|
657
|
+
try {
|
|
658
|
+
|
|
659
|
+
// The parent owns async fetch/routing while the worker must block.
|
|
660
|
+
parentPort.postMessage( { importScript: url, signal, port: port2 }, [ port2 ] );
|
|
661
|
+
if ( Atomics.wait( signal, 0, 0, 45_000 ) === 'timed-out' ) throw new Error( `importScripts timed out: ${url}` );
|
|
662
|
+
const result = receiveMessageOnPort( port1 ).message;
|
|
663
|
+
if ( result.error ) throw new DOMException( result.error, 'NetworkError' );
|
|
664
|
+
run( result.source, url );
|
|
665
|
+
|
|
666
|
+
} finally { port1.close(); }
|
|
667
|
+
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
};
|
|
671
|
+
if ( thread === 'module' ) await import( url );
|
|
672
|
+
else run( `(() => { const process = undefined;\n${source ?? modules[ url ] ?? await ( await fetchPage( url ) ).text()}\n})();` );
|
|
673
|
+
return;
|
|
674
|
+
|
|
675
|
+
}
|
|
676
|
+
const { installNativeLayout } = await import( './layout.mjs' );
|
|
677
|
+
paintPage = await installNativeLayout( dom );
|
|
678
|
+
// Match addInitScript's function scope: capture internals must not become
|
|
679
|
+
// enumerable window properties and accidental application shader owners.
|
|
680
|
+
for ( const source of initialScripts ) run( `(() => {\n${source}\n})();` );
|
|
681
|
+
let classicExecution = Promise.resolve();
|
|
682
|
+
const executeScript = async ( script, inlineURL ) => {
|
|
683
|
+
|
|
684
|
+
if ( script.noModule || script.hasAttribute( 'nomodule' ) || script.hasAttribute( 'noModule' ) || script.type === 'importmap' || script.type && ! [ 'module', 'text/javascript', 'application/javascript' ].includes( script.type ) ) return;
|
|
685
|
+
if ( script.type === 'module' ) {
|
|
686
|
+
|
|
687
|
+
if ( ! script.src && ! inlineURL ) unsupported( 'dynamic inline module script' );
|
|
688
|
+
await import( script.src ? new URL( script.src, pageURL ).href : inlineURL );
|
|
689
|
+
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
|
|
693
|
+
const source = script.src ? await ( await fetchPage( script.src, undefined, 'script' ) ).text() : script.textContent;
|
|
694
|
+
const execute = async () => {
|
|
695
|
+
|
|
696
|
+
document.currentScript = script;
|
|
697
|
+
try {
|
|
698
|
+
|
|
699
|
+
// Discard completion values, then drain microtasks before clearing currentScript.
|
|
700
|
+
// A nextTick queued from this Promise job runs after the microtask queue empties.
|
|
701
|
+
run( source, script.src || url );
|
|
702
|
+
await new Promise( resolve => process.nextTick( resolve ) );
|
|
703
|
+
|
|
704
|
+
} finally { document.currentScript = null; }
|
|
705
|
+
|
|
706
|
+
};
|
|
707
|
+
await ( classicExecution = classicExecution.then( execute, execute ) );
|
|
708
|
+
|
|
709
|
+
}
|
|
710
|
+
script.dispatchEvent( new dom.Event( 'load' ) );
|
|
711
|
+
|
|
712
|
+
};
|
|
713
|
+
const executedScripts = new WeakSet( scripts );
|
|
714
|
+
const insertedScripts = new dom.MutationObserver( records => {
|
|
715
|
+
|
|
716
|
+
for ( const record of records ) for ( const node of record.addedNodes ) {
|
|
717
|
+
|
|
718
|
+
if ( node.nodeType !== 1 ) continue;
|
|
719
|
+
for ( const script of [ ...( node.localName === 'script' ? [ node ] : [] ), ...node.querySelectorAll( 'script' ) ] ) {
|
|
720
|
+
|
|
721
|
+
if ( executedScripts.has( script ) || ! script.isConnected ) continue;
|
|
722
|
+
executedScripts.add( script );
|
|
723
|
+
void executeScript( script ).catch( error => { script.dispatchEvent( new dom.Event( 'error' ) ); failure( error ); } );
|
|
724
|
+
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
} );
|
|
730
|
+
insertedScripts.observe( document, { childList: true, subtree: true } );
|
|
731
|
+
for ( const [ index, script ] of scripts.entries() ) {
|
|
732
|
+
|
|
733
|
+
await executeScript( script, `${url.split( '#' )[ 0 ]}#inline-${index}` );
|
|
734
|
+
|
|
735
|
+
}
|
|
736
|
+
document.readyState = 'interactive';
|
|
737
|
+
document.dispatchEvent( new dom.Event( 'DOMContentLoaded' ) );
|
|
738
|
+
document.readyState = 'complete';
|
|
739
|
+
globalThis.onload?.( new Event( 'load' ) );
|
|
740
|
+
globalThis.dispatchEvent( new Event( 'load' ) );
|
|
741
|
+
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function installAnimationFrames( interval ) {
|
|
745
|
+
|
|
746
|
+
const callbacks = new Map();
|
|
747
|
+
let id = 0, timer, deadline = wallNow() + interval;
|
|
748
|
+
globalThis.requestAnimationFrame = callback => {
|
|
749
|
+
|
|
750
|
+
if ( typeof callback !== 'function' ) throw new TypeError( 'Animation callback must be a function.' );
|
|
751
|
+
const token = ++ id; callbacks.set( token, callback );
|
|
752
|
+
if ( timer === undefined ) {
|
|
753
|
+
|
|
754
|
+
const now = wallNow();
|
|
755
|
+
if ( interval > 0 && deadline < now ) deadline += Math.ceil( ( now - deadline ) / interval ) * interval;
|
|
756
|
+
timer = setTimeout( () => {
|
|
757
|
+
|
|
758
|
+
timer = undefined; deadline += interval;
|
|
759
|
+
const timestamp = wallNow();
|
|
760
|
+
for ( const token of [ ...callbacks.keys() ] ) {
|
|
761
|
+
|
|
762
|
+
const callback = callbacks.get( token ); callbacks.delete( token );
|
|
763
|
+
try { callback?.( timestamp ); } catch ( error ) { failure( error ); }
|
|
764
|
+
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
}, Math.max( 0, deadline - now ) );
|
|
768
|
+
|
|
769
|
+
}
|
|
770
|
+
return token;
|
|
771
|
+
|
|
772
|
+
};
|
|
773
|
+
globalThis.cancelAnimationFrame = token => {
|
|
774
|
+
|
|
775
|
+
callbacks.delete( token );
|
|
776
|
+
if ( callbacks.size === 0 && timer !== undefined ) { clearTimeout( timer ); timer = undefined; }
|
|
777
|
+
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function installCanvasTargets( backend ) {
|
|
783
|
+
|
|
784
|
+
if ( backend.isWebGLBackend !== true || ! backend.gl || canvasTargetBackends.has( backend ) ) return;
|
|
785
|
+
canvasTargetBackends.add( backend );
|
|
786
|
+
const source = backend.gl.canvas;
|
|
787
|
+
const begin = backend.beginRender, finish = backend.finishRender;
|
|
788
|
+
backend.beginRender = function ( context ) {
|
|
789
|
+
|
|
790
|
+
if ( context.textures === null ) {
|
|
791
|
+
|
|
792
|
+
const { width, height } = this.getDrawingBufferSize();
|
|
793
|
+
if ( source.width !== width ) source.width = width;
|
|
794
|
+
if ( source.height !== height ) source.height = height;
|
|
795
|
+
|
|
796
|
+
}
|
|
797
|
+
return begin.call( this, context );
|
|
798
|
+
|
|
799
|
+
};
|
|
800
|
+
backend.finishRender = function ( context ) {
|
|
801
|
+
|
|
802
|
+
const result = finish.call( this, context );
|
|
803
|
+
const target = this.renderer.getCanvasTarget().domElement;
|
|
804
|
+
if ( context.textures === null && surfaces.get( target ) !== surfaces.get( source ) ) {
|
|
805
|
+
|
|
806
|
+
// ponytail: one readback per alternate canvas; share native framebuffers if this path becomes a bottleneck.
|
|
807
|
+
const output = target.getContext( '2d' );
|
|
808
|
+
if ( ! output ) throw new Error( 'Native WebGL CanvasTarget has an incompatible context.' );
|
|
809
|
+
output.clearRect( 0, 0, target.width, target.height );
|
|
810
|
+
output.drawImage( source, 0, 0 );
|
|
811
|
+
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function prepareCanvas( element ) {
|
|
820
|
+
|
|
821
|
+
if ( surfaces.has( element ) ) return;
|
|
822
|
+
const canvas = new NativeOffscreenCanvas( Number( element.getAttribute( 'width' ) ?? 300 ), Number( element.getAttribute( 'height' ) ?? 150 ) );
|
|
823
|
+
surfaces.set( element, surfaces.get( canvas ) );
|
|
824
|
+
for ( const key of [ 'width', 'height' ] ) Object.defineProperty( element, key, {
|
|
825
|
+
get: () => canvas[ key ], set: value => { canvas[ key ] = value; globalThis.__THREE_BLOCKS_INVALIDATE_LAYOUT__?.(); }, configurable: true,
|
|
826
|
+
} );
|
|
827
|
+
element._toRGBA8 = () => canvas._toRGBA8();
|
|
828
|
+
element.toDataURL = ( ...args ) => canvas.toDataURL( ...args );
|
|
829
|
+
element.toBlob = ( ...args ) => canvas.toBlob( ...args );
|
|
830
|
+
element.transferControlToOffscreen = () => {
|
|
831
|
+
|
|
832
|
+
if ( canvas._ctx || surfaces.get( canvas ).context || surfaces.get( canvas ).canvas2d || canvas.transferred ) throw new DOMException( 'Canvas already has a context or was transferred.', 'InvalidStateError' );
|
|
833
|
+
canvas.transferred = true;
|
|
834
|
+
return canvas;
|
|
835
|
+
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
class NativeOffscreenCanvas extends Canvas {
|
|
841
|
+
|
|
842
|
+
constructor( width, height ) {
|
|
843
|
+
|
|
844
|
+
super( width, height );
|
|
845
|
+
this.surfaceId = `${parentPort ? 'thread' : 'page'}-${++ surfaceId}`;
|
|
846
|
+
surfaces.set( this, { canvas: this } );
|
|
847
|
+
transferredSurfaces.set( this.surfaceId, this );
|
|
848
|
+
|
|
849
|
+
}
|
|
850
|
+
getContext( type, options ) {
|
|
851
|
+
|
|
852
|
+
if ( this.detached ) throw new DOMException( 'Canvas was transferred.', 'InvalidStateError' );
|
|
853
|
+
if ( ! gpuEnabled && [ 'webgpu', 'webgl', 'webgl2', 'experimental-webgl' ].includes( type ) ) return null;
|
|
854
|
+
const surface = surfaces.get( this );
|
|
855
|
+
if ( surface.kind && surface.kind !== type ) return null;
|
|
856
|
+
surface.kind = type;
|
|
857
|
+
if ( type === 'webgpu' ) return gpuContext( this );
|
|
858
|
+
if ( type === '2d' ) {
|
|
859
|
+
|
|
860
|
+
if ( ! surface.canvas2d ) {
|
|
861
|
+
|
|
862
|
+
surface.canvas2d = createCanvas( this.width, this.height );
|
|
863
|
+
const context = surface.canvas2d.getContext( '2d', options );
|
|
864
|
+
const getImageData = context.getImageData.bind( context );
|
|
865
|
+
context.getImageData = ( x, y, width, height, settings ) => {
|
|
866
|
+
|
|
867
|
+
if ( settings?.pixelFormat && settings.pixelFormat !== 'rgba-unorm8' ) unsupported( `Canvas2D pixel format ${settings.pixelFormat}` );
|
|
868
|
+
return getImageData( x, y, width, height, typeof settings === 'object' ? settings?.colorSpace : settings );
|
|
869
|
+
|
|
870
|
+
};
|
|
871
|
+
const drawImage = context.drawImage.bind( context );
|
|
872
|
+
context.drawImage = ( source, ...args ) => {
|
|
873
|
+
|
|
874
|
+
const pixels = source._toRGBA8?.();
|
|
875
|
+
if ( pixels ) {
|
|
876
|
+
|
|
877
|
+
const copy = createCanvas( pixels.width, pixels.height );
|
|
878
|
+
copy.getContext( '2d' ).putImageData( new SkiaImageData( new Uint8ClampedArray( pixels.data ), pixels.width, pixels.height ), 0, 0 );
|
|
879
|
+
source = copy;
|
|
880
|
+
|
|
881
|
+
}
|
|
882
|
+
return drawImage( source, ...args );
|
|
883
|
+
|
|
884
|
+
};
|
|
885
|
+
const putImageData = context.putImageData.bind( context );
|
|
886
|
+
context.putImageData = ( source, ...args ) => putImageData( source instanceof SkiaImageData ? source : new SkiaImageData( source.data, source.width, source.height ), ...args );
|
|
887
|
+
}
|
|
888
|
+
return surface.canvas2d.getContext( '2d', options );
|
|
889
|
+
|
|
890
|
+
}
|
|
891
|
+
const context = super.getContext( type, options );
|
|
892
|
+
surface.premultiplied = context?.getContextAttributes?.().premultipliedAlpha === true;
|
|
893
|
+
return context;
|
|
894
|
+
|
|
895
|
+
}
|
|
896
|
+
get width() { return super.width; }
|
|
897
|
+
set width( value ) { super.width = value; const raster = surfaces.get( this )?.canvas2d; if ( raster ) raster.width = this.width; }
|
|
898
|
+
get height() { return super.height; }
|
|
899
|
+
set height( value ) { super.height = value; const raster = surfaces.get( this )?.canvas2d; if ( raster ) raster.height = this.height; }
|
|
900
|
+
getImageData() {
|
|
901
|
+
|
|
902
|
+
const surface = surfaces.get( this );
|
|
903
|
+
if ( surface.context ) throw new Error( 'Use await createImageBitmap(canvas) to read a native WebGPU canvas.' );
|
|
904
|
+
return surface.canvas2d?.getContext( '2d' ).getImageData( 0, 0, this.width, this.height ) ?? super.getImageData();
|
|
905
|
+
|
|
906
|
+
}
|
|
907
|
+
_toRGBA8() {
|
|
908
|
+
|
|
909
|
+
return straightRGBA( { ...super._toRGBA8(), premultiplied: surfaces.get( this ).premultiplied } );
|
|
910
|
+
|
|
911
|
+
}
|
|
912
|
+
async convertToBlob( options = {} ) { return new Blob( [ PNG.sync.write( await readPixels( this ) ) ], { type: 'image/png' } ); }
|
|
913
|
+
transferToImageBitmap() {
|
|
914
|
+
|
|
915
|
+
const { width, height, data } = this._toRGBA8();
|
|
916
|
+
const result = new ImageBitmap( width, height, data.slice() );
|
|
917
|
+
this.width = width;
|
|
918
|
+
return result;
|
|
919
|
+
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
class NativeGPUCanvasContext {
|
|
925
|
+
|
|
926
|
+
#configuration;
|
|
927
|
+
#texture;
|
|
928
|
+
constructor( canvas ) { this.canvas = canvas; }
|
|
929
|
+
configure( value ) { this.#texture?.destroy(); this.#texture = undefined; this.#configuration = value; }
|
|
930
|
+
unconfigure() { this.#texture?.destroy(); this.#texture = undefined; this.#configuration = undefined; }
|
|
931
|
+
get device() { return this.#configuration.device; }
|
|
932
|
+
get alphaMode() { return this.#configuration.alphaMode ?? 'opaque'; }
|
|
933
|
+
getCurrentTexture() {
|
|
934
|
+
|
|
935
|
+
if ( ! this.#configuration ) throw new Error( 'WebGPU canvas is not configured.' );
|
|
936
|
+
const { canvas } = this;
|
|
937
|
+
if ( ! this.#texture || this.#texture.width !== canvas.width || this.#texture.height !== canvas.height ) {
|
|
938
|
+
|
|
939
|
+
this.#texture?.destroy();
|
|
940
|
+
this.#texture = this.device.createTexture( {
|
|
941
|
+
size: [ canvas.width, canvas.height ], format: this.#configuration.format,
|
|
942
|
+
usage: ( this.#configuration.usage ?? GPUTextureUsage.RENDER_ATTACHMENT ) | GPUTextureUsage.COPY_SRC,
|
|
943
|
+
} );
|
|
944
|
+
|
|
945
|
+
}
|
|
946
|
+
return this.#texture;
|
|
947
|
+
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function gpuContext( canvas ) {
|
|
953
|
+
|
|
954
|
+
const surface = surfaces.get( canvas );
|
|
955
|
+
return surface.context ??= new NativeGPUCanvasContext( canvas );
|
|
956
|
+
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function screenshot( { selector, index = 0, handle, clip, omitBackground } ) {
|
|
960
|
+
|
|
961
|
+
const element = handle ? handles.get( handle ) : selector ? document.querySelectorAll( selector )[ index ] : null;
|
|
962
|
+
if ( ( handle || selector ) && ! element ) throw new Error( 'Native capture element was not found.' );
|
|
963
|
+
const bounds = clip ?? element?.getBoundingClientRect() ?? { x: 0, y: 0, width: innerWidth, height: innerHeight };
|
|
964
|
+
return PNG.sync.write( await paintPage( bounds, readPixels, omitBackground ) );
|
|
965
|
+
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function readPixels( canvas ) {
|
|
969
|
+
|
|
970
|
+
const surface = surfaces.get( canvas );
|
|
971
|
+
if ( ! surface ) throw new Error( 'Native capture has no renderer surface.' );
|
|
972
|
+
if ( surface.worker ) return surface.worker.readPixels( surface.canvas.surfaceId );
|
|
973
|
+
const { width, height } = canvas;
|
|
974
|
+
let data;
|
|
975
|
+
if ( surface.context ) {
|
|
976
|
+
|
|
977
|
+
const { device } = surface.context;
|
|
978
|
+
const texture = surface.context.getCurrentTexture();
|
|
979
|
+
if ( ! [ 'rgba8unorm', 'bgra8unorm' ].includes( texture.format ) ) unsupported( `HDR canvas (${texture.format})` );
|
|
980
|
+
const bytesPerRow = Math.ceil( width * 4 / 256 ) * 256;
|
|
981
|
+
const buffer = device.createBuffer( { size: bytesPerRow * height, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ } );
|
|
982
|
+
try {
|
|
983
|
+
|
|
984
|
+
const encoder = device.createCommandEncoder();
|
|
985
|
+
encoder.copyTextureToBuffer( { texture }, { buffer, bytesPerRow }, [ width, height ] );
|
|
986
|
+
device.queue.submit( [ encoder.finish() ] );
|
|
987
|
+
await buffer.mapAsync( GPUMapMode.READ );
|
|
988
|
+
const mapped = new Uint8Array( buffer.getMappedRange() );
|
|
989
|
+
data = Buffer.alloc( width * height * 4 );
|
|
990
|
+
for ( let y = 0; y < height; y ++ ) data.set( mapped.subarray( y * bytesPerRow, y * bytesPerRow + width * 4 ), y * width * 4 );
|
|
991
|
+
if ( texture.format === 'bgra8unorm' ) for ( let i = 0; i < data.length; i += 4 ) [ data[ i ], data[ i + 2 ] ] = [ data[ i + 2 ], data[ i ] ];
|
|
992
|
+
buffer.unmap();
|
|
993
|
+
|
|
994
|
+
} finally { buffer.destroy(); }
|
|
995
|
+
|
|
996
|
+
} else data = Buffer.from( surface.canvas.getImageData().data );
|
|
997
|
+
if ( surface.context?.alphaMode === 'opaque' ) for ( let i = 3; i < data.length; i += 4 ) data[ i ] = 255;
|
|
998
|
+
return straightRGBA( { width, height, data, premultiplied: surface.context ? surface.context.alphaMode === 'premultiplied' : surface.premultiplied === true } );
|
|
999
|
+
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function straightRGBA( image ) {
|
|
1003
|
+
|
|
1004
|
+
if ( image.premultiplied ) for ( let i = 0; i < image.data.length; i += 4 ) {
|
|
1005
|
+
|
|
1006
|
+
const alpha = image.data[ i + 3 ];
|
|
1007
|
+
if ( alpha > 0 && alpha < 255 ) for ( let c = 0; c < 3; c ++ ) image.data[ i + c ] = Math.min( 255, Math.round( image.data[ i + c ] * 255 / alpha ) );
|
|
1008
|
+
|
|
1009
|
+
}
|
|
1010
|
+
return { ...image, premultiplied: false };
|
|
1011
|
+
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const evaluate = ( { source, arg, args, callable } ) => {
|
|
1015
|
+
|
|
1016
|
+
globalThis.__nativeArgument = args ?? [ arg ];
|
|
1017
|
+
try { return run( callable ? `(${source})(...globalThis.__nativeArgument)` : source, 'three-blocks-native-evaluate.mjs' ); }
|
|
1018
|
+
finally { delete globalThis.__nativeArgument; }
|
|
1019
|
+
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// Page evaluation returns data, not executable properties such as bridge.settle.
|
|
1023
|
+
// Preserve typed arrays, special numbers and cycles used by GPU harness reports.
|
|
1024
|
+
function evaluationData( value, seen = new Map() ) {
|
|
1025
|
+
|
|
1026
|
+
if ( typeof value === 'function' || typeof value === 'symbol' ) return undefined;
|
|
1027
|
+
if ( value === null || typeof value !== 'object' ) return value;
|
|
1028
|
+
if ( seen.has( value ) ) return seen.get( value );
|
|
1029
|
+
if ( ! Array.isArray( value ) && Object.getPrototypeOf( value ) !== Object.prototype ) return value;
|
|
1030
|
+
const result = Array.isArray( value ) ? [] : {};
|
|
1031
|
+
seen.set( value, result );
|
|
1032
|
+
for ( const [ key, child ] of Object.entries( value ) ) result[ key ] = evaluationData( child, seen );
|
|
1033
|
+
return result;
|
|
1034
|
+
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function command( message ) {
|
|
1038
|
+
|
|
1039
|
+
if ( message.route ) {
|
|
1040
|
+
|
|
1041
|
+
routeRequests.get( message.route )?.( message.response ); routeRequests.delete( message.route ); return;
|
|
1042
|
+
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
|
|
1046
|
+
let value;
|
|
1047
|
+
if ( message.method === 'goto' ) value = await navigate( message.args );
|
|
1048
|
+
else if ( message.method === 'evaluate' ) value = evaluationData( await evaluate( message.args ) );
|
|
1049
|
+
else if ( message.method === 'handle' ) {
|
|
1050
|
+
|
|
1051
|
+
const object = await evaluate( message.args );
|
|
1052
|
+
const id = ++ handleId; handles.set( id, object ); value = { id, element: object instanceof Element };
|
|
1053
|
+
|
|
1054
|
+
} else if ( message.method === 'releaseHandle' ) handles.delete( message.args.id );
|
|
1055
|
+
else if ( message.method === 'handleEvaluate' ) value = evaluationData( await evaluate( { ...message.args, args: [ handles.get( message.args.id ), ...message.args.args ], callable: true } ) );
|
|
1056
|
+
else if ( message.method === 'pixels' ) value = await readPixels( document.querySelector( 'canvas' ) );
|
|
1057
|
+
else if ( message.method === 'screenshot' ) value = await screenshot( message.args );
|
|
1058
|
+
else if ( message.method === 'wait' ) {
|
|
1059
|
+
|
|
1060
|
+
const deadline = wallNow() + message.args.timeout;
|
|
1061
|
+
while ( ! await evaluate( message.args ) ) {
|
|
1062
|
+
|
|
1063
|
+
if ( wallNow() >= deadline ) throw new Error( `Native waitForFunction timed out after ${message.args.timeout}ms: ${message.args.source}` );
|
|
1064
|
+
await delay( 5 );
|
|
1065
|
+
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
} else throw new Error( `Unknown native command: ${message.method}` );
|
|
1069
|
+
send( { id: message.id, value } );
|
|
1070
|
+
|
|
1071
|
+
} catch ( error ) { send( { id: message.id, error: error.stack ?? String( error ) } ); }
|
|
1072
|
+
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function installLayout( dom, viewport, storage, media ) {
|
|
1076
|
+
|
|
1077
|
+
globalThis.__THREE_BLOCKS_LOCATE__ = steps => {
|
|
1078
|
+
|
|
1079
|
+
const query = ( root, selector ) => {
|
|
1080
|
+
|
|
1081
|
+
const found = [ ...root.querySelectorAll( selector ) ];
|
|
1082
|
+
for ( const element of root.querySelectorAll( '*' ) ) if ( element.shadowRoot ) found.push( ...query( element.shadowRoot, selector ) );
|
|
1083
|
+
if ( root.shadowRoot ) found.push( ...query( root.shadowRoot, selector ) );
|
|
1084
|
+
return found;
|
|
1085
|
+
|
|
1086
|
+
};
|
|
1087
|
+
let elements = [ document ];
|
|
1088
|
+
for ( const { selector, text, index } of steps ) {
|
|
1089
|
+
|
|
1090
|
+
elements = [ ...new Set( elements.flatMap( element => query( element, selector ) ) ) ];
|
|
1091
|
+
if ( text !== undefined ) elements = elements.filter( element => text instanceof RegExp ? text.test( element.textContent ) : element.textContent.includes( text ) );
|
|
1092
|
+
if ( index !== undefined ) elements = elements[ index ] ? [ elements[ index ] ] : [];
|
|
1093
|
+
|
|
1094
|
+
}
|
|
1095
|
+
return elements;
|
|
1096
|
+
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
globalThis.ResizeObserver = class {
|
|
1100
|
+
|
|
1101
|
+
#callback; #elements = new Set();
|
|
1102
|
+
constructor( callback ) { this.#callback = callback; }
|
|
1103
|
+
observe( target ) {
|
|
1104
|
+
|
|
1105
|
+
this.#elements.add( target );
|
|
1106
|
+
queueMicrotask( () => {
|
|
1107
|
+
|
|
1108
|
+
if ( ! this.#elements.has( target ) ) return;
|
|
1109
|
+
const contentRect = target.getBoundingClientRect();
|
|
1110
|
+
const size = [ { inlineSize: contentRect.width, blockSize: contentRect.height } ];
|
|
1111
|
+
this.#callback( [ { target, contentRect, contentBoxSize: size, borderBoxSize: size, devicePixelContentBoxSize: size } ], this );
|
|
1112
|
+
|
|
1113
|
+
} );
|
|
1114
|
+
|
|
1115
|
+
}
|
|
1116
|
+
unobserve( target ) { this.#elements.delete( target ); }
|
|
1117
|
+
disconnect() { this.#elements.clear(); }
|
|
1118
|
+
|
|
1119
|
+
};
|
|
1120
|
+
globalThis.IntersectionObserver = class {
|
|
1121
|
+
|
|
1122
|
+
#callback; #elements = new Set();
|
|
1123
|
+
constructor( callback ) { this.#callback = callback; }
|
|
1124
|
+
observe( target ) {
|
|
1125
|
+
|
|
1126
|
+
this.#elements.add( target );
|
|
1127
|
+
queueMicrotask( () => {
|
|
1128
|
+
|
|
1129
|
+
if ( ! this.#elements.has( target ) ) return;
|
|
1130
|
+
const rect = target.getBoundingClientRect();
|
|
1131
|
+
const visible = getComputedStyle( target ).display !== 'none' && rect.width > 0 && rect.height > 0;
|
|
1132
|
+
this.#callback( [ { target, isIntersecting: visible, intersectionRatio: visible ? 1 : 0, boundingClientRect: rect, intersectionRect: rect, time: performance.now() } ], this );
|
|
1133
|
+
|
|
1134
|
+
} );
|
|
1135
|
+
|
|
1136
|
+
}
|
|
1137
|
+
unobserve( target ) { this.#elements.delete( target ); }
|
|
1138
|
+
disconnect() { this.#elements.clear(); }
|
|
1139
|
+
takeRecords() { return []; }
|
|
1140
|
+
|
|
1141
|
+
};
|
|
1142
|
+
globalThis.matchMedia = query => Object.assign( new EventTarget(), { media: query, matches: media.some( feature => query.includes( `${feature.name}: ${feature.value}` ) ), onchange: null, addListener() {}, removeListener() {} } );
|
|
1143
|
+
globalThis.screen = { width: viewport.width, height: viewport.height };
|
|
1144
|
+
globalThis.devicePixelRatio = viewport.deviceScaleFactor ?? 1;
|
|
1145
|
+
globalThis.MutationObserver = dom.MutationObserver;
|
|
1146
|
+
for ( const name of [ 'localStorage', 'sessionStorage' ] ) {
|
|
1147
|
+
|
|
1148
|
+
const values = storage[ name ] ??= {};
|
|
1149
|
+
const save = () => send( { event: 'storage', storage } );
|
|
1150
|
+
globalThis[ name ] = { get length() { return Object.keys( values ).length; }, key: index => Object.keys( values )[ index ] ?? null, getItem: key => values[ key ] ?? null, setItem: ( key, value ) => { values[ key ] = String( value ); save(); }, removeItem: key => { delete values[ key ]; save(); }, clear: () => { for ( const key of Object.keys( values ) ) delete values[ key ]; save(); } };
|
|
1151
|
+
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Native objects need an explicit wire representation; ArrayBuffers and ports
|
|
1157
|
+
// retain worker_threads' real transfer/detachment semantics.
|
|
1158
|
+
function nativeTransfers( transfer ) { return transfer.filter( value => ! ( value instanceof NativeOffscreenCanvas || value instanceof ImageBitmap || value instanceof VideoFrame ) ); }
|
|
1159
|
+
function pack( value, transfer, owner, seen = new Map() ) {
|
|
1160
|
+
|
|
1161
|
+
if ( value === null || typeof value !== 'object' ) return value;
|
|
1162
|
+
if ( seen.has( value ) ) return seen.get( value );
|
|
1163
|
+
if ( value instanceof NativeOffscreenCanvas ) {
|
|
1164
|
+
|
|
1165
|
+
if ( ! transfer.includes( value ) || value.detached ) throw new DOMException( 'OffscreenCanvas must be transferred exactly once.', 'DataCloneError' );
|
|
1166
|
+
const result = { __tbNative: 'canvas', id: value.surfaceId, width: value.width, height: value.height };
|
|
1167
|
+
value.detached = true; surfaces.get( value ).worker = owner;
|
|
1168
|
+
return result;
|
|
1169
|
+
|
|
1170
|
+
}
|
|
1171
|
+
if ( value instanceof ImageBitmap || value instanceof VideoFrame ) {
|
|
1172
|
+
|
|
1173
|
+
const result = { __tbNative: value instanceof ImageBitmap ? 'bitmap' : 'frame', ...value._toRGBA8(), timestamp: value.timestamp };
|
|
1174
|
+
if ( transfer.includes( value ) ) value.close();
|
|
1175
|
+
return result;
|
|
1176
|
+
|
|
1177
|
+
}
|
|
1178
|
+
if ( ! Array.isArray( value ) && Object.getPrototypeOf( value ) !== Object.prototype ) return value;
|
|
1179
|
+
const result = Array.isArray( value ) ? [] : {};
|
|
1180
|
+
seen.set( value, result );
|
|
1181
|
+
for ( const [ key, child ] of Object.entries( value ) ) result[ key ] = pack( child, transfer, owner, seen );
|
|
1182
|
+
return result;
|
|
1183
|
+
|
|
1184
|
+
}
|
|
1185
|
+
function unpack( value, seen = new Map() ) {
|
|
1186
|
+
|
|
1187
|
+
if ( value === null || typeof value !== 'object' ) return value;
|
|
1188
|
+
if ( seen.has( value ) ) return seen.get( value );
|
|
1189
|
+
if ( value.__tbNative === 'canvas' ) {
|
|
1190
|
+
|
|
1191
|
+
const canvas = new NativeOffscreenCanvas( value.width, value.height );
|
|
1192
|
+
transferredSurfaces.delete( canvas.surfaceId ); canvas.surfaceId = value.id;
|
|
1193
|
+
transferredSurfaces.set( value.id, canvas ); return canvas;
|
|
1194
|
+
|
|
1195
|
+
}
|
|
1196
|
+
if ( value.__tbNative === 'bitmap' ) return new ImageBitmap( value.width, value.height, value.data );
|
|
1197
|
+
if ( value.__tbNative === 'frame' ) return new VideoFrame( value.data, { format: 'RGBA', codedWidth: value.width, codedHeight: value.height, timestamp: value.timestamp } );
|
|
1198
|
+
if ( ! Array.isArray( value ) && Object.getPrototypeOf( value ) !== Object.prototype ) return value;
|
|
1199
|
+
const result = Array.isArray( value ) ? [] : {};
|
|
1200
|
+
seen.set( value, result );
|
|
1201
|
+
for ( const [ key, child ] of Object.entries( value ) ) result[ key ] = unpack( child, seen );
|
|
1202
|
+
return result;
|
|
1203
|
+
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
class NativeWorker extends EventTarget {
|
|
1207
|
+
|
|
1208
|
+
#thread;
|
|
1209
|
+
#pixels = new Map();
|
|
1210
|
+
#id = 0;
|
|
1211
|
+
constructor( url, options = {} ) {
|
|
1212
|
+
|
|
1213
|
+
super();
|
|
1214
|
+
url = new URL( url, pageURL ).href;
|
|
1215
|
+
this.#thread = new Thread( new URL( './worker.mjs', import.meta.url ), {
|
|
1216
|
+
execArgv: [ '--experimental-vm-modules' ], workerData: { url, viewport: pageViewport, thread: options.type ?? 'classic', imports: importMap, scopes: importScopes, routes, gpu: gpuEnabled, cookies: cookieJar.serializeSync(), blob: url.startsWith( 'blob:' ) ? resolveObjectURL( url ) : undefined },
|
|
1217
|
+
} );
|
|
1218
|
+
this.#thread.on( 'message', message => {
|
|
1219
|
+
|
|
1220
|
+
if ( message.importScript ) {
|
|
1221
|
+
|
|
1222
|
+
void ( async () => {
|
|
1223
|
+
|
|
1224
|
+
try {
|
|
1225
|
+
|
|
1226
|
+
const response = await fetchPage( message.importScript, undefined, 'script' );
|
|
1227
|
+
if ( ! response.ok ) throw new Error( `importScripts ${message.importScript}: HTTP ${response.status}` );
|
|
1228
|
+
message.port.postMessage( { source: await response.text() } );
|
|
1229
|
+
|
|
1230
|
+
} catch ( error ) { message.port.postMessage( { error: String( error ) } ); }
|
|
1231
|
+
finally { message.port.close(); Atomics.store( message.signal, 0, 1 ); Atomics.notify( message.signal, 0 ); }
|
|
1232
|
+
|
|
1233
|
+
} )();
|
|
1234
|
+
return;
|
|
1235
|
+
|
|
1236
|
+
}
|
|
1237
|
+
if ( message.native ) {
|
|
1238
|
+
|
|
1239
|
+
if ( message.native.event === 'route' ) {
|
|
1240
|
+
|
|
1241
|
+
const id = ++ routeId;
|
|
1242
|
+
routeRequests.set( id, response => this.#thread.postMessage( { nativeRoute: message.native.id, response } ) );
|
|
1243
|
+
send( { ...message.native, id } );
|
|
1244
|
+
|
|
1245
|
+
} else send( message.native );
|
|
1246
|
+
return;
|
|
1247
|
+
|
|
1248
|
+
}
|
|
1249
|
+
if ( message.pixels ) {
|
|
1250
|
+
|
|
1251
|
+
const request = this.#pixels.get( message.pixels ); this.#pixels.delete( message.pixels );
|
|
1252
|
+
if ( message.error ) request.reject( new Error( message.error ) ); else request.resolve( message.value );
|
|
1253
|
+
return;
|
|
1254
|
+
|
|
1255
|
+
}
|
|
1256
|
+
const event = new MessageEvent( 'message', { data: unpack( message.data ) } );
|
|
1257
|
+
this.onmessage?.( event ); this.dispatchEvent( event );
|
|
1258
|
+
|
|
1259
|
+
} );
|
|
1260
|
+
this.#thread.on( 'error', error => {
|
|
1261
|
+
|
|
1262
|
+
const event = Object.assign( new Event( 'error' ), { message: error.message, error } );
|
|
1263
|
+
this.onerror?.( event ); this.dispatchEvent( event ); failure( error );
|
|
1264
|
+
|
|
1265
|
+
} );
|
|
1266
|
+
|
|
1267
|
+
}
|
|
1268
|
+
postMessage( value, transfer = [] ) {
|
|
1269
|
+
|
|
1270
|
+
if ( ! Array.isArray( transfer ) ) transfer = transfer.transfer ?? [];
|
|
1271
|
+
this.#thread.postMessage( { data: pack( value, transfer, this ) }, nativeTransfers( transfer ) );
|
|
1272
|
+
|
|
1273
|
+
}
|
|
1274
|
+
readPixels( canvas ) {
|
|
1275
|
+
|
|
1276
|
+
const id = ++ this.#id;
|
|
1277
|
+
return new Promise( ( resolve, reject ) => { this.#pixels.set( id, { resolve, reject } ); this.#thread.postMessage( { readPixels: canvas, id } ); } );
|
|
1278
|
+
|
|
1279
|
+
}
|
|
1280
|
+
terminate() {
|
|
1281
|
+
|
|
1282
|
+
for ( const request of this.#pixels.values() ) request.reject( new Error( 'Render worker terminated.' ) );
|
|
1283
|
+
this.#pixels.clear(); return this.#thread.terminate();
|
|
1284
|
+
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if ( parentPort ) {
|
|
1290
|
+
|
|
1291
|
+
const queued = [];
|
|
1292
|
+
let ready = false;
|
|
1293
|
+
const receive = async message => {
|
|
1294
|
+
|
|
1295
|
+
if ( message.nativeRoute ) { routeRequests.get( message.nativeRoute )?.( message.response ); routeRequests.delete( message.nativeRoute ); return; }
|
|
1296
|
+
|
|
1297
|
+
if ( ! ready ) { queued.push( message ); return; }
|
|
1298
|
+
if ( message.readPixels ) {
|
|
1299
|
+
|
|
1300
|
+
try { parentPort.postMessage( { pixels: message.id, value: await readPixels( transferredSurfaces.get( message.readPixels ) ) } ); }
|
|
1301
|
+
catch ( error ) { parentPort.postMessage( { pixels: message.id, error: error.stack } ); }
|
|
1302
|
+
return;
|
|
1303
|
+
|
|
1304
|
+
}
|
|
1305
|
+
const event = new MessageEvent( 'message', { data: unpack( message.data ) } );
|
|
1306
|
+
globalThis.onmessage?.( event ); globalThis.dispatchEvent( event );
|
|
1307
|
+
|
|
1308
|
+
};
|
|
1309
|
+
parentPort.on( 'message', receive );
|
|
1310
|
+
await navigate( workerData );
|
|
1311
|
+
ready = true;
|
|
1312
|
+
for ( const message of queued ) await receive( message );
|
|
1313
|
+
|
|
1314
|
+
} else process.on( 'message', message => command( jsonIPC ? decodeIPC( message ) : message ) );
|